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

Additional Stores #215

Merged
merged 8 commits into from
Dec 15, 2023
106 changes: 33 additions & 73 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@
</site-notification>
<nav-bar :nav-items="navItems" />
<main :class="{'mx-4 pt-24 lg:mx-8 min-h-full pb-24': !routeIsHome, 'pt-32': routeIsHome}">
<router-view
:calendars="calendars"
:appointments="appointments"
/>
<router-view />
</main>
<footer-bar />
</template>
Expand All @@ -32,23 +29,25 @@
</template>

<script setup>
import { appointmentState } from "@/definitions";
import { createFetch } from "@vueuse/core";
import { ref, inject, provide, onMounted, computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import NavBar from "@/components/NavBar";
import TitleBar from "@/components/TitleBar";
import FooterBar from "@/components/FooterBar.vue";
import SiteNotification from "@/elements/SiteNotification";
import { useSiteNotificationStore } from "@/stores/alert-store";
import { createFetch } from '@vueuse/core';
import {
inject, provide, onMounted, computed,
} from 'vue';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you use a line length limitation < 120? Just wondering about the line breaks here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to fix this up as my ide is at odds with eslint. 😵

import { useRoute, useRouter } from 'vue-router';
import NavBar from '@/components/NavBar';
import TitleBar from '@/components/TitleBar';
import FooterBar from '@/components/FooterBar.vue';
import SiteNotification from '@/elements/SiteNotification';
import { useSiteNotificationStore } from '@/stores/alert-store';

// stores
import { useUserStore } from '@/stores/user-store';
import { useCalendarStore } from '@/stores/calendar-store';
import { useAppointmentStore } from '@/stores/appointment-store';

// component constants
const currentUser = useUserStore(); // data: { username, email, name, level, timezone, id }
const apiUrl = inject("apiUrl");
const dj = inject("dayjs");
const apiUrl = inject('apiUrl');
const route = useRoute();
const router = useRouter();
const siteNotificationStore = useSiteNotificationStore();
Expand All @@ -68,8 +67,8 @@ const call = createFetch({
async onFetchError({ data, response, error }) {
// Catch any google refresh error that may occur
if (
data?.detail?.error === 'google_refresh_error' &&
!siteNotificationStore.isSameNotification('google_refresh_error')
data?.detail?.error === 'google_refresh_error'
&& !siteNotificationStore.isSameNotification('google_refresh_error')
) {
// Ensure other async calls don't reach here
siteNotificationStore.lock(data.detail.error);
Expand Down Expand Up @@ -97,26 +96,26 @@ const call = createFetch({
},
},
fetchOptions: {
mode: "cors",
credentials: "include",
mode: 'cors',
credentials: 'include',
},
});
provide("call", call);
provide('call', call);
provide('isPasswordAuth', import.meta.env?.VITE_AUTH_SCHEME === 'password');
provide('isFxaAuth', import.meta.env?.VITE_AUTH_SCHEME === 'fxa');
provide('fxaEditProfileUrl', import.meta.env?.VITE_FXA_EDIT_PROFILE);

// menu items for main navigation
const navItems = [
"calendar",
"schedule",
"appointments",
"settings",
'calendar',
'schedule',
'appointments',
'settings',
];

// db tables
const calendars = ref([]);
const appointments = ref([]);
const calendarStore = useCalendarStore();
const appointmentStore = useAppointmentStore();

// true if route can be accessed without authentication
const routeIsPublic = computed(
Expand All @@ -126,65 +125,26 @@ const routeIsHome = computed(
() => ['home'].includes(route.name),
);

// query db for all calendar data
const getDbCalendars = async (onlyConnected = true) => {
const { data, error } = await call(`me/calendars?only_connected=${onlyConnected}`).get().json();
if (!error.value) {
if (data.value === null || typeof data.value === "undefined") return;
calendars.value = data.value;
}
};
// query db for all appointments data
const getDbAppointments = async () => {
const { data, error } = await call("me/appointments").get().json();
if (!error.value) {
if (data.value === null || typeof data.value === "undefined") return;
appointments.value = data.value;
}
};

// check appointment status for current state (past|pending|booked)
const getAppointmentStatus = (a) => {
// check past events
if (a.slots.filter((s) => dj(s.start).isAfter(dj())).length === 0) {
return appointmentState.past;
}
// check booked events
if (a.slots.filter((s) => s.attendee_id != null).length > 0) {
return appointmentState.booked;
}
// else event is still wating to be booked
return appointmentState.pending;
};
const getAppointmentStatus = (a) => appointmentStore.status(a);

// extend retrieved data
const extendDbData = () => {
// build { calendarId => calendarData } object for direct lookup
const calendarsById = {};
calendars.value.forEach((c) => {
calendarStore.allCalendars.forEach((c) => {
calendarsById[c.id] = c;
});
// extend appointments data with active state and calendar title and color
appointments.value.forEach((a) => {
a.calendar_title = calendarsById[a.calendar_id]?.title;
a.calendar_color = calendarsById[a.calendar_id]?.color;
a.status = getAppointmentStatus(a);
a.active = a.status !== appointmentState.past; // TODO
// convert start dates from UTC back to users timezone
a.slots.forEach((s) => {
s.start = dj.utc(s.start).tz(currentUser.data.timezone ?? dj.tz.guess());
});
});
appointmentStore.mergeCalendarInfo(calendarsById);
};

// retrieve calendars and appointments after checking login and persisting user to db
const getDbData = async (options = {}) => {
const { onlyConnectedCalendars = true } = options;

const getDbData = async () => {
if (currentUser?.exists()) {
await Promise.all([
getDbCalendars(onlyConnectedCalendars),
getDbAppointments(),
calendarStore.fetch(call),
appointmentStore.fetch(call),
]);
extendDbData();
}
Expand All @@ -196,6 +156,6 @@ onMounted(async () => {
});

// provide refresh functions for components
provide("refresh", getDbData);
provide("getAppointmentStatus", getAppointmentStatus);
provide('refresh', getDbData);
provide('getAppointmentStatus', getAppointmentStatus);
</script>
21 changes: 8 additions & 13 deletions frontend/src/components/SettingsAccount.vue
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@

<script setup>
import { ref, inject, onMounted, computed } from 'vue';
import { useAuth0 } from '@auth0/auth0-vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user-store';
Expand All @@ -164,17 +163,18 @@ import TextButton from '@/elements/TextButton.vue';
// icons
import { IconExternalLink } from '@tabler/icons-vue';

// stores
import { useExternalConnectionsStore } from '@/stores/external-connections-store';

// component constants
const { t } = useI18n({ useScope: 'global' });
const call = inject('call');
const refresh = inject('refresh');
const router = useRouter();
const user = useUserStore();
const logout = inject('logout');
const externalConnectionsStore = useExternalConnectionsStore();

const externalConnections = ref({});
const hasZoomAccountConnected = computed(() => (externalConnections.value?.zoom?.length ?? []) > 0);
const zoomAccountName = computed(() => (externalConnections.value?.zoom[0].name ?? null));
const hasZoomAccountConnected = computed(() => (externalConnectionsStore.zoom.length) > 0);
const zoomAccountName = computed(() => (externalConnectionsStore.zoom[0].name ?? null));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we only allow one zoom connection, any objection to the zoom getter just grabbing the first item?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I think this is fine. Maybe adding a comment indication that we currently only handle one zoom connection would make things clearer here. Is externalConnectionsStore.zoom[0].name ?? null sufficient or do we need another ? before the .name, in case no connection is defined?


const activeUsername = ref(user.data.username);
const activeDisplayName = ref(user.data.name);
Expand Down Expand Up @@ -205,15 +205,9 @@ const getSignedUserUrl = async () => {
signedUserUrl.value = data.value.url;
};

const getExternalConnections = async () => {
const { data } = await call('account/external-connections').get().json();
externalConnections.value = data.value;
};

const refreshData = async () => Promise.all([
getSignedUserUrl(),
getExternalConnections(),
refresh(),
externalConnectionsStore.fetch(call),
]);

// save user data
Expand Down Expand Up @@ -261,6 +255,7 @@ const connectZoom = async () => {
};
const disconnectZoom = async () => {
await call('zoom/disconnect').post();
await useExternalConnectionsStore().reset();
await refreshData();
};

Expand Down
23 changes: 11 additions & 12 deletions frontend/src/components/SettingsCalendar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<calendar-management
:title="t('heading.calendarsUnconnected')"
:type="calendarManagementType.connect"
:calendars="calendars"
:calendars="calendarStore.unconnectedCalendars"
:loading="loading"
@sync="syncCalendars"
@modify="connectCalendar"
Expand All @@ -18,7 +18,7 @@
<calendar-management
:title="t('heading.calendarsConnected')"
:type="calendarManagementType.edit"
:calendars="calendars"
:calendars="calendarStore.connectedCalendars"
:loading="loading"
@remove="deleteCalendar"
@modify="editCalendar"
Expand Down Expand Up @@ -199,7 +199,9 @@
<script setup>
import { calendarManagementType } from '@/definitions';
import { IconArrowRight } from '@tabler/icons-vue';
import { ref, reactive, inject, onMounted, computed } from 'vue';
import {
ref, reactive, inject, onMounted, computed,
} from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import AlertBox from '@/elements/AlertBox';
Expand All @@ -208,12 +210,14 @@ import GoogleSignInBtn from '@/assets/img/google/1x/btn_google_signin_light_norm
import GoogleSignInBtn2x from '@/assets/img/google/2x/[email protected]';
import PrimaryButton from '@/elements/PrimaryButton';
import SecondaryButton from '@/elements/SecondaryButton';
import ConfirmationModal from "@/components/ConfirmationModal.vue";
import ConfirmationModal from '@/components/ConfirmationModal.vue';
import { useCalendarStore } from '@/stores/calendar-store';

// component constants
const { t } = useI18n({ useScope: 'global' });
const call = inject('call');
const refresh = inject('refresh');
const calendarStore = useCalendarStore();

const calendarConnectError = ref('');

Expand All @@ -223,11 +227,6 @@ const deleteCalendarModalTarget = ref(null);
// Temp until we get a store solution rolling
const loading = ref(false);

// view properties
defineProps({
calendars: Array, // list of calendars from db
});

// handle calendar user input to add or edit calendar connections
const inputModes = {
hidden: 0,
Expand Down Expand Up @@ -264,7 +263,9 @@ const closeModals = async () => {
};

const refreshData = async () => {
await refresh({ onlyConnectedCalendars: false });
// Invalidate our calendar store
await calendarStore.reset();
await refresh();
loading.value = false;
};

Expand Down Expand Up @@ -391,7 +392,5 @@ onMounted(async () => {
calendarConnectError.value = route.query.error;
await router.replace(route.path);
}

await refreshData();
});
</script>
4 changes: 2 additions & 2 deletions frontend/src/stores/alert-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const initialSiteNotificationObject = {
// eslint-disable-next-line import/prefer-default-export
export const useSiteNotificationStore = defineStore('siteNotification', {
state: () => ({
data: initialSiteNotificationObject,
data: structuredClone(initialSiteNotificationObject),
}),
getters: {
isVisible() {
Expand Down Expand Up @@ -50,7 +50,7 @@ export const useSiteNotificationStore = defineStore('siteNotification', {
});
},
reset() {
this.$patch({ data: initialSiteNotificationObject });
this.$patch({ data: structuredClone(initialSiteNotificationObject) });
},
},
});
66 changes: 66 additions & 0 deletions frontend/src/stores/appointment-store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { defineStore } from 'pinia';
import { appointmentState } from '@/definitions';
import { useUserStore } from '@/stores/user-store';
import dj from 'dayjs';

const initialData = {
appointments: [],
isInit: false,
};

// eslint-disable-next-line import/prefer-default-export
export const useAppointmentStore = defineStore('appointments', {
state: () => ({
data: structuredClone(initialData),
}),
getters: {
isLoaded() {
return this.data.isInit;
},
appointments() {
return this.data.appointments;
},
pendingAppointments() {
return this.data.appointments.filter((a) => a.status === appointmentState.pending);
},
},
actions: {
status(appointment) {
// check past events
if (appointment.slots.filter((s) => dj(s.start).isAfter(dj())).length === 0) {
return appointmentState.past;
}
// check booked events
if (appointment.slots.filter((s) => s.attendee_id != null).length > 0) {
return appointmentState.booked;
}
// else event is still wating to be booked
return appointmentState.pending;
},
reset() {
this.$patch({ data: structuredClone(initialData) });
},
async mergeCalendarInfo(calendarsById) {
const userStore = useUserStore();

this.data.appointments.forEach((a) => {
a.calendar_title = calendarsById[a.calendar_id]?.title;
a.calendar_color = calendarsById[a.calendar_id]?.color;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At some point we can improve this by appending the corresponding calendar object to the appointment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And done!

a.status = this.status(a);
a.active = a.status !== appointmentState.past; // TODO
// 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());
});
});
},
async fetch(call) {
const { data, error } = await call('me/appointments').get().json();
if (!error.value) {
if (data.value === null || typeof data.value === 'undefined') return;
this.data.appointments = data.value;
this.data.isInit = true;
}
},
},
});
Loading