Skip to content

Commit

Permalink
Wire PDF field extraction to "import PDF" function.
Browse files Browse the repository at this point in the history
  • Loading branch information
danielnaab committed Jan 12, 2024
1 parent 79b0ce3 commit 14230f3
Show file tree
Hide file tree
Showing 13 changed files with 214 additions and 54 deletions.
1 change: 1 addition & 0 deletions apps/spotlight/public/sample-documents
123 changes: 92 additions & 31 deletions apps/spotlight/src/components/react/form-builder/document-importer.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,86 @@
import React, { PropsWithChildren, useReducer } from 'react';

import { extractFormFieldData, suggestFormDetails } from '@atj/documents';
import { SuggestedForm, UD105_TEST_DATA } from '@atj/documents';
import {
DocumentFieldMap,
addDocumentFieldsToForm,
getDocumentFieldData,
suggestFormDetails,
} from '@atj/documents';

import { onFileInputChangeGetFile } from '../../../lib/file-input';
import { FormView } from '../form/view';
import { Form, createFormContext, createPrompt } from '@atj/forms';
import { saveFormToStorage } from '../../../lib/form-repo';
import { useNavigate } from 'react-router-dom';

type State = { page: number; suggestedForm?: SuggestedForm };
type State = {
page: number;
form: Form;
documentFields?: DocumentFieldMap;
previewForm?: Form;
};
type Action =
| { type: 'SELECT_PDF'; data: SuggestedForm }
| {
type: 'SAVE_FORM_FIELDS';
data: SuggestedForm;
type: 'SELECT_PDF';
data: DocumentFieldMap;
}
| {
type: 'PREVIEW_FORM';
data: DocumentFieldMap;
}
| {
type: 'GOTO_PAGE';
page: number;
};

export const DocumentImporter = () => {
export const DocumentImporter = ({
formId,
form,
}: {
formId: string;
form: Form;
}) => {
const navigate = useNavigate();
const [state, dispatch] = useReducer(
(state: State, action: Action) => {
if (action.type === 'SELECT_PDF') {
return {
page: 2,
suggestedForm: action.data,
documentFields: action.data,
form: state.form,
};
}
if (action.type === 'SAVE_FORM_FIELDS') {
if (action.type === 'PREVIEW_FORM') {
return {
page: 3,
suggestedForm: action.data,
documentFields: action.data,
form: state.form,
};
}
if (action.type === 'GOTO_PAGE') {
return {
...state,
page: action.page,
form: state.form,
};
}
return state;
},
{ page: 1 }
{
page: 1,
form: form,
}
);

const selectDocumentByUrl = async (url: string) => {
const response = await fetch(url);
const blob = await response.blob();
const data = new Uint8Array(await blob.arrayBuffer());
const fieldData = await getDocumentFieldData(data);
const fieldInfo = suggestFormDetails(fieldData);
dispatch({ type: 'SELECT_PDF', data: fieldInfo });
};

const Step: React.FC<
PropsWithChildren<{ title: string; step: number; current: number }>
> = ({ children, title, step, current }) => {
Expand Down Expand Up @@ -103,22 +140,34 @@ export const DocumentImporter = () => {
type="file"
accept=".pdf"
onChange={onFileInputChangeGetFile(async fileDetails => {
const fieldData = await extractFormFieldData(fileDetails.data);
const fieldData = await getDocumentFieldData(fileDetails.data);
const fieldInfo = suggestFormDetails(fieldData);
dispatch({ type: 'SELECT_PDF', data: fieldInfo });
})}
/>
</div>
</div>
<label className="usa-label">
Or use an example file, the UD-105 unlawful detainer response:
Or use an example file, selected for testing purposes:
<button
className="usa-button--unstyled"
onClick={async () => {
selectDocumentByUrl(
'sample-documents/ca-unlawful-detainer/ud105.pdf'
);
}}
>
sample-documents/ca-unlawful-detainer/ud105.pdf
</button>
<button
className="usa-button--unstyled"
onClick={() => {
dispatch({ type: 'SELECT_PDF', data: UD105_TEST_DATA });
onClick={async () => {
selectDocumentByUrl(
'sample-documents/alabama-name-change/ps-12.pdf'
);
}}
>
UD-105.pdf
sample-documents/alabama-name-change/ps-12.pdf
</button>
</label>
</div>
Expand All @@ -139,30 +188,42 @@ export const DocumentImporter = () => {
className="usa-form usa-form--large"
onSubmit={event => {
dispatch({
type: 'SAVE_FORM_FIELDS',
data: state.suggestedForm as SuggestedForm,
type: 'PREVIEW_FORM',
data: state.documentFields || {},
});
}}
>
{/*<EditFieldset fields={state.suggestedForm as SuggestedForm} />*/}
{/*<EditFieldset fields={state.suggestedForm} />*/}
<ul>
{Object.values(state.documentFields || {}).map((field, index) => {
return <li key={index}>{JSON.stringify(field)}</li>;
})}
</ul>
<ButtonBar />
</form>
);
};
const PreviewFormPage = () => {
const previewForm = addDocumentFieldsToForm(
form,
state.documentFields || {}
);
const formContext = createFormContext(previewForm);
const prompt = createPrompt(formContext);
return (
<form
className="usa-form usa-form--large"
onSubmit={event => {
dispatch({
type: 'SAVE_FORM_FIELDS',
data: state.suggestedForm as SuggestedForm,
});
}}
>
<FormView prompt={state.suggestedForm as SuggestedForm} />
<ButtonBar />
</form>
<>
<FormView prompt={prompt} />
<form
className="usa-form usa-form--large"
onSubmit={event => {
event.preventDefault();
saveFormToStorage(window.localStorage, formId, previewForm);
navigate(`/${formId}/edit`);
}}
>
<ButtonBar />
</form>
</>
);
};

Expand Down
6 changes: 1 addition & 5 deletions apps/spotlight/src/components/react/form/import-document.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import { getFormFromStorage } from '../../../lib/form-repo';
import { createFormContext, createPrompt } from '@atj/forms';
import { DocumentImporter } from '../form-builder/document-importer';

export const FormDocumentImport = ({ formId }: { formId: string }) => {
Expand All @@ -9,8 +8,5 @@ export const FormDocumentImport = ({ formId }: { formId: string }) => {
if (!form) {
return 'null form retrieved from storage';
}
const context = createFormContext(form);
const prompt = createPrompt(context);

return <DocumentImporter />;
return <DocumentImporter formId={formId} form={form} />;
};
Binary file not shown.
4 changes: 2 additions & 2 deletions packages/documents/src/__tests__/extract.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { describe, expect, it } from 'vitest';

import { extractFormFieldData } from '..';
import { getDocumentFieldData } from '..';
import { loadSamplePDF } from './sample-data';

describe('PDF form field extraction', () => {
it('extracts data from California UD-105 form', async () => {
const pdfBytes = await loadSamplePDF('ca-unlawful-detainer/ud105.pdf');
const fields = await extractFormFieldData(pdfBytes);
const fields = await getDocumentFieldData(pdfBytes);
expect(fields).toEqual({
'UD-105[0].Page4[0].List4[0].Lia[0].Check47[0]': {
type: 'CheckBox',
Expand Down
4 changes: 2 additions & 2 deletions packages/documents/src/__tests__/fill-pdf.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { beforeAll, describe, expect, it } from 'vitest';

import { extractFormFieldData, fillPDF } from '..';
import { getDocumentFieldData, fillPDF } from '..';
import { loadSamplePDF } from './sample-data';

describe('PDF form filler', () => {
Expand Down Expand Up @@ -28,7 +28,7 @@ describe('PDF form filler', () => {
Weight: { type: 'TextField', value: 'weightField' },
})) as Success<Uint8Array>;
expect(result.success).toEqual(true);
const fields = await extractFormFieldData(result.data);
const fields = await getDocumentFieldData(result.data);

expect(fields).toEqual({
'CHARACTER IMAGE': { type: 'not-supported', value: 'not-supported' },
Expand Down
5 changes: 2 additions & 3 deletions packages/documents/src/__tests__/suggestions.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { describe, expect, it } from 'vitest';
import { describe, it } from 'vitest';

import { extractFormFieldData } from '..';
import { loadSampleFields, loadSamplePDF } from './sample-data';
import { loadSampleFields } from './sample-data';

describe('PDF form field extraction', () => {
it('extracts data from California UD-105 form', async () => {
Expand Down
82 changes: 82 additions & 0 deletions packages/documents/src/document.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,85 @@
import { Form, Question, addQuestions } from '@atj/forms';
import { PDFDocument } from './pdf';

export type DocumentTemplate = PDFDocument;

export type DocumentFieldValue =
| {
type: 'TextField';
name: string;
label: string;
value: string;
maxLength?: number;
required: boolean;
}
| {
type: 'CheckBox';
name: string;
label: string;
value: boolean;
required: boolean;
}
| {
type: 'Dropdown';
name: string;
label: string;
value: string[];
required: boolean;
}
| {
type: 'OptionList';
name: string;
label: string;
value: string[];
required: boolean;
}
| {
type: 'not-supported';
name: string;
error: string;
};

export type DocumentFieldMap = Record<string, DocumentFieldValue>;

export const addDocumentFieldsToForm = (
form: Form,
fields: DocumentFieldMap
) => {
const questions: Question[] = [];
Object.entries(fields).map(([key, field]) => {
if (field.type === 'CheckBox') {
questions.push({
id: field.name,
text: field.label,
initial: field.value,
required: field.required,
});
} else if (field.type === 'OptionList') {
questions.push({
id: field.name,
text: field.label,
initial: field.value,
required: field.required,
});
} else if (field.type === 'Dropdown') {
questions.push({
id: field.name,
text: field.label,
initial: field.value,
required: field.required,
});
} else if (field.type === 'TextField') {
questions.push({
id: field.name,
text: field.label,
initial: field.value,
required: field.required,
});
} else if (field.type === 'not-supported') {
console.error(`Skipping field: ${field.error}`);
} else {
const _exhaustiveCheck: never = field;
}
});
return addQuestions(form, questions);
};
1 change: 1 addition & 0 deletions packages/documents/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './document';
export * from './pdf';
export * from './suggestions';
Loading

0 comments on commit 14230f3

Please sign in to comment.