Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schedule page stores #729

Merged
merged 6 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export enum DateFormatStrings {
// Time display formats
Display12Hour = 'hh:mma',
Display24Hour = 'HH:mm',
// Other formats
UniqueMonth = 'YYYY-MM',
}

/**
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ export type ExceptionDetail = {
status?: number;
}
export type PydanticExceptionDetail = {
ctx: { reason: string },
ctx: { reason: string, ge?: string },
input: string,
loc: string[],
msg: string,
Expand Down Expand Up @@ -333,7 +333,7 @@ export type Fetch = (url: string) => UseFetchReturn<any> & PromiseLike<UseFetchR
export type InviteListResponse = UseFetchReturn<Invite[]|Exception>;
export type Refresh = () => Promise<void>;
export type RemoteEventListResponse = UseFetchReturn<RemoteEvent[]>;
export type ScheduleResponse = UseFetchReturn<Schedule>;
export type ScheduleResponse = UseFetchReturn<Schedule|Exception>;
export type ScheduleListResponse = UseFetchReturn<Schedule[]>;
export type SignatureResponse = UseFetchReturn<Signature>;
export type SlotResponse = UseFetchReturn<Slot|Exception>;
Expand Down
65 changes: 62 additions & 3 deletions frontend/src/stores/calendar-store.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import { Calendar, CalendarListResponse, Fetch } from '@/models';
import { Calendar, CalendarListResponse, Fetch, RemoteEvent, RemoteEventListResponse } from '@/models';
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { ref, computed, inject } from 'vue';
import { dayjsKey } from '@/keys';
import { Dayjs } from 'dayjs';
import { DateFormatStrings } from '@/definitions';

// eslint-disable-next-line import/prefer-default-export
export const useCalendarStore = defineStore('calendars', () => {
const dj = inject(dayjsKey);

// State
const isLoaded = ref(false);

// Data
const calendars = ref<Calendar[]>([]);
const unconnectedCalendars = computed((): Calendar[] => calendars.value.filter((cal) => !cal.connected));
const connectedCalendars = computed((): Calendar[] => calendars.value.filter((cal) => cal.connected));

const hasConnectedCalendars = computed(() => connectedCalendars.value.length > 0);

// List of remote events. Retrieved in batches per month.
const remoteEvents = ref<RemoteEvent[]>([]);
// List of month batches already called.
const remoteMonthsRetrieved = ref<string[]>([]);

const connectGoogleCalendar = async (call: Fetch, email: string) => {
const urlFriendlyEmail = encodeURIComponent(email);
const googleUrl = await call(`google/auth?email=${urlFriendlyEmail}`).get();
Expand Down Expand Up @@ -44,6 +53,52 @@ export const useCalendarStore = defineStore('calendars', () => {
}
};

/**
* Get all cremote events from connected calendars for given time span
* @param call preconfigured API fetch function
* @param activeDate Dayjs object defining the current month
* @param force force a refetch
*/
const getRemoteEvents = async (call: Fetch, activeDate: Dayjs, force = false) => {
// Get month identifier to remember this month's events are already retrieved
const month = activeDate.format(DateFormatStrings.UniqueMonth);

// Most calendar impl are non-inclusive of the last day, so just add one day to the end.
const from = activeDate.startOf('month').format(DateFormatStrings.QalendarFullDay);
const to = activeDate.endOf('month').add(1, 'day').format(DateFormatStrings.QalendarFullDay);

// If retrieval is forced, delete cache and start with zero events again
if (force) {
remoteMonthsRetrieved.value = [];
}

// If month is already cached, there's nothing more to do
if (remoteMonthsRetrieved.value.includes(month)) {
return;
}

const calendarEvents = force ? [] : [...remoteEvents.value];

// Only retrieve remote events if we don't have this month already cached
await Promise.all(connectedCalendars.value.map(async (calendar) => {
const { data }: RemoteEventListResponse = await call(`rmt/cal/${calendar.id}/${from}/${to}`).get().json();
if (Array.isArray(data.value)) {
calendarEvents.push(
...data.value.map((event) => ({
...event,
duration: dj(event.end).diff(dj(event.start), 'minutes'),
})),
);
}
}));

// Remember month
remoteMonthsRetrieved.value.push(month);

// Update remote event list
remoteEvents.value = calendarEvents;
};

/**
* Restore default state, empty and unload calendars
*/
Expand All @@ -55,9 +110,11 @@ export const useCalendarStore = defineStore('calendars', () => {
const connectCalendar = async (call: Fetch, id: number) => {
await call(`cal/${id}/connect`).post();
};

const disconnectCalendar = async (call: Fetch, id: number) => {
await call(`cal/${id}/disconnect`).post();
};

const syncCalendars = async (call: Fetch) => {
await call('rmt/sync').post();
};
Expand All @@ -68,12 +125,14 @@ export const useCalendarStore = defineStore('calendars', () => {
calendars,
unconnectedCalendars,
connectedCalendars,
remoteEvents,
fetch,
$reset,
connectGoogleCalendar,
connectCalendar,
disconnectCalendar,
syncCalendars,
calendarById,
getRemoteEvents,
};
});
65 changes: 54 additions & 11 deletions frontend/src/stores/schedule-store.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { i18n } from '@/composables/i18n';
import { defineStore } from 'pinia';
import { ref, computed, inject } from 'vue';
import { ref, computed, inject, Ref } from 'vue';
import { useUserStore } from '@/stores/user-store';
import { DateFormatStrings, MetricEvents } from '@/definitions';
import {
Error, Fetch, Schedule, ScheduleListResponse, ScheduleResponse,
DateFormatStrings,
MetricEvents,
DEFAULT_SLOT_DURATION,
EventLocationType,
MeetingLinkProviderType,
} from '@/definitions';
import {
Error, Fetch, Schedule, ScheduleListResponse, ScheduleResponse, Exception, ExceptionDetail
} from '@/models';
import { dayjsKey } from '@/keys';
import { posthog, usePosthog } from '@/composables/posthog';
Expand All @@ -14,16 +20,42 @@ import { timeFormat } from '@/utils';
export const useScheduleStore = defineStore('schedules', () => {
const dj = inject(dayjsKey);

const defaultSchedule = {
active: false,
name: '',
calendar_id: 0,
location_type: EventLocationType.InPerson,
location_url: '',
details: '',
start_date: dj().format(DateFormatStrings.QalendarFullDay),
end_date: null,
start_time: '09:00',
end_time: '17:00',
earliest_booking: 1440,
farthest_booking: 20160,
weekdays: [1, 2, 3, 4, 5],
slot_duration: DEFAULT_SLOT_DURATION,
meeting_link_provider: MeetingLinkProviderType.None,
booking_confirmation: true,
calendar: {
id: 0,
title: '',
color: '#000',
connected: true,
},
};

// State
const isLoaded = ref(false);

// Data
const schedules = ref<Schedule[]>([]);
const firstSchedule = computed((): Schedule => schedules.value?.length > 0 ? schedules.value[0] : null);
const inactiveSchedules = computed((): Schedule[] => schedules.value.filter((schedule) => !schedule.active));
const activeSchedules = computed((): Schedule[] => schedules.value.filter((schedule) => schedule.active));

/**
* Get all calendars for current user
* Get all schedules for current user
* @param call preconfigured API fetch function
* @param force Force a fetch even if we already have data
*/
Expand All @@ -43,18 +75,18 @@ export const useScheduleStore = defineStore('schedules', () => {
};

/**
* Restore default state, empty and unload calendars
* Restore default state, empty and unload schedules
devmount marked this conversation as resolved.
Show resolved Hide resolved
*/
const $reset = () => {
schedules.value = [];
isLoaded.value = false;
};

const handleErrorResponse = (responseData) => {
const handleErrorResponse = (responseData: Ref<Exception>) => {
const { value } = responseData;

if (value?.detail?.message) {
return value?.detail?.message;
if ((value?.detail as ExceptionDetail)?.message) {
return (value?.detail as ExceptionDetail)?.message;
}

if (value?.detail instanceof Array) {
Expand Down Expand Up @@ -108,7 +140,7 @@ export const useScheduleStore = defineStore('schedules', () => {
if (error.value) {
return {
error: true,
message: handleErrorResponse(data),
message: handleErrorResponse(data as Ref<Exception>),
} as Error;
}

Expand All @@ -129,7 +161,7 @@ export const useScheduleStore = defineStore('schedules', () => {
if (error.value) {
return {
error: true,
message: handleErrorResponse(data),
message: handleErrorResponse(data as Ref<Exception>),
} as Error;
}

Expand Down Expand Up @@ -172,6 +204,17 @@ export const useScheduleStore = defineStore('schedules', () => {
};

return {
isLoaded, schedules, inactiveSchedules, activeSchedules, fetch, $reset, createSchedule, updateSchedule, timeToBackendTime, timeToFrontendTime,
isLoaded,
defaultSchedule,
schedules,
firstSchedule,
inactiveSchedules,
activeSchedules,
fetch,
$reset,
createSchedule,
updateSchedule,
timeToBackendTime,
timeToFrontendTime,
};
});
Loading