// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 import { FormMetadata, FormReviewWorkflowTag, FormSchema } from '@aws/api-typescript'; import { Alert, Badge, Box, Button, Checkbox, Column, ColumnLayout, Container, FormField, Heading, Inline, Input, Modal, Multiselect } from 'aws-northstar'; import Table, { Row } from 'aws-northstar/components/Table'; import { Column as TableColumn } from 'aws-northstar/components/Table/types'; import Grid from 'aws-northstar/layouts/Grid'; import _ from 'lodash'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { API } from '../../api/client'; import { listAllPages } from '../../api/utils'; import { flattenFormSchema, FormValue, stringifyDataAccordingToSchema, stringifySchema, } from '../../utils/review-panel/schema-helper'; import { ReviewMultiselectTagOption, tagIdsToTags } from '../../utils/review-tags-helper'; import { updateStatus } from '../../utils/status-update-helper'; import { SchemaEditor } from '../document-schemas/schema-editor'; import { DrawWrapper, PdfViewer } from '../pdf/pdf-viewer'; export interface FormReviewPanelProps { readonly documentForm: FormMetadata; readonly formSchema: FormSchema; readonly isReadOnly: boolean; } // transform a list of form review workflow tag objects into those consumed // by the Multiselect component export const tagsToMultiselectOptions = (tags: any): ReviewMultiselectTagOption[] => { return tags?.map((tag: any) => { return { label: tag.tagText, value: tag.tagId }; }); }; // extract list of tag ids from a list of Multiselect component value objects export const multiselectOptionsToTagIds = (options: any): string[] => { return options?.map((tag: any) => tag.value); }; /** * Component to render a content review UI for a given form, rendering * the disclosure form PDF on the right of the UI and the textract * extracted content in a table on the left. */ export const FormReviewPanel: React.FC = ({ documentForm, formSchema, isReadOnly }) => { const selectedFormField = useRef(); const [showRawDataModal, setShowRawDataModal] = useState(false); const [pageNumber, setPageNumber] = useState(1); const [isReviewComplete, setIsReviewComplete] = useState(false); const [isSubmittingReview, setIsSubmittingReview] = useState(false); const [showSuccessAlert, setShowSuccessAlert] = useState(false); const [reviewTags, setReviewTags] = useState(); const [selectedReviewTags, setSelectedReviewTags] = useState(); const [availableReviewWorkflowTags, setAvailableReviewWorkflowTags] = useState(); let [docForm, setDocForm] = useState(documentForm); const updateFormValues = (formValueList: FormValue[], row: Row, e: string) => { formValueList.filter(x => x.key === `${row.original.key}`) .map(f => { _.update(f, 'value', function () { return e; }); }); }; // Ref used for us to draw on the page const drawRef = useRef(() => {}); // The form's extracted data is in the shape defined by the schema, flatten here for more intuitive review const flattenedFields = useMemo(() => flattenFormSchema(docForm), [docForm]); const [formValues, setFormValues] = useState(flattenedFields); useEffect(() => { void (async () => { const tags = await listAllPages(API.listFormReviewWorkflowTags.bind(API), 'tags'); setAvailableReviewWorkflowTags(tags); setReviewTags(tagsToMultiselectOptions(tags)); setSelectedReviewTags(tagsToMultiselectOptions(tagIdsToTags(docForm?.tags, tags))); })(); setFormValues(formValues); }, [flattenedFields]); const drawBoundingBoxForSelectedRow = useCallback(() => { const field = selectedFormField.current; if (!drawRef.current || !field) { return; } drawRef.current((ctx, { width, height }) => { // Only draw the box if we're on the right page to display it if (field.page !== undefined && (field.page + 1) === pageNumber) { ctx.fillStyle = 'rgba(144, 238, 144, 0.5)'; ctx.fillRect(field.boundingBox.left * width, field.boundingBox.top * height, field.boundingBox.width * width, field.boundingBox.height * height); } }); }, [drawRef, selectedFormField, pageNumber]); const onPageRenderSuccess = useCallback(() => { // Redraw the bounding box whenever we change page drawBoundingBoxForSelectedRow(); }, [drawBoundingBoxForSelectedRow]); const onSelectRow = useCallback((row) => { selectedFormField.current = row; if (!row) { return; } if (row.page !== undefined && (row.page + 1) !== pageNumber) { // We've selected a row which has a box on a different page to the current page number, so jump to that page. // We return here since the page change is not immediate - rendering of the new page will then trigger drawing // the bounding box. setPageNumber(row.page + 1); return; } // Draw the bounding box for the selected row drawBoundingBoxForSelectedRow(); }, [pageNumber, selectedFormField, drawBoundingBoxForSelectedRow]); const onSelectionChange = useCallback((event) => { if (event[0]?.key !== selectedFormField.current?.key) { onSelectRow(event[0]); } }, [onSelectRow, selectedFormField]); const formFields: TableColumn[] = useMemo(() => [ { id: 'fieldName', width: 200, Header: 'Name', accessor: 'key', Cell: ({ value }) => ({value}), }, { id: 'fieldValue', width: 300, Header: 'Value', accessor: 'value', Cell: ({ value, row }) => { if (isReadOnly) { return {value}; } return ( { onSelectRow(row.original); }} onChange={ (e) => { _.set(docForm, `extractedData[${row.original.key}]`, e); setDocForm(docForm); updateFormValues(formValues, row, e); } } /> ); }, }, { id: 'confidence', width: 100, Header: 'Confidence', accessor: 'confidence', Cell: ({ value }) => ({Number(value).toFixed(2) + ' %'}), }, { id: 'method', width: 100, Header: 'Extraction Method', accessor: 'extractionMethod', Cell: ({ value }) => ({value}), }, ], [isReadOnly, onSelectRow, setDocForm, updateFormValues]); const history = useHistory(); const updateExtractedData = async () => { setIsSubmittingReview(true); setShowSuccessAlert(false); const reviewNotes = (document.getElementById('reviewNotesTextArea') as HTMLInputElement).value; await API.updateFormReview({ updateFormInput: { extractedData: docForm.extractedData, ...((reviewNotes && reviewNotes.length > 0) ? { notes: reviewNotes } : {}), ...(selectedReviewTags ? { tags: multiselectOptionsToTagIds(selectedReviewTags) } : {}), }, documentId: docForm.documentId, formId: docForm.formId, }); if (isReviewComplete) { await updateStatus(docForm.documentId, docForm.formId, 'REVIEWED'); } setShowSuccessAlert(true); setIsSubmittingReview(false); history.goBack(); }; const reviewToolbar = (
Tags
{ isReadOnly ? ( tagIdsToTags( docForm?.tags ?? [], availableReviewWorkflowTags)?.map((item: any) => (), ) ) : ( { setSelectedReviewTags(tags); }} disabled={isReadOnly} options={reviewTags} value={selectedReviewTags} controlId={'tags'} checkboxes={true} />) }
{ setIsReviewComplete(e.target.checked); }}> Review complete
); return ( {isReadOnly && } )} > {showSuccessAlert && Successfully updated review } row.key} selectedRowIds={selectedFormField.current ? [selectedFormField.current.key] : []} tableTitle='Form Fields' columnDefinitions={formFields} items={formValues} disableGroupBy={true} disableSettings={true} disablePagination={true} disableFilters={true} disableSortBy={true} disableRowSelect={!isReadOnly} multiSelect={false} rowCount={formValues.length} defaultPageSize={formValues.length} /> setShowRawDataModal(false)} width="70%"> {formSchema.title} Schema
Extracted Data
); };