Skip to content

Commit

Permalink
Stores TypeScript (#499)
Browse files Browse the repository at this point in the history
* 🔨 Type alert store

* 🔨 Type appointment store

* 🔨 Fix dayjs type

* 🔨 Type booking modal store

* 🔨 Fix dayjs timezone conversion

* 🔨 We don't striclty check for null

* 🔨 Type booking view store

* 🔨 Type calendar store

* 🔨 Type external connections store

* 🔨 Type schedule store

* 🔨 Type user store

* ➕ Add types for API fetch and return

* 🔨 Fix use fetch types

* 🔨 Fix linter issue

* 🔨 Rename FetchAny to Fetch
  • Loading branch information
devmount authored Jun 27, 2024
1 parent 39386e7 commit bc84434
Show file tree
Hide file tree
Showing 13 changed files with 235 additions and 85 deletions.
2 changes: 1 addition & 1 deletion backend/src/appointment/routes/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def get_external_connections(subscriber: Subscriber = Depends(get_subscriber)):
external_connections = defaultdict(list)

if os.getenv('ZOOM_API_ENABLED'):
external_connections['Zoom'] = []
external_connections['zoom'] = []

for ec in subscriber.external_connections:
external_connections[ec.type.name].append(
Expand Down
3 changes: 2 additions & 1 deletion frontend/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module.exports = {
rules: {
'import/extensions': ['error', 'ignorePackages', {
'': 'never',
ts: 'never',
js: 'never',
vue: 'off',
}],
Expand All @@ -37,7 +38,7 @@ module.exports = {
map: [
['@', './src'],
],
extensions: ['.js', '.vue'],
extensions: ['.ts', '.js', '.vue'],
},
},
},
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/CalendarQalendar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ const processCalendarColorScheme = (calendarTitle, calendarColor) => {
* @param d - DayJS date object
* @returns {*}
*/
const applyTimezone = (d) => dj.utc(d).tz(dj.tz.guess());
const applyTimezone = (d) => dj(d).utc().tz(dj.tz.guess());
/**
* Generates a list of Qalendar friendly event objects from events and appointments props.
Expand Down
1 change: 1 addition & 0 deletions frontend/src/composables/dayjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export default function useDayJS(app: App<Element>, locale: string) {
// provide the configured dayjs instance as well es some helper functions
// TODO: provide method to live update the dayjs locale
app.provide('dayjs', dayjs);
app.provide('tzGuess', dayjs.tz.guess());

const hDuration = (m: number): string => ((m < 60)
? dayjs.duration(m, 'minutes').humanize()
Expand Down
144 changes: 144 additions & 0 deletions frontend/src/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { Dayjs } from "dayjs";
import { UseFetchReturn } from '@vueuse/core';

export type Attendee = {
id: number;
email: string;
name: string;
timezone: string;
}

export type Slot = {
id: number;
start: Dayjs;
duration: number;
attendee_id: number;
booking_tkn: string;
booking_expires_at: string;
booking_status: number;
meeting_link_id: number;
meeting_link_url: string;
appointment_id: number;
subscriber_id: number;
time_updated: string;
attendee: Attendee;
}

export type Appointment = {
id: number;
title: string;
details: string;
slug: string;
location_url: string;
calendar_id: number;
duration: number;
location_type: number;
location_suggestions: string;
location_selected: number;
location_name: string;
location_phone: string;
keep_open: boolean;
status: number;
meeting_link_provider: string;
uuid: string;
time_created: string;
time_updated: string;
slots: Slot[];
calendar_title: string;
calendar_color: string;
active: boolean;
};

export type Calendar = {
id?: number;
connected: boolean;
title: string;
color: string;
};

export type ExternalConnection = {
owner_id: number;
name: string;
type: string;
type_id: string;
};

export type ExternalConnectionCollection = {
fxa?: ExternalConnection[];
google?: ExternalConnection[];
zoom?: ExternalConnection[];
};

// This will be used later if we provide custom availabilities
// in addition to general availability too
export type Availability = {
id?: number;
};

export type Schedule = {
active: boolean;
name: string;
slug: string;
calendar_id: number;
location_type: number;
location_url: string;
details: string;
start_date: string;
end_date: string;
start_time: string;
end_time: string;
earliest_booking: number;
farthest_booking: number;
weekdays: number[];
slot_duration: number;
meeting_link_provider: string;
id: number;
time_created: string;
time_updated: string;
availabilities?: Availability[];
calendar: Calendar;
};

export type User = {
email: string;
preferredEmail: string;
level: number;
name: string;
timezone: string;
username: string;
signedUrl: string;
avatarUrl: string;
accessToken: string;
scheduleSlugs: string[];
}

export type Subscriber = {
username: string;
name: string;
email: string;
preferred_email: string;
level: number;
timezone: string;
avatar_url: string;
}

export type Signature = {
url: string;
}

// Types and aliases used for our custom createFetch API calls and return types
export type Fetch = (url: string) => UseFetchReturn<any> & PromiseLike<UseFetchReturn<any>>;
export type BooleanResponse = UseFetchReturn<boolean>;
export type SignatureResponse = UseFetchReturn<Signature>;
export type SubscriberResponse = UseFetchReturn<Subscriber>;
export type TokenResponse = UseFetchReturn<Token>;
export type AppointmentListResponse = UseFetchReturn<Appointment[]>;
export type CalendarListResponse = UseFetchReturn<Calendar[]>;
export type ScheduleListResponse = UseFetchReturn<Schedule[]>;
export type ExternalConnectionCollectionResponse = UseFetchReturn<ExternalConnectionCollection>;

export type Error = { error: boolean|string|null };
export type Token = {
access_token: string;
token_type: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,24 @@ export const useSiteNotificationStore = defineStore('siteNotification', () => {

/**
* Check a given id if it is currently locked
* @param {string} checkId notification id to check agains current id
* @returns {boolean}
* @param checkId notification id to check agains current id
*/
const isSame = (checkId) => id.value === checkId;
const isSame = (checkId: string) => id.value === checkId;

/**
* Lock the given id
* @param {string} lockId notification id to lock in
* @param lockId notification id to lock in
*/
const lock = (lockId) => { id.value = lockId; };
const lock = (lockId: string) => { id.value = lockId; };

/**
* Make a notification with given configuration appear
* @param {string} showId notification identifier
* @param {string} showTitle notification title
* @param {string} showMessage notification message
* @param {string} showActionUrl target url if notification should be a link
* @param showId notification identifier
* @param showTitle notification title
* @param showMessage notification message
* @param showActionUrl target url if notification should be a link
*/
const show = (showId, showTitle, showMessage, showActionUrl) => {
const show = (showId: string, showTitle: string, showMessage: string, showActionUrl: string) => {
isVisible.value = true;
id.value = showId;
title.value = showTitle;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { Dayjs, ConfigType } from 'dayjs';
import { defineStore } from 'pinia';
import { ref, computed, inject } from 'vue';
import { bookingStatus } from '@/definitions';
import { useUserStore } from '@/stores/user-store';
import { Appointment, AppointmentListResponse, Fetch, Slot } from '@/models';

// eslint-disable-next-line import/prefer-default-export
export const useAppointmentStore = defineStore('appointments', () => {
const dj = inject('dayjs');
const dj = inject<(date?: ConfigType) => Dayjs>('dayjs');
const tzGuess = inject<string>('tzGuess');

// State
const isLoaded = ref(false);

// Data
const appointments = ref([]);
const appointments = ref<Appointment[]>([]);
const pendingAppointments = computed(
() => appointments.value.filter((a) => a?.slots[0]?.booking_status === bookingStatus.requested),
(): Appointment[] => appointments.value.filter((a) => a?.slots[0]?.booking_status === bookingStatus.requested),
);

/**
Expand All @@ -25,18 +28,18 @@ export const useAppointmentStore = defineStore('appointments', () => {
appointments.value.forEach((a) => {
a.active = a.status !== bookingStatus.booked;
// convert start dates from UTC back to users timezone
a.slots.forEach((s) => {
s.start = dj.utc(s.start).tz(userStore.data.timezone ?? dj.tz.guess());
a.slots.forEach((s: Slot) => {
s.start = dj(s.start).utc().tz(userStore.data.timezone ?? tzGuess);
});
});
};

/**
* Get all appointments for current user
* @param {function} call preconfigured API fetch function
* @param call preconfigured API fetch function
*/
const fetch = async (call) => {
const { data, error } = await call('me/appointments').get().json();
const fetch = async (call: Fetch) => {
const { data, error }: AppointmentListResponse = await call('me/appointments').get().json();
if (!error.value) {
if (data.value === null || typeof data.value === 'undefined') return;
appointments.value = data.value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { modalStates } from '@/definitions';
export const useBookingModalStore = defineStore('bookingModal', () => {
const open = ref(false);
const state = ref(modalStates.open);
const stateData = ref(null);
const stateData = ref<string>(null);

const isLoading = computed(() => state.value === modalStates.loading);
const isFinished = computed(() => state.value === modalStates.finished);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { Dayjs, ConfigType } from 'dayjs';
import { defineStore } from 'pinia';
import { ref, inject } from 'vue';
import { bookingCalendarViews } from '@/definitions';
import { Appointment, Attendee } from '@/models';

/**
* Store for BookingView and its tightly coupled components.
*/
// eslint-disable-next-line import/prefer-default-export
export const useBookingViewStore = defineStore('bookingView', () => {
const dj = inject('dayjs');
const dj = inject<(date?: ConfigType) => Dayjs>('dayjs');

// States
const activeView = ref(bookingCalendarViews.loading);
const activeDate = ref(dj());
// Data
const selectedEvent = ref(null);
const appointment = ref(null);
const attendee = ref(null);
const selectedEvent = ref<Dayjs>(null);
const appointment = ref<Appointment>(null);
const attendee = ref<Attendee>(null);

/**
* Restore default state, set date to today and remove other data
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Calendar, CalendarListResponse, Fetch } from '@/models';
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

Expand All @@ -7,22 +8,22 @@ export const useCalendarStore = defineStore('calendars', () => {
const isLoaded = ref(false);

// Data
const calendars = ref([]);
const unconnectedCalendars = computed(() => calendars.value.filter((cal) => !cal.connected));
const connectedCalendars = computed(() => calendars.value.filter((cal) => cal.connected));
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);

/**
* Get all calendars for current user
* @param {function} call preconfigured API fetch function
* @param call preconfigured API fetch function
*/
const fetch = async (call) => {
const fetch = async (call: Fetch) => {
if (isLoaded.value) {
return;
}

const { data, error } = await call('me/calendars?only_connected=false').get().json();
const { data, error }: CalendarListResponse = await call('me/calendars?only_connected=false').get().json();
if (!error.value) {
if (data.value === null || typeof data.value === 'undefined') return;
calendars.value = data.value;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ExternalConnection, ExternalConnectionCollection, Fetch, ExternalConnectionCollectionResponse } from '@/models';
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

Expand All @@ -7,10 +8,10 @@ export const useExternalConnectionsStore = defineStore('externalConnections', ()
const isLoaded = ref(false);

// Data
const zoom = ref([]);
const fxa = ref([]);
const google = ref([]);
const connections = computed(() => ({
const zoom = ref<ExternalConnection[]>([]);
const fxa = ref<ExternalConnection[]>([]);
const google = ref<ExternalConnection[]>([]);
const connections = computed((): ExternalConnectionCollection => ({
// FXA should be at the top since it represents the Appointment subscriber.
fxa: fxa.value,
google: google.value,
Expand All @@ -19,14 +20,14 @@ export const useExternalConnectionsStore = defineStore('externalConnections', ()

/**
* Get all external connections for current user
* @param {function} call preconfigured API fetch function
* @param call preconfigured API fetch function
*/
const fetch = async (call) => {
const fetch = async (call: Fetch) => {
if (isLoaded.value) {
return;
}

const { data } = await call('account/external-connections').get().json();
const { data }: ExternalConnectionCollectionResponse = await call('account/external-connections').get().json();
zoom.value = data.value?.zoom ?? [];
fxa.value = data.value?.fxa ?? [];
google.value = data.value?.google ?? [];
Expand Down
Loading

0 comments on commit bc84434

Please sign in to comment.