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

Form submission #307

Draft
wants to merge 25 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
738acb7
Add basic tests for existing form submission behavior.
danielnaab Sep 6, 2024
de72ce5
Add "submit form" test that confirms a completed form returns a PDF.
danielnaab Sep 9, 2024
2f69431
Add README for database package.
danielnaab Sep 12, 2024
ac32c75
upsertFormSession persistence function
danielnaab Sep 13, 2024
ad81caa
Add getFormSession db routine
danielnaab Sep 13, 2024
6276e5e
In-progress work on server-rendered form
danielnaab Sep 30, 2024
019169e
Pass route params to form components via session; form rendering in s…
danielnaab Oct 2, 2024
03fa69c
Use hash urls on client-side form router
danielnaab Oct 2, 2024
469b31a
Add session management services; handling form routing in the app - i…
danielnaab Oct 6, 2024
c61141e
Standard http POST form submissions working. This included many tweak…
danielnaab Oct 9, 2024
0ba158d
Client-side routing fixes
danielnaab Oct 11, 2024
65828c2
Linting server package
danielnaab Oct 11, 2024
2e49d17
Merge remote-tracking branch 'origin/main' into form-submission
danielnaab Oct 11, 2024
581a185
Use node.js v20.18.0
danielnaab Oct 11, 2024
7474e41
Update all playwright resources to 1.48.0
danielnaab Oct 11, 2024
b507717
Update lockfile
danielnaab Oct 11, 2024
ec94b0b
Add type for *.astro files, so "pnpm typecheck" in the workspace will…
danielnaab Oct 11, 2024
60beb31
Remove comment
danielnaab Oct 14, 2024
0a084ea
Rename getAstroAppContext -> getServerContext
danielnaab Oct 14, 2024
8b2a02b
Add initial Playwright test and try to track down problems with Testc…
danielnaab Oct 15, 2024
edc2d29
Mark session incomplete test as expect failure
danielnaab Oct 15, 2024
5dd7b8d
Update incomplete session test
danielnaab Oct 15, 2024
db21c77
Limit to just the node.js form route test. Will use browser mode for …
danielnaab Oct 17, 2024
c1b1b94
Confirm session is updated on form POST
danielnaab Oct 18, 2024
bb0fe5b
Merge remote-tracking branch 'origin/main' into form-submission
danielnaab Oct 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/_validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
run: pnpm install

- name: Install playwright
run: pnpm dlx playwright install --with-deps
run: pnpm dlx playwright@1.48.0 install --with-deps

# While most of the test suite is self-contained, the tests for the demo
# servers require a prod build of @atj/server.
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v20.16.0
v20.18.0
1 change: 0 additions & 1 deletion apps/server-doj/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
"@atj/server": "workspace:*"
},
"devDependencies": {
"@types/node": "^20.14.14",
"@types/supertest": "^6.0.2",
"supertest": "^7.0.0"
}
Expand Down
1 change: 0 additions & 1 deletion apps/server-kansas/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
"@atj/server": "workspace:*"
},
"devDependencies": {
"@types/node": "^20.14.14",
"@types/supertest": "^6.0.2",
"supertest": "^7.0.0"
}
Expand Down
6 changes: 5 additions & 1 deletion apps/spotlight/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,19 @@
},
"dependencies": {
"@astrojs/react": "^3.6.1",
"@atj/common": "workspace:*",
"@atj/design": "workspace:*",
"@atj/forms": "workspace:*",
"astro": "^4.13.2",
"qs": "^6.13.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13"
"react-error-boundary": "^4.0.13",
"react-router-dom": "^6.26.0"
},
"devDependencies": {
"@astrojs/check": "^0.4.1",
"@types/qs": "^6.9.15",
"@types/react": "^18.3.3"
}
}
9 changes: 0 additions & 9 deletions apps/spotlight/src/components/AppFormRouter.tsx

This file was deleted.

88 changes: 88 additions & 0 deletions apps/spotlight/src/features/form-page/components/AppFormPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React, { useEffect } from 'react';

import { defaultPatternComponents, Form } from '@atj/design';
import { defaultFormConfig } from '@atj/forms';

import { getAppContext } from '../../../context.js';
import { useFormSession } from '../hooks/form-session.js';
import { FormRouter } from './FormRouter.js';

export const AppFormPage = () => {
const ctx = getAppContext();
return (
<FormRouter formService={ctx.formService}>
{({ id: formId, route }) => {
const { formSessionResponse, setFormSession } = useFormSession({
formId,
formService: ctx.formService,
route,
});
useEffect(() => {
if (formSessionResponse.status !== 'loaded') {
return;
}
setFormSession({
...formSessionResponse.formSession,
route,
});
}, [route]);
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={async data => {
/*const newSession = applyPromptResponse(
config,
session,
response
);*/
const submission = await ctx.formService.submitForm(
undefined, // TODO: pass sessionId
formId,
data
);
if (submission.success) {
for (const document of submission.data.documents || []) {
downloadPdfDocument(document.fileName, document.data);
}
} else {
console.error(submission.error);
}
}}
/>
)}
</>
);
}}
</FormRouter>
);
};

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);
};
37 changes: 37 additions & 0 deletions apps/spotlight/src/features/form-page/components/FormRouter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { type ReactElement } from 'react';
import { HashRouter, Route, Routes, useParams } from 'react-router-dom';

import { type FormService } from '@atj/forms';
import { useRouteParams } from '../hooks/route-params.js';

// Wrapper around Form that includes a client-side router for loading forms.
type FormRouterProps = {
children: (props: {
id: string;
route: {
params: qs.ParsedQs;
url: string;
};
}) => ReactElement;
formService: FormService;
};

export const FormRouter = ({ children }: FormRouterProps) => {
return (
<HashRouter>
<Routes>
<Route
path="/:id"
Component={() => {
const { id } = useParams();
const route = useRouteParams();
if (id === undefined) {
return <div>id is undefined</div>;
}
return children({ id, route });
}}
/>
</Routes>
</HashRouter>
);
};
49 changes: 49 additions & 0 deletions apps/spotlight/src/features/form-page/hooks/form-session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useEffect, useState } from 'react';

import { type FormService, type FormSession, type RouteData } from '@atj/forms';

export const useFormSession = (opts: {
formService: FormService;
formId: string;
route: {
params: RouteData;
url: string;
};
}) => {
const [formSessionResponse, setFormSessionResponse] = useState<
| { status: 'loading' }
| { status: 'error'; message: string }
| { status: 'loaded'; formSession: FormSession }
>({ status: 'loading' });
useEffect(() => {
opts.formService
.getFormSession({
formId: opts.formId,
formRoute: {
params: opts.route.params,
url: `#${opts.route.url}`,
},
//sessionId: undefined,
})
.then(result => {
if (result.success === false) {
console.error(result.error);
setFormSessionResponse({
status: 'error',
message: result.error,
});
} else {
setFormSessionResponse({
status: 'loaded',
formSession: result.data.data,
});
}
});
}, []);
return {
formSessionResponse,
setFormSession: (formSession: FormSession) => {
setFormSessionResponse({ status: 'loaded', formSession });
},
};
};
21 changes: 21 additions & 0 deletions apps/spotlight/src/features/form-page/hooks/route-params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';

import { type RouteData, getRouteDataFromQueryString } from '@atj/forms';

export const useRouteParams = (): {
params: RouteData;
url: string;
} => {
const location = useLocation();
const params = useMemo(() => {
const queryString = location.search.startsWith('?')
? location.search.substring(1)
: location.search;
return getRouteDataFromQueryString(queryString);
}, [location.search]);
return {
params,
url: `#${location.pathname}`,
};
};
1 change: 1 addition & 0 deletions apps/spotlight/src/features/form-page/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AppFormPage } from './components/AppFormPage.js';
4 changes: 2 additions & 2 deletions apps/spotlight/src/pages/forms/index.astro
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>
2 changes: 1 addition & 1 deletion apps/spotlight/src/pages/manage/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import Layout from '../../layouts/Layout.astro';
---

<Layout title="10x Access to Justice Spotlight">
<AppFormManager client:only />
<AppFormManager client:only="react" />
</Layout>
4 changes: 2 additions & 2 deletions apps/spotlight/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"jsx": "react",
"resolveJsonModule": true
},
"include": ["src/**/*.ts"],
"exclude": ["src/components/**"]
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.astro"],
"exclude": [".astro", "dist", "node_modules"]
}
2 changes: 1 addition & 1 deletion e2e/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# base image with Node.js and playwright preinstalled
FROM mcr.microsoft.com/playwright:v1.47.2-noble as base
FROM mcr.microsoft.com/playwright:v1.48.0-noble as base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
ENV NODE_ENV=test
Expand Down
2 changes: 1 addition & 1 deletion e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"test": "export E2E_ENDPOINT=http://localhost:4321; pnpm playwright test --ui-port=8080 --ui-host=0.0.0.0"
},
"devDependencies": {
"@playwright/test": "^1.47.2",
"@playwright/test": "^1.48.0",
"@storybook/test-runner": "^0.19.1",
"path-to-regexp": "^7.1.0"
},
Expand Down
1 change: 0 additions & 1 deletion infra/cdktf/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^20.14.14",
"jest": "^29.7.0",
"ts-jest": "^29.2.4"
}
Expand Down
26 changes: 14 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,27 @@
"pre-commit": "pnpm format"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^26.0.1",
"@playwright/test": "^1.48.0",
"@rollup/plugin-commonjs": "^28.0.0",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/node": "^20.14.14",
"@vitest/coverage-v8": "^2.0.5",
"@vitest/ui": "^2.0.5",
"esbuild": "^0.23.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@types/node": "^22.7.4",
"@vitest/browser": "^2.1.3",
"@vitest/coverage-v8": "^2.1.3",
"@vitest/ui": "^2.1.3",
"esbuild": "^0.24.0",
"eslint": "^8.57.0",
"husky": "^9.1.4",
"husky": "^9.1.6",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"rimraf": "^6.0.1",
"rollup": "^4.20.0",
"rollup": "^4.23.0",
"rollup-plugin-typescript2": "^0.36.0",
"ts-node": "^10.9.2",
"tsup": "^8.2.4",
"turbo": "^2.0.14",
"typescript": "^5.5.4",
"vitest": "^2.0.5",
"tsup": "^8.3.0",
"turbo": "^2.1.3",
"typescript": "^5.6.2",
"vitest": "^2.1.3",
"vitest-mock-extended": "^2.0.0"
}
}
2 changes: 1 addition & 1 deletion packages/common/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { createService } from './service.js';
export { type ServiceMethod, createService } from './service.js';

export type Success<T> = { success: true; data: T };
export type VoidSuccess = { success: true };
Expand Down
5 changes: 5 additions & 0 deletions packages/common/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ type ServiceFunction<Args extends any[], Return, Context> = (
...args: Args
) => Return;

export type ServiceMethod<F extends ServiceFunction<any, any, any>> =
F extends (context: infer C, ...args: infer A) => infer R
? (...args: A) => R
: never;

type ServiceFunctions<Context> = {
[key: string]: ServiceFunction<any[], any, Context>;
};
Expand Down
Loading
Loading