Skip to content

Commit

Permalink
implemented autosave functionality, made as a composable for reusabil…
Browse files Browse the repository at this point in the history
…ity, can be applied to any form with save draft capabilities
  • Loading branch information
qhanson55 committed Jul 30, 2024
1 parent b0bd1fa commit 7609b5d
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 22 deletions.
21 changes: 18 additions & 3 deletions frontend/src/components/housing/enquiry/EnquiryIntakeForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { onBeforeMount, ref, toRaw } from 'vue';
import { useRouter } from 'vue-router';
import { object, string } from 'yup';
import { Dropdown, InputMask, RadioList, InputText, StepperNavigation, TextArea } from '@/components/form';
import { Dropdown, InputMask, InputText, RadioList, StepperNavigation, TextArea } from '@/components/form';
import CollectionDisclaimer from '@/components/housing/CollectionDisclaimer.vue';
import EnquiryIntakeConfirmation from '@/components/housing/enquiry/EnquiryIntakeConfirmation.vue';
import { Button, Card, Divider, useConfirm, useToast } from '@/lib/primevue';
import { useAutoSave } from '@/composables/formAutoSave';
import { enquiryService, submissionService } from '@/services';
import { useConfigStore } from '@/store';
import { YES_NO_LIST } from '@/utils/constants/application';
Expand All @@ -19,6 +20,13 @@ import { confirmationTemplate } from '@/utils/templates';
import type { Ref } from 'vue';
const { formUpdated, stopAutoSave } = useAutoSave(async () => {
const values = formRef.value?.values;
if (values) {
await onSaveDraft(values, true);
}
});
// Props
type Props = {
activityId?: string;
Expand Down Expand Up @@ -115,7 +123,7 @@ function onInvalidSubmit(e: any) {
document.getElementById('form')?.scrollIntoView({ behavior: 'smooth' });
}
async function onSaveDraft(data: any) {
async function onSaveDraft(data: any, isAutoSave = false) {
editable.value = false;
try {
Expand All @@ -133,7 +141,12 @@ async function onSaveDraft(data: any) {
throw new Error('Failed to retrieve correct draft data');
}
toast.success('Draft saved');
if (isAutoSave) {
toast.success('Draft autosaved');
} else {
toast.success('Draft saved');
formUpdated.value = false;
}
} catch (e: any) {
toast.error('Failed to save draft', e);
} finally {
Expand Down Expand Up @@ -169,6 +182,7 @@ async function onSubmit(data: any) {
formRef.value?.setFieldValue('enquiryId', enquiryResponse.data.enquiryId);
// Send confirmation email
emailConfirmation(enquiryResponse.data.activityId);
stopAutoSave();
} else {
throw new Error('Failed to retrieve correct enquiry draft data');
}
Expand Down Expand Up @@ -261,6 +275,7 @@ async function emailConfirmation(activityId: string) {
:validation-schema="formSchema"
@invalid-submit="(e) => onInvalidSubmit(e)"
@submit="confirmSubmit"
@change="formUpdated = true"
>
<input
type="hidden"
Expand Down
47 changes: 34 additions & 13 deletions frontend/src/components/housing/submission/SubmissionIntakeForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
useConfirm,
useToast
} from '@/lib/primevue';
import { useAutoSave } from '@/composables/formAutoSave';
import { externalApiService, permitService, submissionService } from '@/services';
import { useConfigStore, useTypeStore } from '@/store';
import { YES_NO_LIST, YES_NO_UNSURE_LIST } from '@/utils/constants/application';
Expand Down Expand Up @@ -99,7 +100,14 @@ const orgBookOptions: Ref<Array<any>> = ref([]);
const parcelAccordionIndex: Ref<number | undefined> = ref(undefined);
const spacialAccordionIndex: Ref<number | undefined> = ref(undefined);
const validationErrors: Ref<string[]> = ref([]);
const formUpdated: Ref<boolean> = ref(false);
const hasFormBeenEdited: Ref<boolean> = ref(false);
const { formUpdated, stopAutoSave } = useAutoSave(async () => {
const values = formRef.value?.values;
if (values) {
await onSaveDraft(values, true);
}
});
// Actions
const confirm = useConfirm();
Expand Down Expand Up @@ -174,7 +182,7 @@ const onLatLongInputClick = async () => {
function onInvalidSubmit(e: any) {
validationErrors.value = Array.from(new Set(e.errors ? Object.keys(e.errors).map((x) => x.split('.')[0]) : []));
document.getElementById('form')?.scrollIntoView({ behavior: 'smooth' });
formUpdated.value = false;
hasFormBeenEdited.value = false;
}
function onPermitsHasAppliedChange(e: BasicResponse, fieldsLength: number, push: Function, setFieldValue: Function) {
Expand All @@ -191,7 +199,7 @@ function onPermitsHasAppliedChange(e: BasicResponse, fieldsLength: number, push:
}
}
async function onSaveDraft(data: any) {
async function onSaveDraft(data: any, isAutoSave = false) {
editable.value = false;
const tempData = Object.assign({}, data);
Expand All @@ -212,7 +220,12 @@ async function onSaveDraft(data: any) {
throw new Error('Failed to retrieve correct draft data');
}
toast.success('Draft saved');
if (isAutoSave) {
toast.success('Draft autosaved');
} else {
toast.success('Draft saved');
formUpdated.value = false;
}
} catch (e: any) {
toast.error('Failed to save draft', e);
} finally {
Expand All @@ -235,6 +248,7 @@ async function onSubmit(data: any) {
formRef.value?.setFieldValue('activityId', response.data.activityId);
// Send confirmation email
emailConfirmation(response.data.activityId);
stopAutoSave();
} else {
throw new Error('Failed to retrieve correct draft data');
}
Expand Down Expand Up @@ -370,7 +384,12 @@ onBeforeMount(async () => {
:validation-schema="submissionIntakeSchema"
@invalid-submit="(e) => onInvalidSubmit(e)"
@submit="confirmSubmit"
@change="formUpdated = true"
@change="
() => {
formUpdated = true;
hasFormBeenEdited = true;
}
"
>
<SubmissionAssistance
:form-errors="errors"
Expand Down Expand Up @@ -407,15 +426,15 @@ onBeforeMount(async () => {
'app-error-color':
(validationErrors.includes(IntakeFormCategory.APPLICANT) ||
validationErrors.includes(IntakeFormCategory.BASIC)) &&
!formUpdated
!hasFormBeenEdited
}"
/>
</template>
<template #content="{ nextCallback }">
<CollectionDisclaimer />

<Message
v-if="validationErrors.length && !formUpdated"
v-if="validationErrors.length && !hasFormBeenEdited"
severity="error"
icon="pi pi-exclamation-circle"
:closable="false"
Expand Down Expand Up @@ -571,12 +590,14 @@ onBeforeMount(async () => {
:click-callback="clickCallback"
title="Housing"
icon="fa-house"
:class="{ 'app-error-color': validationErrors.includes(IntakeFormCategory.HOUSING) && !formUpdated }"
:class="{
'app-error-color': validationErrors.includes(IntakeFormCategory.HOUSING) && !hasFormBeenEdited
}"
/>
</template>
<template #content="{ prevCallback, nextCallback }">
<Message
v-if="validationErrors.length && !formUpdated"
v-if="validationErrors.length && !hasFormBeenEdited"
severity="error"
icon="pi pi-exclamation-circle"
:closable="false"
Expand Down Expand Up @@ -918,13 +939,13 @@ onBeforeMount(async () => {
title="Location"
icon="fa-location-dot"
:class="{
'app-error-color': validationErrors.includes(IntakeFormCategory.LOCATION) && !formUpdated
'app-error-color': validationErrors.includes(IntakeFormCategory.LOCATION) && !hasFormBeenEdited
}"
/>
</template>
<template #content="{ prevCallback, nextCallback }">
<Message
v-if="validationErrors.length && !formUpdated"
v-if="validationErrors.length && !hasFormBeenEdited"
severity="error"
icon="pi pi-exclamation-circle"
:closable="false"
Expand Down Expand Up @@ -1233,13 +1254,13 @@ onBeforeMount(async () => {
'app-error-color':
(validationErrors.includes(IntakeFormCategory.PERMITS) ||
validationErrors.includes(IntakeFormCategory.APPLIED_PERMITS)) &&
!formUpdated
!hasFormBeenEdited
}"
/>
</template>
<template #content="{ prevCallback }">
<Message
v-if="validationErrors.length && !formUpdated"
v-if="validationErrors.length && !hasFormBeenEdited"
severity="error"
icon="pi pi-exclamation-circle"
:closable="false"
Expand Down
46 changes: 46 additions & 0 deletions frontend/src/composables/formAutoSave.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ref, onMounted, onBeforeUnmount } from 'vue';

// autosave functionality for forms, saves on inactivity after the delay (default 10 seconds)
export function useAutoSave(saveFunc: () => Promise<void>, delay: number = 10000) {
const formUpdated = ref(false);
let timeoutId: ReturnType<typeof setTimeout> | null = null;

const startTimer = () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(async () => {
if (formUpdated.value) {
await saveFunc();
formUpdated.value = false;
timeoutId = null;
}
}, delay);
};

const onActivity = () => {
startTimer();
};
onMounted(() => {
window.addEventListener('keydown', onActivity);
window.addEventListener('focus', onActivity);
window.addEventListener('click', onActivity);
});

onBeforeUnmount(() => {
window.removeEventListener('keydown', onActivity);
window.removeEventListener('focus', onActivity);
window.removeEventListener('click', onActivity);
if (timeoutId) clearTimeout(timeoutId);
});

return {
formUpdated,
stopAutoSave: () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
}
};
}
4 changes: 0 additions & 4 deletions frontend/src/views/DeveloperView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ const { getConfig } = storeToRefs(useConfigStore());
const permissionService = new PermissionService();
const router = useRouter();
async function ssoRequestBasicAccess() {
await permissionService.requestBasicAccess();
}
async function searchIdirUsers() {
await permissionService.searchIdirUsers({ firstName: 'Kyle' });
}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/views/housing/submission/SubmissionView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,8 @@ const onDeleteNote = (note: Note) => {
:sort-field="SORT_TYPES.CREATED_AT"
:sort-order="SORT_ORDER.DESCENDING"
@update:sort-order="
(order: number) => {
sortOrder = order;
(order: number | undefined) => {
sortOrder = order ?? SORT_ORDER.ASCENDING;
}
"
@update:sort-field="
Expand Down

0 comments on commit 7609b5d

Please sign in to comment.