Skip to content

Commit

Permalink
Schedule page stores (#729)
Browse files Browse the repository at this point in the history
* ➕ Utilize existing schedule store

* 🔨 Type fixes

* ➕ Cache remote events per month on dashboard

* 🔨 Active date improvements

* 🔨 Apply review feedback

* 🔨 Move default schedule to store
  • Loading branch information
devmount authored Oct 31, 2024
1 parent 63c97f1 commit e6fe014
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 112 deletions.
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
*/
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

0 comments on commit e6fe014

Please sign in to comment.