Skip to content

Commit

Permalink
Ingest PDF element pages (#178)
Browse files Browse the repository at this point in the history
* Place PDF ingested patterns on separate pages, corresponding to original location in source PDF.

* Remove page from PDF element type, for now - we don't need it to fill the pdf

* Add logging for submission validation errors

* Use session data rather than form post data when generating PDF

* Add placeholder back button to each page, which acts as a submit button.

* Move the actions definition into PromptComponent, and implement on the page-set pattern. This allows us to easily vary the available actions based on the currently selected page.

* Wire the back button up as a link instead of a submit button.

* Add new actions prop to pageset story

* Add name=action to the submit buttons, to distinguish between next and submit operations. Also, remove the unused actions array from the Prompt type. Prompts are now just a single component, so perhaps the type should just go away.

* remove comment

* Pdf pages (#181)

* Place PDF ingested patterns on separate pages, corresponding to original location in source PDF.

* Remove page from PDF element type, for now - we don't need it to fill the pdf

* Add logging for submission validation errors

* Use session data rather than form post data when generating PDF

* Add placeholder back button to each page, which acts as a submit button.

* Move the actions definition into PromptComponent, and implement on the page-set pattern. This allows us to easily vary the available actions based on the currently selected page.

* Wire the back button up as a link instead of a submit button.

* Add new actions prop to pageset story

* Add name=action to the submit buttons, to distinguish between next and submit operations. Also, remove the unused actions array from the Prompt type. Prompts are now just a single component, so perhaps the type should just go away.

---------

Co-authored-by: Daniel Naab <[email protected]>

* Fix merge issue: remove appending of radio group to rootSequence.

* options don't need page

* options don't need page

* comment radio

* radio option page

* remove radio option page

---------

Co-authored-by: jimmoffet <[email protected]>
  • Loading branch information
danielnaab and jimmoffet authored Jun 7, 2024
1 parent 010a873 commit 384a22c
Show file tree
Hide file tree
Showing 12 changed files with 338 additions and 121 deletions.
18 changes: 17 additions & 1 deletion packages/design/src/Form/ActionBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,26 @@ export default function ActionBar({ actions }: { actions: PromptAction[] }) {
{actions.map((action, index) => {
if (action.type === 'submit') {
return (
<button key={index} type={action.type} className="usa-button">
<button
key={index}
type="submit"
name="action"
value={action.submitAction}
className="usa-button"
>
{action.text}
</button>
);
} else if (action.type === 'link') {
return (
<a
key={index}
href={action.url}
className="usa-button usa-button--outline"
>
{action.text}
</a>
);
}
})}
</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React from 'react';

import { MemoryRouter } from 'react-router-dom';
import type { Meta, StoryObj } from '@storybook/react';

import { type PageSetProps } from '@atj/forms';

import { FormManagerProvider } from '../../../FormManager/store';
import {
createTestFormManagerContext,
Expand All @@ -10,7 +12,6 @@ import {
} from '../../../test-form';

import PageSet from './PageSet';
import { MemoryRouter } from 'react-router-dom';

const meta: Meta<typeof PageSet> = {
title: 'patterns/PageSet',
Expand Down Expand Up @@ -46,5 +47,6 @@ export const Basic = {
active: true,
},
],
},
actions: [],
} satisfies PageSetProps,
} satisfies StoryObj<typeof PageSet>;
5 changes: 4 additions & 1 deletion packages/design/src/Form/components/PageSet/PageSet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import React from 'react';
import { type PageSetProps } from '@atj/forms';

import { type PatternComponent } from '../..';
import { PageMenu } from './PageMenu';
import ActionBar from '../../../Form/ActionBar';
import { useRouteParams } from '../../../FormRouter/hooks';

import { PageMenu } from './PageMenu';

const PageSet: PatternComponent<PageSetProps> = props => {
const { routeParams, pathname } = useRouteParams();
return (
Expand All @@ -25,6 +27,7 @@ const PageSet: PatternComponent<PageSetProps> = props => {
</nav>
<div className="tablet:grid-col-9 tablet:padding-left-4 padding-left-0 padding-bottom-3 padding-top-3 tablet:border-left tablet:border-base-lighter contentWrapper">
{props.children}
<ActionBar actions={props.actions} />
</div>
</div>
);
Expand Down
3 changes: 0 additions & 3 deletions packages/design/src/Form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import {
type PromptComponent,
} from '@atj/forms';

import ActionBar from './ActionBar';

export type FormUIContext = {
config: FormConfig;
components: ComponentForPattern;
Expand Down Expand Up @@ -224,7 +222,6 @@ const FormContents = ({
/>
);
})}
<ActionBar actions={prompt.actions} />
</fieldset>
</>
);
Expand Down
24 changes: 9 additions & 15 deletions packages/forms/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export type CheckboxProps = PatternProps<{
export type PageSetProps = PatternProps<{
type: 'page-set';
pages: { title: string; active: boolean }[];
actions: PromptAction[];
}>;

export type PageProps = PatternProps<{
Expand Down Expand Up @@ -86,17 +87,22 @@ export type PatternProps<T = {}> = {

export type SubmitAction = {
type: 'submit';
text: 'Submit';
submitAction: 'next' | 'submit';
text: string;
};
export type LinkAction = {
type: 'link';
text: string;
url: string;
};
export type PromptAction = SubmitAction;
export type PromptAction = SubmitAction | LinkAction;

export type PromptComponent = {
props: PatternProps;
children: PromptComponent[];
};

export type Prompt = {
actions: PromptAction[];
components: PromptComponent[];
};

Expand All @@ -107,7 +113,6 @@ export const createPrompt = (
): Prompt => {
if (options.validate && sessionIsComplete(config, session)) {
return {
actions: [],
components: [
{
props: {
Expand Down Expand Up @@ -137,12 +142,6 @@ export const createPrompt = (
const root = getRootPattern(session.form);
components.push(createPromptForPattern(config, session, root, options));
return {
actions: [
{
type: 'submit',
text: 'Submit',
},
],
components,
};
};
Expand All @@ -169,10 +168,6 @@ export const createPromptForPattern: CreatePrompt<Pattern> = (
return patternConfig.createPrompt(config, session, pattern, options);
};

export const isPromptAction = (prompt: Prompt, action: string) => {
return prompt.actions.find(a => a.type === action);
};

export const createNullPrompt = ({
config,
pattern,
Expand All @@ -187,6 +182,5 @@ export const createNullPrompt = ({
validate: false,
}),
],
actions: [],
};
};
17 changes: 9 additions & 8 deletions packages/forms/src/documents/__tests__/document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@ describe('addDocument document processing', () => {

console.error(JSON.stringify(errors, null, 2)); // Fix these
expect(rootPattern).toEqual(expect.objectContaining({ type: 'page-set' }));
expect(rootPattern.data.pages.length).toEqual(1);
const pagePattern = getPattern<PagePattern>(
updatedForm,
rootPattern.data.pages[0]
);

// As a sanity check, just confirm that there is content on the first page.
expect(pagePattern.data.patterns.length).toBeGreaterThan(1);
expect(rootPattern.data.pages.length).toEqual(4);
for (let page = 0; page < rootPattern.data.pages.length; page++) {
const pagePattern = getPattern<PagePattern>(
updatedForm,
rootPattern.data.pages[page]
);
// As a sanity check, just confirm that there is content on the first page.
expect(pagePattern.data.patterns.length).toBeGreaterThanOrEqual(1);
}
});
});
271 changes: 213 additions & 58 deletions packages/forms/src/documents/pdf/mock-response.ts

Large diffs are not rendered by default.

57 changes: 36 additions & 21 deletions packages/forms/src/documents/pdf/parsing-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ const TxInput = z.object({
label: z.string(),
default_value: z.string(),
required: z.boolean(),
page: z.number(),
});

const Checkbox = z.object({
component_type: z.literal('checkbox'),
id: z.string(),
label: z.string(),
default_checked: z.boolean(),
page: z.number(),
});

const RadioGroupOption = z.object({
Expand All @@ -108,17 +110,20 @@ const RadioGroup = z.object({
component_type: z.literal('radio_group'),
legend: z.string(),
options: RadioGroupOption.array(),
page: z.number(),
});

const Paragraph = z.object({
component_type: z.literal('paragraph'),
text: z.string(),
page: z.number(),
});

const Fieldset = z.object({
component_type: z.literal('fieldset'),
legend: z.string(),
fields: z.union([TxInput, Checkbox]).array(),
page: z.number(),
});

const ExtractedObject = z.object({
Expand Down Expand Up @@ -172,6 +177,7 @@ export const fetchPdfApiResponse: FetchPdfApiResponse = async (
export const processApiResponse = async (json: any): Promise<ParsedPdf> => {
const extracted: ExtractedObject = ExtractedObject.parse(json.parsed_pdf);
const rootSequence: PatternId[] = [];
const pagePatterns: Record<PatternId, PatternId[]> = {};
const parsedPdf: ParsedPdf = {
patterns: {},
errors: [],
Expand Down Expand Up @@ -210,7 +216,9 @@ export const processApiResponse = async (json: any): Promise<ParsedPdf> => {
}
);
if (paragraph) {
rootSequence.push(paragraph.id);
pagePatterns[element.page] = (pagePatterns[element.page] || []).concat(
paragraph.id
);
}
continue;
}
Expand All @@ -226,7 +234,9 @@ export const processApiResponse = async (json: any): Promise<ParsedPdf> => {
}
);
if (checkboxPattern) {
rootSequence.push(checkboxPattern.id);
pagePatterns[element.page] = (pagePatterns[element.page] || []).concat(
checkboxPattern.id
);
parsedPdf.outputs[checkboxPattern.id] = {
type: 'CheckBox',
name: element.id,
Expand All @@ -245,7 +255,6 @@ export const processApiResponse = async (json: any): Promise<ParsedPdf> => {
'radio-group',
{
label: element.legend,
// outputId: element.id,
options: element.options.map(option => ({
id: option.id,
label: option.label,
Expand All @@ -255,7 +264,9 @@ export const processApiResponse = async (json: any): Promise<ParsedPdf> => {
}
);
if (radioGroupPattern) {
rootSequence.push(radioGroupPattern.id);
pagePatterns[element.page] = (pagePatterns[element.page] || []).concat(
radioGroupPattern.id
);
/*
parsedPdf.outputs[radioGroupPattern.id] = {
type: 'RadioGroup',
Expand Down Expand Up @@ -337,27 +348,30 @@ export const processApiResponse = async (json: any): Promise<ParsedPdf> => {
}
);
if (fieldset) {
rootSequence.push(fieldset.id);
pagePatterns[element.page] = (pagePatterns[element.page] || []).concat(
fieldset.id
);
}
}
}

// Create a pattern for the single, first page.
const pagePattern = processPatternData<PagePattern>(
defaultFormConfig,
parsedPdf,
'page',
{
title: 'Untitled Page',
patterns: rootSequence,
}
);

const pages: PatternId[] = [];
if (pagePattern) {
parsedPdf.patterns[pagePattern.id] = pagePattern;
pages.push(pagePattern.id);
}
const pages: PatternId[] = Object.entries(pagePatterns)
.map(([page, patterns]) => {
const pagePattern = processPatternData<PagePattern>(
defaultFormConfig,
parsedPdf,
'page',
{
title: `Page ${parseInt(page) + 1}`,
patterns,
},
undefined,
parseInt(page)
);
return pagePattern?.id;
})
.filter(page => page !== undefined) as PatternId[];

// Assign the page to the root page set.
const rootPattern = processPatternData<PageSetPattern>(
Expand All @@ -380,7 +394,8 @@ const processPatternData = <T extends Pattern>(
parsedPdf: ParsedPdf,
patternType: T['type'],
patternData: T['data'],
patternId?: PatternId
patternId?: PatternId,
page?: number
) => {
const result = createPattern<T>(config, patternType, patternData, patternId);
if (!result.success) {
Expand Down
37 changes: 37 additions & 0 deletions packages/forms/src/patterns/page-set/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from 'zod';
import {
type CreatePrompt,
type PageSetProps,
type PromptAction,
createPromptForPattern,
getPattern,
} from '../..';
Expand Down Expand Up @@ -31,10 +32,12 @@ export const createPrompt: CreatePrompt<PageSetPattern> = (
),
]
: [];
const actions = getActionsForPage(pattern.data.pages.length, activePage);
return {
props: {
_patternId: pattern.id,
type: 'page-set',
actions,
pages: pattern.data.pages.map((patternId, index) => {
const childPattern = getPattern(session.form, patternId) as PagePattern;
if (childPattern.type !== 'page') {
Expand Down Expand Up @@ -64,3 +67,37 @@ const parseRouteData = (pattern: PageSetPattern, routeParams?: RouteData) => {
const schema = getRouteParamSchema(pattern);
return safeZodParseFormErrors(schema, routeParams || {});
};

const getActionsForPage = (
pageCount: number,
pageIndex: number | null
): PromptAction[] => {
if (pageIndex === null) {
return [];
}
const actions: PromptAction[] = [];
if (pageIndex > 0) {
// FIXME: HACK! Don't do this here. We need to pass the form's ID, or its
// URL, to createPrompt.
const pathName = location.hash.split('?')[0];
actions.push({
type: 'link',
text: 'Back',
url: `${pathName}?page=${pageIndex - 1}`,
});
}
if (pageIndex < pageCount - 1) {
actions.push({
type: 'submit',
submitAction: 'next',
text: 'Next',
});
} else {
actions.push({
type: 'submit',
submitAction: 'submit',
text: 'Submit',
});
}
return actions;
};
Loading

0 comments on commit 384a22c

Please sign in to comment.