Support table line item in the prebuilt page. (#889)

* Extract regional table.

* Support tootip in tableView component.

* Support table item in prebuilt page.

* update comment.

* Fix lint error.

* Fix lint error.

* Remove console.log and tune style a bit.

Co-authored-by: Buddha Wang <scwang0103@gmail.com>
This commit is contained in:
SimoTw 2021-03-10 12:57:33 +08:00 коммит произвёл GitHub
Родитель 60dcc3b57f
Коммит 19e00bff6d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 388 добавлений и 246 удалений

Просмотреть файл

@ -0,0 +1,176 @@
import React from "react";
export interface IRegionalTableState { }
export interface IRegionalTableProps {
regionalTableToView?: any;
tableTagColor: string;
onMouseEnter: (rowName: string, columnName: string) => void;
onMouseLeave: () => void;
}
export default class RegionalTable extends React.Component<IRegionalTableProps, IRegionalTableState> {
makeOnMouseEnter = (rowName, columnName) => () => {
this.props.onMouseEnter(rowName, columnName);
}
onMouseLeave = () => {
this.props.onMouseLeave();
}
private displayRegionalTable = (regionalTableToView) => {
const tableBody = [];
if (regionalTableToView?.type === "array") {
const columnHeaderRow = [];
const colKeys = Object.keys(regionalTableToView?.valueArray?.[0]?.valueObject || {});
if (colKeys.length === 0) {
return (
<div>
<h5 className="mb-4 ml-2 mt-2 pb-1">
<span style={{ borderBottom: `4px solid ${this.props.tableTagColor}` }}>Table name: {regionalTableToView.fieldName}</span>
</h5>
<div className="table-view-container">
<table>
<tbody>
Empty table
</tbody>
</table>
</div>
</div>
);
}
for (let i = 0; i < colKeys.length + 1; i++) {
if (i === 0) {
columnHeaderRow.push(
<th key={i} className={"empty_header hidden"} />
);
} else {
columnHeaderRow.push(
<th key={i} className={"column_header"}>
{colKeys[i - 1]}
</th>
);
}
}
tableBody.push(<tr key={0}>{columnHeaderRow}</tr>);
regionalTableToView?.valueArray?.forEach((row, rowIndex) => {
const rowName = `#${rowIndex}`;
const tableRow = [];
tableRow.push(
<th key={0} className={"row_header hidden"}>
{rowName}
</th>
);
Object.keys(row?.valueObject).forEach((columnName, columnIndex) => {
const tableCell = row?.valueObject?.[columnName];
tableRow.push(
<td
className={"table-cell"}
key={columnIndex + 1}
onMouseEnter={this.makeOnMouseEnter(rowName, columnName)}
onMouseLeave={this.onMouseLeave}
>
{tableCell ? tableCell.text : null}
</td>
);
})
tableBody.push(<tr key={(rowIndex + 1)}>{tableRow}</tr>);
})
} else {
const columnHeaderRow = [];
const colKeys = Object.keys(regionalTableToView?.valueObject?.[Object.keys(regionalTableToView?.valueObject)?.[0]]?.valueObject || {});
if (colKeys.length === 0) {
return (
<div>
<h5 className="mb-4 ml-2 mt-2 pb-1">
<span style={{ borderBottom: `4px solid ${this.props.tableTagColor}` }}>Table name: {regionalTableToView.fieldName}</span>
</h5>
<div className="table-view-container">
<table>
<tbody>
Empty table
</tbody>
</table>
</div>
</div>
);
}
for (let i = 0; i < colKeys.length + 1; i++) {
if (i === 0) {
columnHeaderRow.push(
<th key={i} className={"empty_header hidden"} />
);
} else {
columnHeaderRow.push(
<th key={i} className={"column_header"}>
{colKeys[i - 1]}
</th>
);
}
}
tableBody.push(<tr key={0}>{columnHeaderRow}</tr>);
Object.keys(regionalTableToView?.valueObject).forEach((rowName, index) => {
const tableRow = [];
tableRow.push(
<th key={0} className={"row_header"}>
{rowName}
</th>
);
if (regionalTableToView?.valueObject?.[rowName]) {
Object.keys(regionalTableToView?.valueObject?.[rowName]?.valueObject)?.forEach((columnName, index) => {
const tableCell = regionalTableToView?.valueObject?.[rowName]?.valueObject?.[columnName];
tableRow.push(
<td
className={"table-cell"}
key={index + 1}
onMouseEnter={() => {
this.setState({ highlightedTableCellRowKey: rowName, highlightedTableCellColumnKey: columnName })
}}
onMouseLeave={() => {
this.setState({ highlightedTableCellRowKey: null, highlightedTableCellColumnKey: null })
}}
>
{tableCell ? tableCell.text : null}
</td>
);
});
} else {
colKeys.forEach((columnName, index) => {
tableRow.push(
<td
className={"table-cell"}
key={index + 1}
>
{null}
</td>
);
})
}
tableBody.push(<tr key={index + 1}>{tableRow}</tr>);
});
}
return (
<div>
<h5 className="mb-4 ml-2 mt-2 pb-1">
<span style={{ borderBottom: `4px solid ${this.props.tableTagColor}` }}>Table name: {regionalTableToView.fieldName}</span>
</h5>
<div className="table-view-container">
<table>
<tbody>
{tableBody}
</tbody>
</table>
</div>
</div>
);
}
render() {
return (
<>
{this.displayRegionalTable(this.props.regionalTableToView)}
</>
);
}
}

Просмотреть файл

@ -2,13 +2,40 @@ import * as React from "react";
import { ICustomizations, Customizer, ContextualMenu, IDragOptions, Modal, FontIcon } from "@fluentui/react";
import { getDarkGreyTheme } from "../../../../common/themes";
import "./tableView.scss";
import { TooltipHost, TooltipDelay, DirectionalHint, ITooltipProps, ITooltipHostStyles } from "@fluentui/react";
import { useId } from '@uifabric/react-hooks';
function Tooltip({ children, content }) {
const makeTooltipProps = (content: object): ITooltipProps => ({
onRenderContent: () => (
<ul style={{ margin: 10, padding: 0 }}>
{Object.keys(content).map((key, index) => content[key] ? <li key={index}>{`${key}: ${content[key]}`}</li> : null)}
</ul>
),
});
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: 'inline-block' } };
const tooltipId = useId('tooltip');
const tooltipProps = makeTooltipProps(content);
return (
<TooltipHost
delay={TooltipDelay.zero}
directionalHint={DirectionalHint.topCenter}
id={tooltipId}
tooltipProps={tooltipProps}
styles={hostStyles}
>
{children}
</TooltipHost>
)
}
interface ITableViewProps {
handleTableViewClose: () => any;
tableToView: object;
showToolTips?: boolean;
}
export const TableView: React.FunctionComponent<ITableViewProps> = (props) => {
export const TableView: React.FunctionComponent<ITableViewProps> = ({ handleTableViewClose, tableToView, showToolTips = false }) => {
const dark: ICustomizations = {
settings: {
theme: getDarkGreyTheme(),
@ -23,46 +50,49 @@ export const TableView: React.FunctionComponent<ITableViewProps> = (props) => {
};
function getTableBody() {
const table = props.tableToView;
const table = tableToView;
let tableBody = null;
if (table !== null) {
tableBody = [];
const rows = table["rows"];
// const columns = table["columns"];
for (let i = 0; i < rows; i++) {
const tableRow = [];
tableBody.push(<tr key={i}>{tableRow}</tr>);
}
table["cells"].forEach((cell) => {
const rowIndex = cell["rowIndex"];
const columnIndex = cell["columnIndex"];
const rowSpan = cell["rowSpan"];
const colSpan = cell["columnSpan"];
tableBody[rowIndex]["props"]["children"][columnIndex] =
table["cells"].forEach(({ rowIndex, columnIndex, rowSpan, colSpan, text, confidence }) => {
const content = { confidence: confidence || null };
const hasContentValue = Object.values(content).reduce((hasValue, value) => value || hasValue, false);
tableBody[rowIndex]["props"]["children"][columnIndex] = (
<td key={columnIndex} colSpan={colSpan} rowSpan={rowSpan}>
{cell["text"]}
</td>;
{showToolTips && hasContentValue ? (
<Tooltip content={content}>
{text}
</Tooltip>
) : (
<React.Fragment>{text}</React.Fragment>
)}
</td>
)
});
}
return tableBody;
}
return (
<Customizer {...dark}>
<Modal
titleAriaId={"Table view"}
isOpen={props.tableToView !== null}
isOpen={tableToView !== null}
isModeless={false}
isDarkOverlay={true}
dragOptions={dragOptions}
onDismiss={props.handleTableViewClose}
onDismiss={handleTableViewClose}
scrollableContentClassName={"table-view-scrollable-content"}
>
<FontIcon
className="close-modal"
role="button"
iconName="Cancel"
onClick={props.handleTableViewClose}
onClick={handleTableViewClose}
/>
<div className="table-view-container">
<table className="viewed-table">

Просмотреть файл

@ -38,7 +38,7 @@ import PreventLeaving from "../../common/preventLeaving/preventLeaving";
import { CanvasCommandBar } from "../editorPage/canvasCommandBar";
import { TableView } from "../editorPage/tableView";
import "../predict/predictPage.scss";
import PredictResult from "../predict/predictResult";
import PredictResult, { ITableResultItem } from "../predict/predictResult";
import { ILoadFileHelper, ILoadFileResult, LoadFileHelper } from "./LoadFileHelper";
import "./prebuiltPredictPage.scss";
import { ITableHelper, ITableState, TableHelper } from "./tableHelper";
@ -83,6 +83,10 @@ export interface IPrebuiltPredictPageState extends ILoadFileResult, ITableState
predictionEndpointUrl: string;
liveMode: boolean;
viewRegionalTable?: boolean;
regionalTableToView?: any;
tableTagColor?: string;
}
function mapStateToProps(state: IApplicationState) {
@ -158,6 +162,10 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
predictionEndpointUrl: "",
liveMode: true,
viewRegionalTable: false,
regionalTableToView: null,
tableTagColor: null,
};
private analyzeResults: any;
@ -372,12 +380,20 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
onPredictionClick={this.onPredictionClick}
onPredictionMouseEnter={this.onPredictionMouseEnter}
onPredictionMouseLeave={this.onPredictionMouseLeave}
onTablePredictionClick={this.onTablePredictionClick}
/>
}
{
(Object.keys(predictions).length === 0 && this.state.predictionLoaded) &&
<div>{strings.prebuiltPredict.noFieldCanBeExtracted}</div>
}
{this.state.viewRegionalTable &&
<TableView
handleTableViewClose={this.onTablePredictionClose}
tableToView={this.state.regionalTableToView}
showToolTips={true}
/>
}
</div>
</div>
<Alert
@ -810,13 +826,10 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
private drawPredictionResult = (): void => {
// Comment this line to prevent clear OCR boundary boxes.
// this.imageMap.removeAllFeatures();
const features = [];
const imageExtent = [0, 0, this.state.imageWidth, this.state.imageHeight];
const ocrForCurrentPage: any = this.getOcrFromAnalyzeResult(this.state.analyzeResult)[this.state.currentPage - 1];
const ocrExtent = [0, 0, ocrForCurrentPage.width, ocrForCurrentPage.height];
const predictions = this.getPredictionsFromAnalyzeResult(this.state.analyzeResult);
for (const fieldName of Object.keys(predictions)) {
const field = predictions[fieldName];
const createFeature = (fieldName, field) => {
if (Array.isArray(field)) {
field.forEach(field => createFeature(fieldName, field))
} else {
if (_.get(field, "page", null) === this.state.currentPage) {
const text = fieldName;
const boundingbox = _.get(field, "boundingBox", []);
@ -824,13 +837,25 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
features.push(feature);
}
}
}
const features = [];
const imageExtent = [0, 0, this.state.imageWidth, this.state.imageHeight];
const ocrForCurrentPage: any = this.getOcrFromAnalyzeResult(this.state.analyzeResult)[this.state.currentPage - 1];
const ocrExtent = [0, 0, ocrForCurrentPage.width, ocrForCurrentPage.height];
const predictions = this.flatFields(this.getPredictionsFromAnalyzeResult(this.state.analyzeResult));
for (const [fieldName, field] of Object.entries(predictions)) {
createFeature(fieldName, field);
}
this.imageMap.addFeatures(features);
this.tableHelper.drawTables(this.state.currentPage);
}
private getPredictionsFromAnalyzeResult(analyzeResult: any) {
if (analyzeResult) {
const documentResults = _.get(analyzeResult, "documentResults", []);
private flatFields = (fields: object = {}): { [key: string]: (object[] | object) } => {
/**
* @param fields: primitive types, object types likes array, object, and root level field
* @return flattenfields, a field props or an array of field props
*/
const flattedFields = {};
const isSupportField = fieldName => {
// Define list of unsupported field names.
const blockedFieldNames = ["ReceiptType"];
@ -838,15 +863,13 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
}
const isRootItemObject = obj => obj.hasOwnProperty("text");
// flat fieldProps of type "array" and "object", and extract root level field props in "object" type
const flattedFields = {};
const flatFields = (fields = {}) => {
const flatFieldProps = (displayName, fieldProps) => {
if (isSupportField(displayName)) {
const flatFieldProps = (fieldName, fieldProps) => {
if (isSupportField(fieldName)) {
switch (_.get(fieldProps, "type", "")) {
case "array": {
const valueArray = _.get(fieldProps, "valueArray", []);
for (const [index, valueArrayItem] of valueArray.entries()) {
flatFieldProps(`${displayName} ${index + 1}`, valueArrayItem);
for (const arrayItem of valueArray) {
flatFieldProps(fieldName, arrayItem);
}
break;
}
@ -854,15 +877,24 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
// root level field props
const { type, valueObject, ...restProps } = fieldProps;
if (isRootItemObject(restProps)) {
flatFieldProps(displayName, restProps);
flatFieldProps(fieldName, restProps);
}
for (const objFieldProps of Object.values(fieldProps.valueObject)) {
if (objFieldProps) {
flatFieldProps(fieldName, objFieldProps);
}
for (const [fieldName, objFieldProps] of Object.entries(fieldProps.valueObject)) {
flatFieldProps(`${displayName}: ${fieldName}`, objFieldProps);
}
break;
}
default: {
flattedFields[displayName] = fieldProps;
if (flattedFields[fieldName] == null) {
flattedFields[fieldName] = fieldProps;
}
else if (Array.isArray(flattedFields[fieldName])) {
flattedFields[fieldName].push(fieldProps)
} else {
flattedFields[fieldName] = [flattedFields[fieldName], fieldProps];
}
}
}
}
@ -870,12 +902,13 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
for (const [fieldName, fieldProps] of Object.entries(fields)) {
flatFieldProps(fieldName, fieldProps);
}
}
for (const documentResult of documentResults) {
const fields = documentResult["fields"];
flatFields(fields);
}
return flattedFields;
}
private getPredictionsFromAnalyzeResult(analyzeResult: any) {
if (analyzeResult) {
const documentResults = _.get(analyzeResult, "documentResults", []);
return documentResults.reduce((accFields, documentResult) => ({ ...accFields, ...documentResult.fields }), {});
} else {
return {};
}
@ -997,4 +1030,45 @@ export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPagePro
predictionEndpointUrl: newValue
});
}
private onTablePredictionClick = (predictedItem: ITableResultItem, tagColor: string) => {
const makeTable = (clickedFieldName) => {
function Cell(rowIndex, columnIndex, text = null, confidence = null) {
this.rowIndex = rowIndex;
this.columnIndex = columnIndex;
this.text = text;
this.confidence = confidence;
}
const valueArray = clickedFieldName.valueArray || [];
const columnNames = Object.keys(valueArray[0].valueObject);
const columnHeaders = function makeColumnHeaders() {
const indexColumn = new Cell(0, 0, "");
const contentColumns = columnNames.map((columnName, columnIndex) => new Cell(0, columnIndex + 1, columnName));
return [indexColumn, ...contentColumns];
}()
const matrix: any[] = [columnHeaders];
for (let i = 0; i < valueArray.length; i++) {
const valueObject = valueArray[i].valueObject || {};
const indexColumn = new Cell(i + 1, 0, `#${i + 1}`);
const contentColumns = columnNames.map((columnName, columnIndex) => {
const { text, confidence } = valueObject[columnName] || {};
return new Cell(i + 1, columnIndex + 1, text, confidence);
});
matrix.push([indexColumn, ...contentColumns]);
}
const flattenCells = matrix.reduce((cells, row) => cells = [...cells, ...row], []);
return { cells: flattenCells, columns: matrix[0].length, rows: matrix.length, fieldName: clickedField, tagColor };
}
const predictions = this.getPredictionsFromAnalyzeResult(this.state.analyzeResult)
const clickedFieldName = predictedItem?.fieldName;
const clickedField = predictions[clickedFieldName];
const regionalTableToView = makeTable(clickedField);
this.setState({ viewRegionalTable: true, regionalTableToView });
}
private onTablePredictionClose = () => {
this.setState({ viewRegionalTable: false, regionalTableToView: null })
}
}

Просмотреть файл

@ -44,6 +44,7 @@ import "./predictPage.scss";
import PredictResult, { IAnalyzeModelInfo, ITableResultItem } from "./predictResult";
import RecentModelsView from "./recentModelsView";
import { UploadToTrainingSetView } from "./uploadToTrainingSetView";
import RegionalTable from "../../common/regionalTable/regionalTable";
pdfjsLib.GlobalWorkerOptions.workerSrc = constants.pdfjsWorkerSrc(pdfjsLib.version);
@ -441,7 +442,12 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
{this.state.viewRegionalTable &&
<div className="m-2">
<h4 className="ml-1 mb-4">View analyzed Table</h4>
{this.displayRegionalTable(this.state.regionalTableToView)}
<RegionalTable
regionalTableToView={this.state.regionalTableToView}
tableTagColor={this.state.tableTagColor}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
/>
<PrimaryButton
className="mt-4 ml-2"
theme={getPrimaryGreyTheme()}
@ -1072,158 +1078,6 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
this.setState({ viewRegionalTable: true, regionalTableToView: predictedItem, tableTagColor: tagColor });
}
private displayRegionalTable = (regionalTableToView) => {
const tableBody = [];
if (regionalTableToView?.type === "array") {
const columnHeaderRow = [];
const colKeys = Object.keys(regionalTableToView?.valueArray?.[0]?.valueObject || {});
if (colKeys.length === 0) {
return (
<div>
<h5 className="mb-4 ml-2 mt-2 pb-1">
<span style={{ borderBottom: `4px solid ${this.state.tableTagColor}` }}>Table name: {regionalTableToView.fieldName}</span>
</h5>
<div className="table-view-container">
<table>
<tbody>
Empty table
</tbody>
</table>
</div>
</div>
);
}
for (let i = 0; i < colKeys.length + 1; i++) {
if (i === 0) {
columnHeaderRow.push(
<th key={i} className={"empty_header hidden"} />
);
} else {
columnHeaderRow.push(
<th key={i} className={"column_header"}>
{colKeys[i - 1]}
</th>
);
}
}
tableBody.push(<tr key={0}>{columnHeaderRow}</tr>);
regionalTableToView?.valueArray?.forEach((row, rowIndex) => {
const tableRow = [];
tableRow.push(
<th key={0} className={"row_header hidden"}>
{"#" + rowIndex}
</th>
);
Object.keys(row?.valueObject).forEach((columnName, columnIndex) => {
const tableCell = row?.valueObject?.[columnName];
tableRow.push(
<td
className={"table-cell"}
key={columnIndex + 1}
onMouseEnter={() => {
this.setState({ highlightedTableCellRowKey: "#" + rowIndex, highlightedTableCellColumnKey: columnName })
}}
onMouseLeave={() => {
this.setState({ highlightedTableCellRowKey: null, highlightedTableCellColumnKey: null })
}}
>
{tableCell ? tableCell.text : null}
</td>
);
})
tableBody.push(<tr key={(rowIndex + 1)}>{tableRow}</tr>);
})
} else {
const columnHeaderRow = [];
const colKeys = Object.keys(regionalTableToView?.valueObject?.[Object.keys(regionalTableToView?.valueObject)?.[0]]?.valueObject || {});
if (colKeys.length === 0) {
return (
<div>
<h5 className="mb-4 ml-2 mt-2 pb-1">
<span style={{ borderBottom: `4px solid ${this.state.tableTagColor}` }}>Table name: {regionalTableToView.fieldName}</span>
</h5>
<div className="table-view-container">
<table>
<tbody>
Empty table
</tbody>
</table>
</div>
</div>
);
}
for (let i = 0; i < colKeys.length + 1; i++) {
if (i === 0) {
columnHeaderRow.push(
<th key={i} className={"empty_header hidden"} />
);
} else {
columnHeaderRow.push(
<th key={i} className={"column_header"}>
{colKeys[i - 1]}
</th>
);
}
}
tableBody.push(<tr key={0}>{columnHeaderRow}</tr>);
Object.keys(regionalTableToView?.valueObject).forEach((rowName, index) => {
const tableRow = [];
tableRow.push(
<th key={0} className={"row_header"}>
{rowName}
</th>
);
if (regionalTableToView?.valueObject?.[rowName]) {
Object.keys(regionalTableToView?.valueObject?.[rowName]?.valueObject)?.forEach((columnName, index) => {
const tableCell = regionalTableToView?.valueObject?.[rowName]?.valueObject?.[columnName];
tableRow.push(
<td
className={"table-cell"}
key={index + 1}
onMouseEnter={() => {
this.setState({ highlightedTableCellRowKey: rowName, highlightedTableCellColumnKey: columnName })
}}
onMouseLeave={() => {
this.setState({ highlightedTableCellRowKey: null, highlightedTableCellColumnKey: null })
}}
>
{tableCell ? tableCell.text : null}
</td>
);
});
} else {
colKeys.forEach((columnName, index) => {
tableRow.push(
<td
className={"table-cell"}
key={index + 1}
>
{null}
</td>
);
})
}
tableBody.push(<tr key={index + 1}>{tableRow}</tr>);
});
}
return (
<div>
<h5 className="mb-4 ml-2 mt-2 pb-1">
<span style={{ borderBottom: `4px solid ${this.state.tableTagColor}` }}>Table name: {regionalTableToView.fieldName}</span>
</h5>
<div className="table-view-container">
<table>
<tbody>
{tableBody}
</tbody>
</table>
</div>
</div>
);
}
private onPredictionMouseEnter = (predictedItem: any) => {
this.setState({
highlightedField: predictedItem.fieldName ?? "",
@ -1376,4 +1230,12 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
this.props.appTitleActions.setTitle(project.name);
}
}
private onMouseEnter = (rowName: string, columnName: string) => {
this.setState({ highlightedTableCellRowKey: rowName, highlightedTableCellColumnKey: columnName });
}
private onMouseLeave = () => {
this.setState({ highlightedTableCellRowKey: null, highlightedTableCellColumnKey: null });
}
}