From 24749e3000372d6c4ad6e7f44d8c8650f0cd86b7 Mon Sep 17 00:00:00 2001 From: Daniel Naab Date: Mon, 15 Jan 2024 10:52:15 -0600 Subject: [PATCH] Wire document downloads up to form submission. This involved some clean-up of the structure of the document importer and other refactoring. Additionally, there are inconsistencies on the imported fields that is preventing the PDFs from being properly filled, so that will be be follow-up work. --- apps/rest-api/package.json | 2 +- apps/spotlight/package.json | 2 +- .../react/form-builder/document-importer.tsx | 224 ++++++++++++------ .../src/components/react/form/delete.tsx | 7 +- .../src/components/react/form/edit.tsx | 5 +- .../components/react/form/import-document.tsx | 3 +- .../src/components/react/form/list.tsx | 2 +- .../src/components/react/form/view.tsx | 55 ++++- apps/spotlight/src/lib/form-repo.ts | 52 ---- packages/documents/src/document.ts | 40 +--- packages/documents/src/pdf/generate.ts | 20 ++ packages/documents/src/pdf/index.ts | 15 +- packages/form-service/.gitignore | 1 + packages/form-service/README.md | 3 + packages/form-service/package.json | 16 ++ packages/form-service/src/browser.ts | 51 ++++ packages/form-service/src/form-repo.ts | 93 ++++++++ packages/form-service/src/index.ts | 3 + packages/form-service/src/types.ts | 6 + packages/form-service/tsconfig.json | 11 + packages/forms/dist/index.js | 11 +- packages/forms/package.json | 2 +- packages/forms/src/documents/index.ts | 37 +++ packages/forms/src/index.ts | 25 +- pnpm-lock.yaml | 25 +- tsconfig.json | 6 + workspace-dependencies.svg | 128 +++++----- 27 files changed, 579 insertions(+), 266 deletions(-) delete mode 100644 apps/spotlight/src/lib/form-repo.ts create mode 100644 packages/form-service/.gitignore create mode 100644 packages/form-service/README.md create mode 100644 packages/form-service/package.json create mode 100644 packages/form-service/src/browser.ts create mode 100644 packages/form-service/src/form-repo.ts create mode 100644 packages/form-service/src/index.ts create mode 100644 packages/form-service/src/types.ts create mode 100644 packages/form-service/tsconfig.json create mode 100644 packages/forms/src/documents/index.ts diff --git a/apps/rest-api/package.json b/apps/rest-api/package.json index 3a497905..a6a4e103 100644 --- a/apps/rest-api/package.json +++ b/apps/rest-api/package.json @@ -10,7 +10,7 @@ "dev": "tsup src/* --watch" }, "dependencies": { - "@atj/interviews": "workspace:*" + "@atj/form-service": "workspace:*" }, "devDependencies": { "@types/aws-lambda": "^8.10.109", diff --git a/apps/spotlight/package.json b/apps/spotlight/package.json index 3ead725d..d15bd2f3 100644 --- a/apps/spotlight/package.json +++ b/apps/spotlight/package.json @@ -12,8 +12,8 @@ "dependencies": { "@astrojs/react": "^3.0.3", "@atj/design": "workspace:*", - "@atj/docassemble": "workspace:*", "@atj/documents": "workspace:*", + "@atj/form-service": "workspace:*", "@atj/forms": "workspace:*", "@atj/interviews": "workspace:*", "astro": "^3.5.4", 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 01281df1..a0be9de3 100644 --- a/apps/spotlight/src/components/react/form-builder/document-importer.tsx +++ b/apps/spotlight/src/components/react/form-builder/document-importer.tsx @@ -1,37 +1,22 @@ import React, { PropsWithChildren, useReducer } from 'react'; +import { useNavigate } from 'react-router-dom'; import { - DocumentFieldMap, addDocumentFieldsToForm, getDocumentFieldData, suggestFormDetails, } from '@atj/documents'; +import { + DocumentFieldMap, + Form, + addDocument, + createFormContext, + createPrompt, +} from '@atj/forms'; 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; - form: Form; - documentFields?: DocumentFieldMap; - previewForm?: Form; -}; -type Action = - | { - type: 'SELECT_PDF'; - data: DocumentFieldMap; - } - | { - type: 'PREVIEW_FORM'; - data: DocumentFieldMap; - } - | { - type: 'GOTO_PAGE'; - page: number; - }; +import { saveFormToStorage } from '@atj/form-service'; export const DocumentImporter = ({ formId, @@ -40,46 +25,7 @@ export const DocumentImporter = ({ formId: string; form: Form; }) => { - const navigate = useNavigate(); - const [state, dispatch] = useReducer( - (state: State, action: Action) => { - if (action.type === 'SELECT_PDF') { - return { - page: 2, - documentFields: action.data, - form: state.form, - }; - } - if (action.type === 'PREVIEW_FORM') { - return { - page: 3, - documentFields: action.data, - form: state.form, - }; - } - if (action.type === 'GOTO_PAGE') { - return { - ...state, - page: action.page, - form: state.form, - }; - } - return state; - }, - { - 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 { state, actions } = useDocumentImporter(form); const Step: React.FC< PropsWithChildren<{ title: string; step: number; current: number }> @@ -108,7 +54,7 @@ export const DocumentImporter = ({
  • diff --git a/apps/spotlight/src/components/react/form/edit.tsx b/apps/spotlight/src/components/react/form/edit.tsx index 66fd5b1d..40223be1 100644 --- a/apps/spotlight/src/components/react/form/edit.tsx +++ b/apps/spotlight/src/components/react/form/edit.tsx @@ -3,7 +3,7 @@ import { useForm } from 'react-hook-form'; import { Link } from 'react-router-dom'; import { Form, addQuestions, createForm, getFlatFieldList } from '@atj/forms'; -import { getFormFromStorage, saveFormToStorage } from '../../../lib/form-repo'; +import { getFormFromStorage, saveFormToStorage } from '@atj/form-service'; export const FormEdit = ({ formId }: { formId: string }) => { const form = getFormFromStorage(window.localStorage, formId); @@ -14,7 +14,6 @@ export const FormEdit = ({ formId }: { formId: string }) => {

    Edit form interface

    Editing form {formId}
    - {JSON.stringify(form)}
    ); }; + +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); +}; diff --git a/apps/spotlight/src/lib/form-repo.ts b/apps/spotlight/src/lib/form-repo.ts deleted file mode 100644 index 3a48de2d..00000000 --- a/apps/spotlight/src/lib/form-repo.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Form, FormSummary, createForm } from '@atj/forms'; - -export const getFormFromStorage = ( - storage: Storage, - id?: string -): Form | null => { - if (!storage || !id) { - return null; - } - const formString = storage.getItem(id); - if (!formString) { - return null; - } - return JSON.parse(formString); -}; - -export const getFormListFromStorage = (storage: Storage) => { - const keys = []; - for (let i = 0; i < storage.length; i++) { - const key = storage.key(i); - keys.push(key); - } - return keys; -}; - -export const addFormSummaryToStorage = ( - storage: Storage, - summary: FormSummary -) => { - const form = createForm(summary); - const uuid = crypto.randomUUID(); - storage.setItem(uuid, JSON.stringify(form)); - return { - success: true, - data: uuid, - }; -}; - -export const saveFormToStorage = ( - storage: Storage, - formId: string, - form: Form -) => { - storage.setItem(formId, JSON.stringify(form)); - return { - success: true, - }; -}; - -export const deleteFormFromStorage = (storage: Storage, formId: string) => { - storage.removeItem(formId); -}; diff --git a/packages/documents/src/document.ts b/packages/documents/src/document.ts index 7179eff1..ba102651 100644 --- a/packages/documents/src/document.ts +++ b/packages/documents/src/document.ts @@ -1,46 +1,8 @@ -import { Form, Question, addQuestions } from '@atj/forms'; +import { DocumentFieldMap, 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 diff --git a/packages/documents/src/pdf/generate.ts b/packages/documents/src/pdf/generate.ts index 7af92144..ecaa14af 100644 --- a/packages/documents/src/pdf/generate.ts +++ b/packages/documents/src/pdf/generate.ts @@ -1,7 +1,27 @@ import { PDFDocument, type PDFForm } from 'pdf-lib'; +import { DocumentFieldMap } from '@atj/forms'; import { PDFFieldType } from '.'; +export const createDocumentFieldData = ( + documentFields: DocumentFieldMap, + formFields: Record, + formData: Record +): Record => { + const results = {} as Record; + Object.entries(documentFields).forEach(([documentId, docField]) => { + if (docField.type === 'not-supported') { + return; + } + const fieldId = formFields[documentId]; + results[documentId] = { + type: docField.type, + value: formData[fieldId], + }; + }); + return results; +}; + export const fillPDF = async ( pdfBytes: Uint8Array, fieldData: Record diff --git a/packages/documents/src/pdf/index.ts b/packages/documents/src/pdf/index.ts index 670b018e..230e9279 100644 --- a/packages/documents/src/pdf/index.ts +++ b/packages/documents/src/pdf/index.ts @@ -1,14 +1,15 @@ export { getDocumentFieldData } from './extract'; -export { fillPDF } from './generate'; +export * from './generate'; export { generateDummyPDF } from './generate-dummy'; export type PDFDocument = { type: 'pdf'; - fields: { - id: string; - type: PDFFieldType; - label: string; - default?: any; - }[]; + fields: PDFField[]; +}; +export type PDFField = { + id: string; + type: PDFFieldType; + label: string; + default?: any; }; export type PDFFieldType = 'TextField' | 'CheckBox' | 'Dropdown' | 'OptionList'; diff --git a/packages/form-service/.gitignore b/packages/form-service/.gitignore new file mode 100644 index 00000000..849ddff3 --- /dev/null +++ b/packages/form-service/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/packages/form-service/README.md b/packages/form-service/README.md new file mode 100644 index 00000000..babb41e5 --- /dev/null +++ b/packages/form-service/README.md @@ -0,0 +1,3 @@ +# @atj/form-service + +The public API for interacting with forms. diff --git a/packages/form-service/package.json b/packages/form-service/package.json new file mode 100644 index 00000000..11e758fe --- /dev/null +++ b/packages/form-service/package.json @@ -0,0 +1,16 @@ +{ + "name": "@atj/form-service", + "version": "1.0.0", + "description": "10x ATJ public forms interface", + "license": "CC0", + "main": "src/index.ts", + "scripts": { + "build": "tsup src/* --env.NODE_ENV production", + "dev": "tsup src/* --watch", + "test": "vitest run --coverage" + }, + "dependencies": { + "@atj/documents": "workspace:*", + "@atj/forms": "workspace:*" + } +} diff --git a/packages/form-service/src/browser.ts b/packages/form-service/src/browser.ts new file mode 100644 index 00000000..4d5b509e --- /dev/null +++ b/packages/form-service/src/browser.ts @@ -0,0 +1,51 @@ +import { createDocumentFieldData, fillPDF } from '@atj/documents'; + +import { type FormService } from '.'; +import { getFormFromStorage } from './form-repo'; + +type Fetch = typeof fetch; + +export const createBrowserFormService = (fetch: Fetch, storage: Storage) => { + return { + submitForm: async (formId: string, formData: Record) => { + const form = getFormFromStorage(storage, formId); + if (form === null) { + return Promise.resolve({ + success: false as const, + error: 'Form not found', + }); + } + const errors = new Array(); + const documents = new Array<{ fileName: string; data: Uint8Array }>(); + for (const document of form.documents) { + const response = await fetch(document.path); + const pdfBytes = new Uint8Array(await response.arrayBuffer()); + console.log('document', document); + const docFieldData = createDocumentFieldData( + document.fields, + document.formFields, + formData + ); + const pdfDocument = await fillPDF(pdfBytes, docFieldData); + if (!pdfDocument.success) { + errors.push(pdfDocument.error); + } else { + documents.push({ + fileName: document.path, + data: pdfDocument.data, + }); + } + } + if (errors.length > 0) { + return { + success: false as const, + error: errors.join('\n'), + }; + } + return { + success: true as const, + data: documents, + }; + }, + }; +}; diff --git a/packages/form-service/src/form-repo.ts b/packages/form-service/src/form-repo.ts new file mode 100644 index 00000000..af208c80 --- /dev/null +++ b/packages/form-service/src/form-repo.ts @@ -0,0 +1,93 @@ +import { Form, FormSummary, createForm } from '@atj/forms'; + +export const getFormFromStorage = ( + storage: Storage, + id?: string +): Form | null => { + if (!storage || !id) { + return null; + } + const formString = storage.getItem(id); + if (!formString) { + return null; + } + return parseStringForm(formString); +}; + +export const getFormListFromStorage = (storage: Storage) => { + const keys = []; + for (let i = 0; i < storage.length; i++) { + const key = storage.key(i); + keys.push(key); + } + return keys; +}; + +export const addFormSummaryToStorage = ( + storage: Storage, + summary: FormSummary +) => { + const form = createForm(summary); + const uuid = crypto.randomUUID(); + storage.setItem(uuid, stringifyForm(form)); + return { + success: true, + data: uuid, + }; +}; + +export const saveFormToStorage = ( + storage: Storage, + formId: string, + form: Form +) => { + storage.setItem(formId, stringifyForm(form)); + return { + success: true, + }; +}; + +export const deleteFormFromStorage = (storage: Storage, formId: string) => { + storage.removeItem(formId); +}; + +const stringifyForm = (form: Form) => { + return JSON.stringify({ + ...form, + documents: form.documents.map(document => ({ + ...document, + // TODO: we probably want to do this somewhere in the documents module + data: uint8ArrayToBase64(document.data), + })), + }); +}; + +const parseStringForm = (formString: string): Form => { + const form = JSON.parse(formString) as Form; + return { + ...form, + documents: form.documents.map(document => ({ + ...document, + data: base64ToUint8Array((document as any).data), + })), + }; +}; + +const uint8ArrayToBase64 = (buffer: Uint8Array): string => { + let binary = ''; + const len = buffer.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(buffer[i]); + } + return btoa(binary); +}; + +const base64ToUint8Array = (base64: string): Uint8Array => { + const binaryString = atob(base64); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; +}; diff --git a/packages/form-service/src/index.ts b/packages/form-service/src/index.ts new file mode 100644 index 00000000..e2cf79d2 --- /dev/null +++ b/packages/form-service/src/index.ts @@ -0,0 +1,3 @@ +export { type FormService } from './types'; +export { createBrowserFormService } from './browser'; +export * from './form-repo'; diff --git a/packages/form-service/src/types.ts b/packages/form-service/src/types.ts new file mode 100644 index 00000000..e0e220f9 --- /dev/null +++ b/packages/form-service/src/types.ts @@ -0,0 +1,6 @@ +export type FormService = { + submitForm: ( + formId: string, + formData: Record + ) => Promise>; +}; diff --git a/packages/form-service/tsconfig.json b/packages/form-service/tsconfig.json new file mode 100644 index 00000000..c4e89861 --- /dev/null +++ b/packages/form-service/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "emitDeclarationOnly": true + }, + "include": [ + "./src" + ], + "references": [] +} diff --git a/packages/forms/dist/index.js b/packages/forms/dist/index.js index a832997b..5067472f 100644 --- a/packages/forms/dist/index.js +++ b/packages/forms/dist/index.js @@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru // src/index.ts var src_exports = {}; __export(src_exports, { + addDocument: () => addDocument, addQuestions: () => addQuestions, createForm: () => createForm, createFormContext: () => createFormContext, @@ -68,7 +69,8 @@ var createForm = (summary, questions = []) => { order: questions.map((question) => { return question.id; }) - } + }, + documents: [] }; }; var createFormContext = (form) => { @@ -145,8 +147,15 @@ var getFlatFieldList = (form) => { return _exhaustiveCheck; } }; +var addDocument = (form, document) => { + return { + ...form, + documents: [...form.documents, document] + }; +}; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { + addDocument, addQuestions, createForm, createFormContext, diff --git a/packages/forms/package.json b/packages/forms/package.json index ed9f697c..afd0cf3f 100644 --- a/packages/forms/package.json +++ b/packages/forms/package.json @@ -1,7 +1,7 @@ { "name": "@atj/forms", "version": "1.0.0", - "description": "10x ATJ business logic", + "description": "10x ATJ form handling", "license": "CC0", "main": "src/index.ts", "scripts": { diff --git a/packages/forms/src/documents/index.ts b/packages/forms/src/documents/index.ts new file mode 100644 index 00000000..35bc2ef8 --- /dev/null +++ b/packages/forms/src/documents/index.ts @@ -0,0 +1,37 @@ +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; diff --git a/packages/forms/src/index.ts b/packages/forms/src/index.ts index 77704a26..9b93d8d0 100644 --- a/packages/forms/src/index.ts +++ b/packages/forms/src/index.ts @@ -1,3 +1,6 @@ +import { DocumentFieldMap } from './documents'; + +export * from './documents'; export * from './prompts'; type QuestionId = string; @@ -21,10 +24,7 @@ export type Form = { summary: FormSummary; questions: Record; strategy: T; - documents: { - path: string; - fieldMap: Record; - }[]; + documents: FormDocument[]; }; export type FormContext = { @@ -46,6 +46,13 @@ export type NullStrategy = { export type FormStrategy = SequentialStrategy | NullStrategy; +export type FormDocument = { + data: Uint8Array; + path: string; + fields: DocumentFieldMap; + formFields: Record; +}; + export const createForm = ( summary: FormSummary, questions: Question[] = [] @@ -160,3 +167,13 @@ export const getFlatFieldList = (form: Form) => { return _exhaustiveCheck; } }; + +export const addDocument = ( + form: Form, + document: FormDocument +) => { + return { + ...form, + documents: [...form.documents, document], + }; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a741ee9b..ca1ecfc4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,9 +60,9 @@ importers: apps/rest-api: dependencies: - '@atj/interviews': + '@atj/form-service': specifier: workspace:* - version: link:../../packages/interviews + version: link:../../packages/form-service devDependencies: '@types/aws-lambda': specifier: ^8.10.109 @@ -79,12 +79,12 @@ importers: '@atj/design': specifier: workspace:* version: link:../../packages/design - '@atj/docassemble': - specifier: workspace:* - version: link:../../packages/docassemble '@atj/documents': specifier: workspace:* version: link:../../packages/documents + '@atj/form-service': + specifier: workspace:* + version: link:../../packages/form-service '@atj/forms': specifier: workspace:* version: link:../../packages/forms @@ -206,6 +206,15 @@ importers: specifier: ^20.10.4 version: 20.10.4 + packages/form-service: + dependencies: + '@atj/documents': + specifier: workspace:* + version: link:../documents + '@atj/forms': + specifier: workspace:* + version: link:../forms + packages/forms: {} packages/interviews: {} @@ -4526,7 +4535,7 @@ packages: dependencies: semver: 7.5.4 shelljs: 0.8.5 - typescript: 5.4.0-dev.20240108 + typescript: 5.4.0-dev.20240112 dev: false /dset@3.1.3: @@ -10890,8 +10899,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - /typescript@5.4.0-dev.20240108: - resolution: {integrity: sha512-flXeU+FYwW3mL6zcOz1lNX0juSolUIeIRs4nO8j77jB+N/3lrqWNOSz05dzRC4eYJcjFIIOvv5y9u8MQ5nTtzg==} + /typescript@5.4.0-dev.20240112: + resolution: {integrity: sha512-jpdPO53r47ZiTebHw2O9jKp1ItTtwPe9kc62LGCZHEexROJn1MXChI7l/UmRtItOhOKwe2ZXOBlNMQh4YTMhIg==} engines: {node: '>=14.17'} hasBin: true dev: false diff --git a/tsconfig.json b/tsconfig.json index df44dd7b..9d3181ba 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,12 @@ { "path": "./packages/documents/tsconfig.json" }, + { + "path": "./packages/form-service/tsconfig.json" + }, + { + "path": "./packages/forms/tsconfig.json" + }, { "path": "./packages/interviews/tsconfig.json" }, diff --git a/workspace-dependencies.svg b/workspace-dependencies.svg index 4b55cd29..91498eb9 100644 --- a/workspace-dependencies.svg +++ b/workspace-dependencies.svg @@ -4,130 +4,142 @@ - + workspace - + @atj/cli-app - -@atj/cli-app + +@atj/cli-app @atj/interviews - -@atj/interviews + +@atj/interviews @atj/cli-app->@atj/interviews - - + + @atj/dependency-graph - -@atj/dependency-graph + +@atj/dependency-graph @atj/cli-app->@atj/dependency-graph - - + + @atj/docassemble - -@atj/docassemble + +@atj/docassemble @atj/cli-app->@atj/docassemble - - + + - + @atj/docassemble->@atj/interviews - - + + - + @atj/documents - -@atj/documents + +@atj/documents - + @atj/docassemble->@atj/documents - - + + - + +@atj/form-rest-api + +@atj/form-rest-api + + + @atj/form-service - -@atj/form-service + +@atj/form-service - + -@atj/form-service->@atj/interviews - - +@atj/form-rest-api->@atj/form-service + + - + @atj/spotlight - -@atj/spotlight + +@atj/spotlight - + @atj/spotlight->@atj/interviews - - + + - - -@atj/spotlight->@atj/docassemble - - + + +@atj/spotlight->@atj/form-service + + - + @atj/design - -@atj/design + +@atj/design @atj/spotlight->@atj/design - - + + - + @atj/spotlight->@atj/documents - - + + - + @atj/forms - -@atj/forms + +@atj/forms + + + +@atj/spotlight->@atj/forms + + - + @atj/documents->@atj/forms - - + +