diff --git a/apps/spotlight/public/sample-documents b/apps/spotlight/public/sample-documents new file mode 120000 index 00000000..d48f4bfb --- /dev/null +++ b/apps/spotlight/public/sample-documents @@ -0,0 +1 @@ +../../../packages/documents/samples \ No newline at end of file diff --git a/apps/spotlight/src/components/react/form-builder/document-importer.tsx b/apps/spotlight/src/components/react/form-builder/document-importer.tsx index 821f936e..01281df1 100644 --- a/apps/spotlight/src/components/react/form-builder/document-importer.tsx +++ b/apps/spotlight/src/components/react/form-builder/document-importer.tsx @@ -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 }) => { @@ -103,7 +140,7 @@ 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 }); })} @@ -111,14 +148,26 @@ export const DocumentImporter = () => { @@ -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 || {}, }); }} > - {/**/} + {/**/} + ); }; const PreviewFormPage = () => { + const previewForm = addDocumentFieldsToForm( + form, + state.documentFields || {} + ); + const formContext = createFormContext(previewForm); + const prompt = createPrompt(formContext); return ( -
{ - dispatch({ - type: 'SAVE_FORM_FIELDS', - data: state.suggestedForm as SuggestedForm, - }); - }} - > - - - + <> + +
{ + event.preventDefault(); + saveFormToStorage(window.localStorage, formId, previewForm); + navigate(`/${formId}/edit`); + }} + > + + + ); }; diff --git a/apps/spotlight/src/components/react/form/import-document.tsx b/apps/spotlight/src/components/react/form/import-document.tsx index 831d7ad4..b7bcece6 100644 --- a/apps/spotlight/src/components/react/form/import-document.tsx +++ b/apps/spotlight/src/components/react/form/import-document.tsx @@ -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 }) => { @@ -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 ; + return ; }; diff --git a/packages/documents/samples/alabama-name-change/ps-12.pdf b/packages/documents/samples/alabama-name-change/ps-12.pdf new file mode 100644 index 00000000..0e6add9c Binary files /dev/null and b/packages/documents/samples/alabama-name-change/ps-12.pdf differ diff --git a/packages/documents/src/__tests__/extract.test.ts b/packages/documents/src/__tests__/extract.test.ts index 42a1c1ae..1414e82f 100644 --- a/packages/documents/src/__tests__/extract.test.ts +++ b/packages/documents/src/__tests__/extract.test.ts @@ -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', diff --git a/packages/documents/src/__tests__/fill-pdf.test.ts b/packages/documents/src/__tests__/fill-pdf.test.ts index d28b1a61..4c61e7d3 100644 --- a/packages/documents/src/__tests__/fill-pdf.test.ts +++ b/packages/documents/src/__tests__/fill-pdf.test.ts @@ -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', () => { @@ -28,7 +28,7 @@ describe('PDF form filler', () => { Weight: { type: 'TextField', value: 'weightField' }, })) as Success; 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' }, diff --git a/packages/documents/src/__tests__/suggestions.test.ts b/packages/documents/src/__tests__/suggestions.test.ts index 01241fa0..18222b9c 100644 --- a/packages/documents/src/__tests__/suggestions.test.ts +++ b/packages/documents/src/__tests__/suggestions.test.ts @@ -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 () => { diff --git a/packages/documents/src/document.ts b/packages/documents/src/document.ts index d5a4b00d..7179eff1 100644 --- a/packages/documents/src/document.ts +++ b/packages/documents/src/document.ts @@ -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; + +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); +}; diff --git a/packages/documents/src/index.ts b/packages/documents/src/index.ts index deb22a6c..d846d4a5 100644 --- a/packages/documents/src/index.ts +++ b/packages/documents/src/index.ts @@ -1,2 +1,3 @@ +export * from './document'; export * from './pdf'; export * from './suggestions'; diff --git a/packages/documents/src/pdf/extract.ts b/packages/documents/src/pdf/extract.ts index 32a4ce34..ae7ac08d 100644 --- a/packages/documents/src/pdf/extract.ts +++ b/packages/documents/src/pdf/extract.ts @@ -1,8 +1,10 @@ import * as pdfLib from 'pdf-lib'; -import { type PDFFieldType } from '.'; +import { DocumentFieldMap, DocumentFieldValue } from '../document'; -export const extractFormFieldData = async (pdfBytes: Uint8Array) => { +export const getDocumentFieldData = async ( + pdfBytes: Uint8Array +): Promise => { const pdfDoc = await pdfLib.PDFDocument.load(pdfBytes); const form = pdfDoc.getForm(); const fields = form.getFields(); @@ -13,33 +15,45 @@ export const extractFormFieldData = async (pdfBytes: Uint8Array) => { ); }; -const getFieldValue = ( - field: pdfLib.PDFField -): { type: PDFFieldType | 'not-supported'; value: any } => { +const getFieldValue = (field: pdfLib.PDFField): DocumentFieldValue => { if (field instanceof pdfLib.PDFTextField) { return { type: 'TextField', - value: field.getText(), + name: field.getName(), + label: field.getName(), + value: field.getText() || '', + maxLength: field.getMaxLength(), + required: field.isRequired(), }; } else if (field instanceof pdfLib.PDFCheckBox) { return { type: 'CheckBox', + name: field.getName(), + label: field.getName(), value: field.isChecked(), + required: field.isRequired(), }; } else if (field instanceof pdfLib.PDFDropdown) { return { type: 'Dropdown', + name: field.getName(), + label: field.getName(), value: field.getSelected(), + required: field.isRequired(), }; } else if (field instanceof pdfLib.PDFOptionList) { return { type: 'OptionList', + name: field.getName(), + label: field.getName(), value: field.getSelected(), + required: field.isRequired(), }; } else { return { type: 'not-supported', - value: 'not-supported', + name: field.getName(), + error: `unsupported type: ${field.constructor.name}`, }; } }; diff --git a/packages/documents/src/pdf/index.ts b/packages/documents/src/pdf/index.ts index 3bd27f07..670b018e 100644 --- a/packages/documents/src/pdf/index.ts +++ b/packages/documents/src/pdf/index.ts @@ -1,4 +1,4 @@ -export { extractFormFieldData } from './extract'; +export { getDocumentFieldData } from './extract'; export { fillPDF } from './generate'; export { generateDummyPDF } from './generate-dummy'; diff --git a/packages/documents/src/suggestions.ts b/packages/documents/src/suggestions.ts index 6277d50d..cb070b91 100644 --- a/packages/documents/src/suggestions.ts +++ b/packages/documents/src/suggestions.ts @@ -1,3 +1,5 @@ +import { DocumentFieldMap } from './document'; + export type SuggestedForm = { id: string; tag: 'input' | 'textarea'; @@ -6,11 +8,15 @@ export type SuggestedForm = { value?: string; type?: 'text'; }[]; -export const suggestFormDetails = (docData: any) => { +export const suggestFormDetails = ( + docData: DocumentFieldMap +): DocumentFieldMap => { + /* const cache = getFakeCache(); const hash = getObjectHash(docData); const data = cache.get(hash); - return data as SuggestedForm; + */ + return docData; }; const getFakeCache = () => { diff --git a/packages/forms/src/index.ts b/packages/forms/src/index.ts index 9f9266a5..575af691 100644 --- a/packages/forms/src/index.ts +++ b/packages/forms/src/index.ts @@ -5,7 +5,7 @@ type QuestionId = string; export type Question = { id: QuestionId; text: string; - initial: string; + initial: string | boolean | string[]; // TODO: create separate types required: boolean; }; type QuestionValue = any;