-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Form rendering and submission managed by backend (#307)
* Add basic tests for existing form submission behavior. * Add "submit form" test that confirms a completed form returns a PDF. * Add README for database package. * upsertFormSession persistence function * Add getFormSession db routine * In-progress work on server-rendered form * Pass route params to form components via session; form rendering in server app. todo: client rendering via spotlight app * Use hash urls on client-side form router * Add session management services; handling form routing in the app - in server, use Astro, move FormRouter into Spotlight, get rid of window.location references in @atj/design. * Standard http POST form submissions working. This included many tweaks to routing/page handling and session services updates. * Client-side routing fixes * Linting server package * Use node.js v20.18.0 * Update all playwright resources to 1.48.0 * Update lockfile * Add type for *.astro files, so "pnpm typecheck" in the workspace will not fail on imports of .astro modules from .ts modules. * Remove comment * Rename getAstroAppContext -> getServerContext * Add initial Playwright test and try to track down problems with Testcontainers usage (not working locally). Committing to test on CI. * Mark session incomplete test as expect failure * Update incomplete session test * Limit to just the node.js form route test. Will use browser mode for component tests, and avoid Playwright for now. * Confirm session is updated on form POST * Improve Spotlight behavior. Still some reorg work to do on the follow-up PR. * Organize frontend state * Minor cleanup * Fix e2e test: prefix localStorage key with "forms/"
- Loading branch information
1 parent
7bcd57f
commit eef7768
Showing
111 changed files
with
4,892 additions
and
1,479 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
v20.16.0 | ||
v20.18.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
79 changes: 79 additions & 0 deletions
79
apps/spotlight/src/features/form-page/components/AppFormPage.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import React, { useEffect } from 'react'; | ||
import { | ||
type Location, | ||
HashRouter, | ||
Route, | ||
Routes, | ||
useLocation, | ||
useParams, | ||
} from 'react-router-dom'; | ||
|
||
import { defaultPatternComponents, Form } from '@atj/design'; | ||
import { defaultFormConfig, getRouteDataFromQueryString } from '@atj/forms'; | ||
|
||
import { getAppContext } from '../../../context.js'; | ||
import { useFormPageStore } from '../store/index.js'; | ||
|
||
export const AppFormPage = () => { | ||
return ( | ||
<HashRouter> | ||
<Routes> | ||
<Route path="/:id" element={<AppFormRoute />} /> | ||
</Routes> | ||
</HashRouter> | ||
); | ||
}; | ||
|
||
const AppFormRoute = () => { | ||
const { actions, formSessionResponse } = useFormPageStore(); | ||
const { id } = useParams(); | ||
const location = useLocation(); | ||
const ctx = getAppContext(); | ||
|
||
if (id === undefined) { | ||
throw new Error('id is undefined'); | ||
} | ||
|
||
useEffect( | ||
() => | ||
actions.initialize({ | ||
formId: id, | ||
route: getRouteParamsFromLocation(location), | ||
}), | ||
[location, id] | ||
); | ||
return ( | ||
<> | ||
{formSessionResponse.status === 'loading' && <div>Loading...</div>} | ||
{formSessionResponse.status === 'error' && ( | ||
<div className="usa-alert usa-alert--error" role="alert"> | ||
<div className="usa-alert__body"> | ||
<h4 className="usa-alert__heading">Error loading form</h4> | ||
<p className="usa-alert__text">{formSessionResponse.message}</p> | ||
</div> | ||
</div> | ||
)} | ||
{formSessionResponse.status === 'loaded' && ( | ||
<Form | ||
context={{ | ||
config: defaultFormConfig, | ||
components: defaultPatternComponents, | ||
uswdsRoot: ctx.uswdsRoot, | ||
}} | ||
session={formSessionResponse.formSession} | ||
onSubmit={data => actions.onSubmitForm({ formId: id, data })} | ||
/> | ||
)} | ||
</> | ||
); | ||
}; | ||
|
||
const getRouteParamsFromLocation = (location: Location) => { | ||
const queryString = location.search.startsWith('?') | ||
? location.search.substring(1) | ||
: location.search; | ||
return { | ||
params: getRouteDataFromQueryString(queryString), | ||
url: `${location.pathname}`, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { AppFormPage } from './components/AppFormPage.js'; |
56 changes: 56 additions & 0 deletions
56
apps/spotlight/src/features/form-page/store/actions/get-form-session.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { type FormSession, type RouteData } from '@atj/forms'; | ||
import { type FormPageContext } from './index.js'; | ||
|
||
export type FormSessionResponse = | ||
| { status: 'loading' } | ||
| { status: 'error'; message: string } | ||
| { | ||
status: 'loaded'; | ||
formSession: FormSession; | ||
sessionId: string | undefined; | ||
}; | ||
|
||
export type GetFormSession = ( | ||
ctx: FormPageContext, | ||
opts: { | ||
formId: string; | ||
route: { | ||
params: RouteData; | ||
url: string; | ||
}; | ||
sessionId?: string; | ||
} | ||
) => void; | ||
|
||
export const getFormSession: GetFormSession = async (ctx, opts) => { | ||
ctx.setState({ formSessionResponse: { status: 'loading' } }); | ||
ctx.config.formService | ||
.getFormSession({ | ||
formId: opts.formId, | ||
formRoute: { | ||
params: opts.route.params, | ||
url: `#${opts.route.url}`, | ||
}, | ||
sessionId: opts.sessionId, | ||
}) | ||
.then(result => { | ||
if (result.success === false) { | ||
console.error(result.error); | ||
ctx.setState({ | ||
formSessionResponse: { | ||
status: 'error', | ||
message: result.error, | ||
}, | ||
}); | ||
} else { | ||
console.log('using session', result.data.data); | ||
ctx.setState({ | ||
formSessionResponse: { | ||
status: 'loaded', | ||
formSession: result.data.data, | ||
sessionId: result.data.id, | ||
}, | ||
}); | ||
} | ||
}); | ||
}; |
35 changes: 35 additions & 0 deletions
35
apps/spotlight/src/features/form-page/store/actions/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { type ServiceMethod, createService } from '@atj/common'; | ||
|
||
import { type AppContext, getAppContext } from '../../../../context.js'; | ||
import { type GetFormSession, getFormSession } from './get-form-session.js'; | ||
import { type Initialize, initialize } from './initialize.js'; | ||
import { type OnSubmitForm, onSubmitForm } from './on-submit-form.js'; | ||
import { type FormPageState } from '../state.js'; | ||
|
||
export type FormPageContext = { | ||
config: AppContext; | ||
getState: () => FormPageState; | ||
setState: (state: Partial<FormPageState>) => void; | ||
}; | ||
|
||
export interface FormPageActions { | ||
getFormSession: ServiceMethod<GetFormSession>; | ||
initialize: ServiceMethod<Initialize>; | ||
onSubmitForm: ServiceMethod<OnSubmitForm>; | ||
} | ||
|
||
export const createFormPageActions = ( | ||
getState: () => FormPageState, | ||
setState: (state: Partial<FormPageState>) => void | ||
): FormPageActions => { | ||
const ctx: FormPageContext = { | ||
getState, | ||
setState, | ||
config: getAppContext(), | ||
}; | ||
return createService(ctx, { | ||
getFormSession, | ||
initialize, | ||
onSubmitForm, | ||
}); | ||
}; |
18 changes: 18 additions & 0 deletions
18
apps/spotlight/src/features/form-page/store/actions/initialize.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { type FormRoute } from '@atj/forms'; | ||
import { type FormPageContext } from './index.js'; | ||
import { getFormSession } from './get-form-session.js'; | ||
|
||
export type Initialize = ( | ||
ctx: FormPageContext, | ||
opts: { formId: string; route: FormRoute } | ||
) => void; | ||
|
||
export const initialize: Initialize = (ctx, opts) => { | ||
// Get the session ID from local storage so we can use it on page reload. | ||
const sessionId = window.localStorage.getItem('form_session_id') || undefined; | ||
getFormSession(ctx, { | ||
formId: opts.formId, | ||
route: opts.route, | ||
sessionId, | ||
}); | ||
}; |
56 changes: 56 additions & 0 deletions
56
apps/spotlight/src/features/form-page/store/actions/on-submit-form.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { type FormPageContext } from './index.js'; | ||
|
||
export type OnSubmitForm = ( | ||
ctx: FormPageContext, | ||
opts: { | ||
formId: string; | ||
sessionId?: string; | ||
data: Record<string, string>; | ||
} | ||
) => void; | ||
|
||
export const onSubmitForm: OnSubmitForm = async (ctx, opts) => { | ||
const state = ctx.getState(); | ||
if (state.formSessionResponse.status !== 'loaded') { | ||
console.error("Can't submit data. Form session not loaded"); | ||
return; | ||
} | ||
/*const newSession = applyPromptResponse( | ||
config, | ||
session, | ||
response | ||
);*/ | ||
const submission = await ctx.config.formService.submitForm( | ||
opts.sessionId, | ||
opts.formId, | ||
opts.data, | ||
state.formSessionResponse.formSession.route | ||
); | ||
if (submission.success) { | ||
for (const document of submission.data.documents || []) { | ||
downloadPdfDocument(document.fileName, document.data); | ||
} | ||
ctx.setState({ | ||
formSessionResponse: { | ||
status: 'loaded', | ||
formSession: submission.data.session, | ||
sessionId: submission.data.sessionId, | ||
}, | ||
}); | ||
window.localStorage.setItem('form_session_id', submission.data.sessionId); | ||
} else { | ||
console.error(submission.error); | ||
} | ||
}; | ||
|
||
export const downloadPdfDocument = (fileName: string, pdfData: Uint8Array) => { | ||
const blob = new Blob([pdfData], { type: 'application/pdf' }); | ||
const url = URL.createObjectURL(blob); | ||
const element = document.createElement('a'); | ||
element.setAttribute('href', url); | ||
element.setAttribute('download', fileName); | ||
element.style.display = 'none'; | ||
document.body.appendChild(element); | ||
element.click(); | ||
document.body.removeChild(element); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { create } from 'zustand'; | ||
|
||
import { | ||
type FormPageActions, | ||
createFormPageActions, | ||
} from './actions/index.js'; | ||
import { type FormPageState, getInitialState } from './state.js'; | ||
|
||
type Store = FormPageState & { | ||
actions: FormPageActions; | ||
}; | ||
|
||
export const useFormPageStore = create<Store>((set, get) => { | ||
return { | ||
...getInitialState(), | ||
actions: createFormPageActions(get, set), | ||
}; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { type FormSessionResponse } from './actions/get-form-session.js'; | ||
|
||
export type FormPageState = { | ||
formId: string; | ||
formSessionResponse: FormSessionResponse; | ||
}; | ||
|
||
export const getInitialState = (): FormPageState => ({ | ||
formId: '', | ||
formSessionResponse: { status: 'loading' }, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
--- | ||
import AppFormRouter from '../../components/AppFormRouter'; | ||
import { AppFormPage } from '../../features/form-page'; | ||
import ContentLayout from '../../layouts/ContentLayout.astro'; | ||
--- | ||
|
||
<ContentLayout title="10x Access to Justice Spotlight"> | ||
<AppFormRouter client:only /> | ||
<AppFormPage client:only="react" /> | ||
</ContentLayout> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.