From 4cd0ae78027655074e8190c4afc101f241547a3d Mon Sep 17 00:00:00 2001 From: tzuge <47162374+tzuge@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:02:25 -0700 Subject: [PATCH] feat(form-app): support view of user forms --- .../src/app/containers/FormTenant.tsx | 4 +- apps/form-app/src/app/containers/Forms.tsx | 74 ++++++++++++------- .../form-app/src/app/state/form.slice.spec.ts | 8 ++ apps/form-app/src/app/state/form.slice.ts | 67 +++++++++-------- 4 files changed, 98 insertions(+), 55 deletions(-) diff --git a/apps/form-app/src/app/containers/FormTenant.tsx b/apps/form-app/src/app/containers/FormTenant.tsx index ef16e35b6..3672806bd 100644 --- a/apps/form-app/src/app/containers/FormTenant.tsx +++ b/apps/form-app/src/app/containers/FormTenant.tsx @@ -17,6 +17,7 @@ import { import { FeedbackNotification } from './FeedbackNotification'; import { FormDefinition } from './FormDefinition'; import { useFeedbackLinkHandler } from '../util/feedbackUtils'; +import { Forms } from './Forms'; const AccountActionsSpan = styled.span` .username { @@ -90,7 +91,8 @@ export const FormTenant = () => { {userInitialized && (
- } /> + } /> + } /> } />
diff --git a/apps/form-app/src/app/containers/Forms.tsx b/apps/form-app/src/app/containers/Forms.tsx index 559ea325b..b2a57dc0d 100644 --- a/apps/form-app/src/app/containers/Forms.tsx +++ b/apps/form-app/src/app/containers/Forms.tsx @@ -1,42 +1,60 @@ -import { GoAButton, GoAButtonGroup, GoASkeleton, GoATable } from '@abgov/react-components-new'; +import { GoAButton, GoAButtonGroup, GoATable } from '@abgov/react-components-new'; import { Band, Container, RowLoadMore, RowSkeleton } from '@core-services/app-common'; import { useDispatch, useSelector } from 'react-redux'; -import { FunctionComponent } from 'react'; +import { FunctionComponent, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; import { AppDispatch, busySelector, canCreateDraftSelector, createForm, defaultUserFormSelector, + definitionFormsSelector, findUserForms, FormDefinition, - formsSelector, FormStatus, + tenantSelector, } from '../state'; +const FormsLayout = styled.div` + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + overflow: auto; +`; + interface FormsProps { - definition: FormDefinition; + definition?: FormDefinition; } export const Forms: FunctionComponent = ({ definition }) => { const dispatch = useDispatch(); const navigate = useNavigate(); + const tenant = useSelector(tenantSelector); const busy = useSelector(busySelector); - const { forms, next } = useSelector(formsSelector); + const { forms, next } = useSelector((state) => definitionFormsSelector(state, definition?.id)); const { form: defaultForm } = useSelector(defaultUserFormSelector); const canCreateDraft = useSelector(canCreateDraftSelector); + useEffect(() => { + dispatch(findUserForms({ definitionId: definition?.id })); + }, [dispatch, definition]); + return ( -
- + + Continue working on existing draft forms, or view forms you submitted in the past. - {canCreateDraft && ( + {definition && canCreateDraft && ( { const form = await dispatch(createForm(definition.id)).unwrap(); @@ -52,46 +70,52 @@ export const Forms: FunctionComponent = ({ definition }) => { + {!definition && Form} Created on Submitted on Status - Actions + {forms.map((form) => ( + {!definition && {form.definition?.name}} {form.created.toFormat('LLLL dd, yyyy')} {form.submitted?.toFormat('LLLL dd, yyyy')} {form.status} - {form.status === FormStatus.draft ? ( - navigate(`../${form.id}`)} - > - Continue draft - - ) : ( - navigate(`../${form.id}`)}> - View submitted - - )} + {form.definition && + (form.status === FormStatus.draft ? ( + navigate(`/${tenant.name}/${form.definition.id}/${form.id}`)} + > + Continue draft + + ) : ( + navigate(`/${tenant.name}/${form.definition.id}/${form.id}`)} + > + View submitted + + ))} ))} - + dispatch(findUserForms({ definitionId: definition.id, after }))} + onLoadMore={(after) => dispatch(findUserForms({ definitionId: definition?.id, after }))} /> -
+ ); }; diff --git a/apps/form-app/src/app/state/form.slice.spec.ts b/apps/form-app/src/app/state/form.slice.spec.ts index 074244429..84f41a9cb 100644 --- a/apps/form-app/src/app/state/form.slice.spec.ts +++ b/apps/form-app/src/app/state/form.slice.spec.ts @@ -32,6 +32,9 @@ const initialState: FormState = { saving: false, submitting: false, }, + initialized: { + forms: false, + }, }; const definitionsToTest: FormState = { @@ -55,6 +58,7 @@ const definitionsToTest: FormState = { form: { definition: { id: 'TEST', + name: 'test', }, created: null, submitted: null, @@ -73,6 +77,9 @@ const definitionsToTest: FormState = { saving: false, submitting: false, }, + initialized: { + forms: false, + }, }; const loadedFormDefinition: FormDefinition = { @@ -91,6 +98,7 @@ const payload = { form: { definition: { id: 'TEST', + name: 'test', }, created: null, submitted: null, diff --git a/apps/form-app/src/app/state/form.slice.ts b/apps/form-app/src/app/state/form.slice.ts index effe68ca4..423ec2ebd 100644 --- a/apps/form-app/src/app/state/form.slice.ts +++ b/apps/form-app/src/app/state/form.slice.ts @@ -36,7 +36,7 @@ interface SerializableFormDefinition { } interface SerializableForm { - definition: { id: string }; + definition: { id: string; name: string }; id: string; urn: string; status: 'draft' | 'locked' | 'submitted' | 'archived'; @@ -81,6 +81,9 @@ export interface FormState { saving: boolean; submitting: boolean; }; + initialized: { + forms: boolean; + }; } const FORM_SERVICE_ID = 'urn:ads:platform:form-service'; @@ -213,7 +216,7 @@ export const loadDefinition = createAsyncThunk( export const findUserForms = createAsyncThunk( 'form/find-user-forms', - async ({ definitionId, after }: { definitionId: string; after?: string }, { getState, rejectWithValue }) => { + async ({ definitionId, after }: { definitionId?: string; after?: string }, { getState, rejectWithValue }) => { try { const { config, user } = getState() as AppState; const formServiceUrl = config.directory[FORM_SERVICE_ID]; @@ -498,6 +501,9 @@ const initialFormState: FormState = { saving: false, submitting: false, }, + initialized: { + forms: false, + }, }; export const formSlice = createSlice({ @@ -517,8 +523,6 @@ export const formSlice = createSlice({ state.selected = meta.arg; // Clear the form if the form definition is changing. if (state.form && state.form.definition.id !== meta.arg) { - state.forms = {}; - state.results = []; state.next = null; state.form = null; state.data = {}; @@ -532,13 +536,6 @@ export const formSlice = createSlice({ .addCase(loadDefinition.fulfilled, (state, { payload, meta }) => { state.busy.loading = false; state.definitions[meta.arg] = payload; - - //Check form definition id case sensitivity, and use the definition id in the payload object, - //instead of using the value in querystring because if the case is not the same - //grabbing the object using the form definition id in the querystring as the key wont work. - if (payload && payload.id?.toLowerCase() === state.selected?.toLowerCase()) { - state.selected = payload.id; - } }) .addCase(loadDefinition.rejected, (state) => { state.busy.loading = false; @@ -580,6 +577,7 @@ export const formSlice = createSlice({ state.forms = payload.results.reduce((forms, result) => ({ ...forms, [result.id]: result }), state.forms); state.results = [...(payload.page.after ? state.results : []), ...payload.results.map((result) => result.id)]; state.next = payload.page.next; + state.initialized.forms = true; }) .addCase(findUserForms.rejected, (state) => { state.busy.loading = false; @@ -643,18 +641,28 @@ export const formsSelector = createSelector( (state: AppState) => state.form.next, (forms, results, next) => ({ forms: results - .map((result) => - forms[result] - ? { - ...forms[result], - status: FormStatus[forms[result].status], - created: forms[result].created && DateTime.fromISO(forms[result].created), - submitted: forms[result].submitted && DateTime.fromISO(forms[result].submitted), - } - : null - ) + .map((result) => { + const form = forms[result]; + const status = FormStatus[form.status]; + return { + ...forms[result], + status, + created: form.created && DateTime.fromISO(form.created), + // Submitted can be set for draft forms if it was returned to draft. + submitted: status !== FormStatus.draft ? form.submitted && DateTime.fromISO(form.submitted) : undefined, + }; + }) .filter((result) => !!result) - .sort((a, b) => a.created.diff(b.created).as('seconds')), + .sort((a, b) => b.created.diff(a.created).as('seconds')), + next, + }) +); + +export const definitionFormsSelector = createSelector( + formsSelector, + (_, definitionId: string) => definitionId, + ({ forms, next }, definitionId) => ({ + forms: forms.filter((form) => !definitionId || form.definition?.id === definitionId), next, }) ); @@ -668,15 +676,18 @@ export const formSelector = createSelector( ...form, status: FormStatus[form.status], created: form.created && DateTime.fromISO(form.created), - submitted: form.submitted && DateTime.fromISO(form.submitted), + submitted: + FormStatus[form.status] !== FormStatus.draft + ? form.submitted && DateTime.fromISO(form.submitted) + : undefined, } : null ); export const defaultUserFormSelector = createSelector( formsSelector, - (state: AppState) => state.form.busy.loading, - ({ forms }, loading) => { + (state: AppState) => state.form.initialized.forms, + ({ forms }, initialized) => { let form: ReturnType['forms'][0]; if (forms.length === 1) { // If user only has one form, then that's the default form. @@ -690,7 +701,7 @@ export const defaultUserFormSelector = createSelector( } return { form, - initialized: !loading, + initialized, empty: forms.length < 1, }; } @@ -715,12 +726,10 @@ export const isClerkSelector = createSelector( export const busySelector = (state: AppState) => state.form.busy; export const canCreateDraftSelector = createSelector( - (state: AppState) => state.form.busy.creating, isApplicantSelector, definitionSelector, defaultUserFormSelector, - (creating, isApplicant, { definition }, { form }) => - !creating && isApplicant && (definition?.oneFormPerApplicant === false || !form) + (isApplicant, { definition }, { form }) => isApplicant && (definition?.oneFormPerApplicant === false || !form) ); export const showSubmitSelector = createSelector(definitionSelector, ({ definition }) => {