Merge branch 'master' into alex-krasn/fix-onprem-network-error-message

This commit is contained in:
alex-krasn 2020-10-30 15:50:49 -07:00 коммит произвёл GitHub
Родитель 00cdfcb400 b0404c6276
Коммит 921541e7cb
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
37 изменённых файлов: 561 добавлений и 207 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -38,3 +38,4 @@ secrets.sh
es6-src/
report/
debug.log
src/git-commit-info.txt

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

@ -173,6 +173,15 @@ Click on the Analyze icon on the left pane to open the Analyze page. Upload a fo
Tip: You can also run the Analyze API with a REST call. To learn how to do this, see [Train with labels using Python](https://docs.microsoft.com/en-us/azure/cognitive-services/form-recognizer/quickstarts/python-labeled-data).
#### Compose a model ####
Click the Compose icon on the left pane to open the Compose page. FoTT will display the first page of your models—by decending order of Model ID—in a list. Select multiple models you want to compose into one model and click the **Compose** button. Once the new model has been composed, it's ready to analyze with.
![alt text](docs/images/compose.png "Compose")
To load more of your models, click the **Load next page** button at the bottom of the list. This will load the next page of your models by decending order of model ID.
You can sort the currently loaded models by clicking the column headers at the top of the list. Only the currently loaded models will be sorted. You will need to load all pages of your models first and then sort to view the complete sorted list of your models.
#### Save a project and resume later ####
To resume your project at another time or in another browser, you need to save your project's security token and reenter it later.

Двоичные данные
docs/images/compose.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 354 KiB

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

@ -43,7 +43,7 @@
"redux": "^4.0.4",
"redux-thunk": "^2.3.0",
"rimraf": "^3.0.2",
"serialize-javascript": "^3.0.0",
"serialize-javascript": "^5.0.1",
"shortid": "^2.2.15",
"utif": "^3.1.0",
"vott-react": "^0.2.12",
@ -52,8 +52,8 @@
"scripts": {
"start": "env-cmd -f .env.electron nf start -p 3000",
"compile": "tsc",
"build": "react-scripts build",
"react-start": "react-scripts start",
"build": "node ./scripts/dump_git_info.js && react-scripts build",
"react-start": "node ./scripts/dump_git_info.js && react-scripts start",
"test": "react-scripts test --env=jsdom --silent",
"eject": "react-scripts eject",
"webpack:dev": "webpack --config ./config/webpack.dev.js",

21
scripts/dump_git_info.js Normal file
Просмотреть файл

@ -0,0 +1,21 @@
spawn = require('child_process').spawn,
fs = require('fs');
git = spawn('git', ['log', '-1']),
buf = Buffer.alloc(0);
git.stdout.on('data', (data) => {
buf = Buffer.concat([buf, data])
});
git.stderr.on('data', (data) => {
console.log(data.toString());
});
git.on('close', (code) => {
fs.writeFile("src/git-commit-info.txt", buf.toString(), (err, data) => {
if (err) {
console.log(err);
}
});
});

4
scripts/dump_git_info.sh Normal file
Просмотреть файл

@ -0,0 +1,4 @@
#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
git status && git log -1 > $DIR/../src/git-commit-info.txt || echo 'Not a Git repo. Continue...'

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

@ -148,6 +148,9 @@
.ms-Icon-18px {
font-size: 18px;
}
.ms-Icon-25px{
font-size: 25px;
}
.ms-Spinner-label {
color: inherit;

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -37,7 +37,9 @@ export const constants = {
convertedThumbnailQuality: 0.2,
recentModelRecordsCount: 5,
apiModelsPath: `/formrecognizer/${apiVersion}/custom/models`,
autoLabelBatchSize: 10,
autoLabelBatchSizeMax: 10,
autoLabelBatchSizeMin: 3,
showOriginLabelsByDefault: true,
pdfjsWorkerSrc(version: string) {
return `https://fotts.azureedge.net/npm/pdfjs-dist/${version}/pdf.worker.js`;

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

@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { IAppStrings } from "../strings";
import {IAppStrings} from "../strings";
/*eslint-disable no-template-curly-in-string, no-multi-str*/
@ -41,8 +41,8 @@ export const english: IAppStrings = {
},
openCloudProject: {
title: "Open Cloud Project",
selectConnection: "Select a Connection",
pasteSharedUri: "Please paste shared project string here",
selectConnection: "Open cloud project",
pasteSharedUri: "Paste shared project token here",
},
recentProjects: "Recent Projects",
deleteProject: {
@ -131,7 +131,7 @@ export const english: IAppStrings = {
downloadJson: "Download JSON file",
trainConfirm: {
title: "Labels not revised yet",
message: "You have label files not yet revised, do you want to train with those files?"
message: "There are newly auto-labeled files not yet revised by you, do you want to train with those files?"
},
errors: {
electron: {
@ -272,6 +272,10 @@ export const english: IAppStrings = {
},
toolbar: {
add: "Add new tag",
onlyShowCurrentPageTags: "Only show tags used in current page",
showAllTags: "Show all tags",
showOriginLabels:"Show origin labels",
hideOriginLabels:"Hide origin labels",
contextualMenu: "Contextual Menu",
delete: "Delete tag",
edit: "Edit tag",
@ -449,10 +453,10 @@ export const english: IAppStrings = {
additionalActions: {
text: "Additional actions",
subIMenuItems: {
runOcrOnCurrentDocument: "Run OCR on current document",
runOcrOnAllDocuments: "Run OCR on all documents",
runOcrOnCurrentDocument: "Run Layout on current document",
runOcrOnAllDocuments: "Run Layout on all documents",
runAutoLabelingCurrentDocument: "Auto-label the current document",
runAutoLabelingOnNotLabelingDocuments: "Auto-label next ${batchSize} unlabeled documents",
runAutoLabelingOnMultipleUnlabeledDocuments: "Auto-label multiple unlabeled documents",
noPredictModelOnProject: "Predict model not avaliable, please train the model first.",
}
}
@ -529,7 +533,7 @@ export const english: IAppStrings = {
},
tips: {
quickLabeling: {
name: "Lable with hot keys",
name: "Label with hot keys",
description: "Hotkeys 1 through 0 and all letters are assigned to first 36 tags. After selecting one or multiple words, press tag's assigned hotkey.",
},
renameTag: {
@ -700,13 +704,13 @@ export const english: IAppStrings = {
shareProject: {
name: "Share Project",
errors: {
cannotDecodeString: "Cannot decode shared string! Please, check if your string has been modified.",
cannotDecodeString: "Cannot decode shared token. Check if shared token has been modified.",
connectionNotFound: "Connection not found. Add shared project's connection to your connections.",
noConnections: "Connection is required for project sharing",
connectionRequirement: "Shared project's connection must be added before opening it",
tokenNameExist: "Warning! You already have token with same name as in shared project. Please create a new token, and update the existing project which uses ''${sharedTokenName}'' with new token name."
},
copy: {
success: "String for sharing your project has been saved to clipboard. In order to use it, paste it in appropriate section of the 'Open Cloud Project' popup.",
success: "Project token copied to clipboard and ready to share. Reciever of project token can click 'Open Cloud Project' from the Home page to use shared token.",
}
},
};

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

@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { IAppStrings } from "../strings";
import {IAppStrings} from "../strings";
/*eslint-disable no-template-curly-in-string, no-multi-str*/
@ -42,7 +42,7 @@ export const spanish: IAppStrings = {
},
openCloudProject: {
title: "Abrir Proyecto de la Nube",
selectConnection: "Select a Connection",
selectConnection: "Proyecto de nube abierta",
pasteSharedUri: "Pegue la cadena de proyecto compartida aquí",
},
deleteProject: {
@ -132,7 +132,7 @@ export const spanish: IAppStrings = {
downloadJson: "Descargar archivo JSON",
trainConfirm: {
title: "Etiquetas no revisadas todavía",
message: "Tiene archivos de etiquetas que aún no han sido revisados, ¿desea entrenar con esos archivos?"
message: "Hay archivos recientemente etiquetados automáticamente que aún no ha revisado, ¿desea entrenar con esos archivos?"
},
errors: {
electron: {
@ -271,6 +271,10 @@ export const spanish: IAppStrings = {
},
toolbar: {
add: "Agregar nueva etiqueta",
onlyShowCurrentPageTags: "Mostrar solo las etiquetas utilizadas en la página actual",
showAllTags: "Mostrar todas las etiquetas",
showOriginLabels: "Mostrar etiquetas de origen",
hideOriginLabels: "Ocultar etiquetas de origen",
contextualMenu: "Menú contextual",
delete: "Borrar etiqueta",
edit: "Editar etiqueta",
@ -450,10 +454,10 @@ export const spanish: IAppStrings = {
additionalActions: {
text: "Acciones adicionales",
subIMenuItems: {
runOcrOnCurrentDocument: "Ejecutar OCR en el documento actual",
runOcrOnAllDocuments: "Ejecute OCR en todos los documentos",
runOcrOnCurrentDocument: "Ejecutar Layout en el documento actual",
runOcrOnAllDocuments: "Ejecute Layout en todos los documentos",
runAutoLabelingCurrentDocument: "Etiquetar automáticamente el documento actual",
runAutoLabelingOnNotLabelingDocuments: "Etiquetar automáticamente los siguientes ${batchSize} documentos sin etiquetar",
runAutoLabelingOnMultipleUnlabeledDocuments: "Etiquetar automáticamente varios documentos sin etiquetar",
noPredictModelOnProject: "Predecir modelo no disponible, entrene el modelo primero.",
}
}
@ -701,13 +705,13 @@ export const spanish: IAppStrings = {
shareProject: {
name: "Compartir proyecto",
errors: {
cannotDecodeString: "¡No se puede decodificar la cadena compartida! Por favor, verifique si su cadena ha sido modificada.",
cannotDecodeString: "No se puede decodificar el token compartido. Compruebe si se ha modificado el token compartido.",
connectionNotFound: "Conexión no encontrada. Agregue la conexión del proyecto compartido a sus conexiones.",
noConnections: "Se requiere conexión para compartir proyectos",
connectionRequirement: "La conexión del proyecto compartido debe agregarse antes de abrirlo",
tokenNameExist: "¡Advertencia! Ya tiene token con el mismo nombre que en el proyecto compartido. Cree un nuevo token y actualice el proyecto existente que usa ''${sharedTokenName}'' con el nuevo nombre del token.",
},
copy: {
success: "La cadena para compartir su proyecto se ha guardado en el portapapeles. Para usarlo, péguelo en la sección correspondiente de la ventana emergente 'Open Cloud Project'.",
success: "Token de proyecto copiado al portapapeles y listo para compartir. El receptor del token del proyecto puede hacer clic en 'Abrir proyecto en la nube' desde la página de inicio para usar el token compartido.",
}
}
};

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

@ -246,6 +246,10 @@ export interface IAppStrings {
}
toolbar: {
add: string,
onlyShowCurrentPageTags:string,
showAllTags:string,
showOriginLabels: string
hideOriginLabels: string,
contextualMenu: string,
delete: string,
edit: string,
@ -447,7 +451,7 @@ export interface IAppStrings {
runOcrOnCurrentDocument: string,
runOcrOnAllDocuments: string,
runAutoLabelingCurrentDocument: string,
runAutoLabelingOnNotLabelingDocuments: string,
runAutoLabelingOnMultipleUnlabeledDocuments: string,
noPredictModelOnProject: string,
}
}
@ -589,7 +593,7 @@ export interface IAppStrings {
errors: {
cannotDecodeString: string,
connectionNotFound: string,
noConnections: string,
connectionRequirement: string,
tokenNameExist: string,
},
copy: {

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

@ -74,6 +74,10 @@
"name": "CircleRing",
"unicode": "EA3A"
},
{
"name": "ClearFilter",
"unicode": "EF8F"
},
{
"name": "Cloud",
"unicode": "E753"
@ -110,6 +114,14 @@
"name": "Filter",
"unicode": "E71C"
},
{
"name": "GroupedList",
"unicode": "EF74"
},
{
"name": "GroupList",
"unicode": "F168"
},
{
"name": "Help",
"unicode": "E897"

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

@ -6,7 +6,7 @@ import path from "path";
import rimraf from "rimraf";
import { BrowserWindow, dialog } from "electron";
import { IStorageProvider } from "../../../providers/storage/storageProviderFactory";
import { IAsset, AssetState, AssetType, StorageType } from "../../../models/applicationState";
import { IAsset, AssetState, AssetType, StorageType, ILabelData, AssetLabelingState } from "../../../models/applicationState";
import { AssetService } from "../../../services/assetService";
import { constants } from "../../../common/constants";
import { strings } from "../../../common/strings";
@ -180,6 +180,11 @@ export default class LocalFileSystem implements IStorageProvider {
const ocrFileName = decodeURIComponent(`${file}${constants.ocrFileExtension}`);
if (files.find((str) => str === labelFileName)) {
asset.state = AssetState.Tagged;
const json = await this.readText(labelFileName);
const labelData = JSON.parse(json) as ILabelData;
if (labelData) {
asset.labelingState = labelData.labelingState || AssetLabelingState.ManuallyLabeled;
}
} else if (files.find((str) => str === ocrFileName)) {
asset.state = AssetState.Visited;
} else {

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

@ -232,8 +232,10 @@ export interface ILabel {
label: string,
key?: IFormRegion[],
value: IFormRegion[],
originValue?: IFormRegion[],
labelType?: string,
confidence?: number,
revised?: boolean;
}
/**

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

@ -110,7 +110,7 @@ export class AssetPreview extends React.Component<IAssetPreviewProps, IAssetPrev
<div className="asset-loading">
<div className="asset-loading-ocr-spinner">
<Label className="p-0" ></Label>
<Spinner size={SpinnerSize.small} label="Running OCR..." ariaLive="off" labelPosition="right" />
<Spinner size={SpinnerSize.small} label="Running Layout..." ariaLive="off" labelPosition="right" />
</div>
</div>
}

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

@ -1,11 +1,15 @@
.shared-string-input-container {
padding: 1rem;
.input-uri {
padding: 1rem;
padding: 1rem 1rem 0 1rem;
.form-control{
font-size: 90%;
}
}
.error-message {
color: #db7272;
padding: 4px 1rem 1rem 1rem;
}
}
.separator {

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

@ -3,13 +3,14 @@
import React from "react";
import { toast } from "react-toastify";
import { Button, Modal, ModalBody, ModalFooter, ModalHeader, InputGroup, Input } from "reactstrap";
import { Modal, ModalBody, ModalFooter, ModalHeader, InputGroup, Input } from "reactstrap";
import { strings, interpolate } from "../../../../common/strings";
import { IConnection, StorageType, ErrorCode, AppError, ISecurityToken } from "../../../../models/applicationState";
import { StorageProviderFactory } from "../../../../providers/storage/storageProviderFactory";
import CondensedList, { ListItem } from "../condensedList/condensedList";
import "./cloudFilePicker.scss"
import { Separator } from "@fluentui/react";
import { PrimaryButton, Separator } from "@fluentui/react";
import { getPrimaryGreenTheme, getPrimaryGreyTheme } from "../../../../common/themes";
/**
* Properties for Cloud File Picker
@ -52,7 +53,6 @@ export interface ICloudFilePickerState {
pastedUri: string;
pasting: boolean;
sharedStringData: ISharedStringData;
haveCloudConnections: boolean;
}
/**
@ -82,7 +82,6 @@ export class CloudFilePicker extends React.Component<ICloudFilePickerProps, IClo
public render() {
const closeBtn = <button className="close" onClick={this.close}>&times;</button>;
return (
<Modal isOpen={this.state.isOpen} centered={true}>
<ModalHeader toggle={this.close} close={closeBtn}>
@ -91,10 +90,10 @@ export class CloudFilePicker extends React.Component<ICloudFilePickerProps, IClo
{!this.state.selectedConnection &&
<>
<div className={"shared-string-input-container"}>
<div className="condensed-list-header bg-darker-2 shared-uri-header">Shared Project String</div>
{!this.state.haveCloudConnections &&
<div className="p-3 text-center">{strings.shareProject.errors.noConnections}</div>
}
<div className="condensed-list-header bg-darker-2 shared-uri-header">Shared project token</div>
<div className="input-uri">
{strings.shareProject.errors.connectionRequirement}
</div>
<InputGroup className="input-uri">
<Input placeholder={strings.homePage.openCloudProject.pasteSharedUri}
id="sharedURI"
@ -102,7 +101,6 @@ export class CloudFilePicker extends React.Component<ICloudFilePickerProps, IClo
value={this.state.pastedUri}
onChange={this.handleChangeUri}
onPaste={this.handlePasteUri}
disabled={!this.state.haveCloudConnections}
/>
</InputGroup>
</div>
@ -115,13 +113,28 @@ export class CloudFilePicker extends React.Component<ICloudFilePickerProps, IClo
}
<ModalFooter>
{this.state.selectedFile || ""}
<Button
className="btn btn-success mr-1"
onClick={this.ok}
disabled={this.state.okDisabled}>Ok</Button>
{!this.state.okDisabled &&
<PrimaryButton
theme={getPrimaryGreenTheme()}
className="mr-1 ml-2"
onClick={this.ok}
>
Open
</PrimaryButton>
}
{this.state.backDisabled && !this.state.pastedUri ?
<Button onClick={this.close}>Close</Button> :
<Button onClick={this.back}>Go Back</Button>
<PrimaryButton
onClick={this.close}
theme={getPrimaryGreyTheme()}
>
Cancel
</PrimaryButton> :
<PrimaryButton
onClick={this.back}
theme={getPrimaryGreyTheme()}
>
Go Back
</PrimaryButton>
}
</ModalFooter>
</Modal>
@ -161,7 +174,6 @@ export class CloudFilePicker extends React.Component<ICloudFilePickerProps, IClo
pastedUri: "",
pasting: false,
sharedStringData: null,
haveCloudConnections: cloudConnectionList.props.items.length > 0,
};
}
@ -192,7 +204,7 @@ export class CloudFilePicker extends React.Component<ICloudFilePickerProps, IClo
}
private getSharedConnection(connections: IConnection[], sasFolder: string) {
const connection: IConnection = connections.find(({ providerOptions }) => providerOptions["sas"].includes(sasFolder));
const connection: IConnection = connections?.find(({ providerOptions }) => providerOptions["sas"]?.includes(sasFolder));
if (connection) {
return connection;
}
@ -263,7 +275,7 @@ export class CloudFilePicker extends React.Component<ICloudFilePickerProps, IClo
const fileList = await this.fileList(connection);
this.setState({
selectedConnection: connection,
modalHeader: `Select a file from "${connection.name}"`,
modalHeader: `Select a project from "${connection.name}"`,
condensedList: fileList,
backDisabled: false,
});

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

@ -33,8 +33,7 @@
display: flex;
flex-direction: column;
position: relative;
&-container {
&-container{
overflow-x: visible;
overflow-y: auto;
padding: 0 0 0 100px;
@ -43,12 +42,13 @@
content: " ";
display: inline-block;
position: absolute;
width: 80px;
height: 100%;
left: -80px;
background: linear-gradient(to right, #00000000 0%,#000000 100%);
top: 44px;
width: 50px;
height: calc(100% - 44px);
left: -55px;
margin-right: 4px;
background: linear-gradient(to right, #00000000 10%,#00000080 80%);
}
};
}
@ -93,14 +93,20 @@
&-2 {
width: 100%;
}
.tag-item-confidence{
position: absolute;
line-height: 2em;
left: -70PX;
z-index: 900;
text-align: right;
width:50px;
text-shadow: 1px 1px 1px #333;
.tag-item-label-container {
display: flex;
flex-direction: row;
border: 1px solid $lighter-5;
&-item1 {
justify-content: center;
align-self: center;
width: 50px;
text-align: center;
}
&-item2 {
border-left: 1px solid $lighter-5;
flex: 1;
}
}
}
@ -128,14 +134,13 @@
&-highlight {
.tag-content {
background-color: $lighter-5 !important;
box-shadow:4px 4px 5px $lighter-5;
box-shadow: 4px 4px 5px $lighter-5;
}
}
&-label {
min-height: 1em;
display: flex;
flex-direction: row;
margin-bottom: 5px;
padding-left: 3px;
font-size: 0.95em;
}
@ -203,12 +208,17 @@
&-item-label {
color: #a0a0a0;
}
&-item-label:hover {
color: #fff;
background: $lighter-4;
cursor: pointer;
&:hover {
color: #fff;
background: $lighter-4;
cursor: pointer;
}
&-origin {
color: #a0a0a0;
background-color: $darker-4;
border-bottom: 1px solid $lighter-5;
width: 100%;
}
}
&-color {

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

@ -19,6 +19,7 @@ describe("Tag Input Component", () => {
function createProps(tags?: ITag[], onChange?): ITagInputProps {
return {
tagsLoaded: true,
pageNumber: 1,
tags: tags || MockFactory.createTestTags(),
lockedTags: [],
selectedRegions: [MockFactory.createTestRegion()],

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

@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import React, { KeyboardEvent } from "react";
import {
ContextualMenu,
ContextualMenuItemType,
@ -10,20 +9,22 @@ import {
IContextualMenuItem,
ICustomizations,
Spinner,
SpinnerSize,
SpinnerSize
} from "@fluentui/react";
import { strings, interpolate } from "../../../../common/strings";
import { getDarkTheme } from "../../../../common/themes";
import { AlignPortal } from "../align/alignPortal";
import { getNextColor } from "../../../../common/utils";
import { IRegion, ITag, ILabel, FieldType, FieldFormat, FeatureCategory } from "../../../../models/applicationState";
import { ColorPicker } from "../colorPicker";
import "./tagInput.scss";
import "../condensedList/condensedList.scss";
import TagInputItem, { ITagInputItemProps, ITagClickProps } from "./tagInputItem";
import TagInputToolbar from "./tagInputToolbar";
import { toast } from "react-toastify";
import debounce from 'lodash/debounce';
import React, {KeyboardEvent} from "react";
import {toast} from "react-toastify";
import {constants} from "../../../../common/constants";
import {interpolate, strings} from "../../../../common/strings";
import {getDarkTheme} from "../../../../common/themes";
import {getNextColor} from "../../../../common/utils";
import {FeatureCategory, FieldFormat, FieldType, ILabel, IRegion, ITag} from "../../../../models/applicationState";
import {AlignPortal} from "../align/alignPortal";
import {ColorPicker} from "../colorPicker";
import "../condensedList/condensedList.scss";
import "./tagInput.scss";
import TagInputItem, {ITagClickProps, ITagInputItemProps} from "./tagInputItem";
import TagInputToolbar from "./tagInputToolbar";
// tslint:disable-next-line:no-var-requires
const tagColors = require("../../common/tagColors.json");
@ -50,6 +51,8 @@ export interface ITagInputProps {
selectedRegions?: IRegion[];
/** The labels in the canvas */
labels: ILabel[];
/** The doc current page number */
pageNumber: number;
/** Tags that are currently locked for editing experience */
lockedTags?: string[];
/** Updates to locked tags */
@ -83,6 +86,8 @@ export interface ITagInputState {
tags: ITag[];
tagOperation: TagOperationMode;
addTags: boolean;
onlyCurrentPageTags: boolean;
showOriginLabels: boolean;
searchTags: boolean;
searchQuery: string;
selectedTag: ITag;
@ -109,7 +114,7 @@ function filterFormat(type: FieldType): FieldFormat[] {
FieldFormat.YMD,
];
default:
return [FieldFormat.NotSpecified];
return [ FieldFormat.NotSpecified ];
}
}
@ -131,6 +136,8 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
searchTags: this.props.showSearchBox,
searchQuery: "",
selectedTag: null,
onlyCurrentPageTags: false,
showOriginLabels: constants.showOriginLabelsByDefault,
};
private tagItemRefs: Map<string, TagInputItem> = new Map<string, TagInputItem>();
@ -156,7 +163,6 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
});
}
}
public render() {
const dark: ICustomizations = {
settings: {
@ -165,7 +171,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
scopedSettings: {},
};
const { selectedTag, tagOperation } = this.state;
const {selectedTag, tagOperation} = this.state;
const selectedTagRef = selectedTag ? this.tagItemRefs.get(selectedTag.name)?.getTagNameRef() : null;
return (
@ -174,7 +180,9 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
<span className="tag-input-title">{strings.tags.title}</span>
<TagInputToolbar
selectedTag={this.state.selectedTag}
onAddTags={() => this.setState({ addTags: !this.state.addTags })}
onAddTags={() => this.setState({addTags: !this.state.addTags})}
onOnlyCurrentPageTags={() => this.setState({onlyCurrentPageTags: !this.state.onlyCurrentPageTags})}
onShowOriginLabels = {(showOriginLabels: boolean) => this.setState({showOriginLabels})}
onSearchTags={() => this.setState({
searchTags: !this.state.searchTags,
searchQuery: "",
@ -186,7 +194,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
onReorder={this.onReOrder}
/>
</div>
{ this.props.tagsLoaded ?
{this.props.tagsLoaded ?
<div className="tag-input-body-container">
<div className="tag-input-body">
{
@ -196,10 +204,10 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
className="tag-search-box"
type="text"
onKeyDown={this.onSearchKeyDown}
onChange={(e) => this.setState({ searchQuery: e.target.value })}
onChange={(e) => this.setState({searchQuery: e.target.value})}
placeholder="Search tags"
autoFocus={true}
onFocus={() => this.setState({ selectedTag: null, tagOperation: TagOperationMode.Rename })}
onFocus={() => this.setState({selectedTag: null, tagOperation: TagOperationMode.Rename})}
/>
<FontIcon iconName="Search" />
</div>
@ -243,13 +251,11 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
</div>
);
}
public triggerNewTagBlur() {
if (this.inputRef.current) {
this.inputRef.current.blur();
}
}
private onRenameTag = (tag: ITag) => {
const tagOperation = this.state.tagOperation === TagOperationMode.Rename
? TagOperationMode.None : TagOperationMode.Rename;
@ -262,7 +268,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
if (!tag) {
return;
}
let lockedTags = [...this.props.lockedTags];
let lockedTags = [ ...this.props.lockedTags ];
if (lockedTags.find((str) => isNameEqual(tag.name, str))) {
lockedTags = lockedTags.filter((str) => !isNameEqual(tag.name, str));
} else {
@ -279,7 +285,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
if (!tag) {
return;
}
const tags = [...this.state.tags];
const tags = [ ...this.state.tags ];
const currentIndex = tags.indexOf(tag);
const newIndex = currentIndex + displacement;
if (newIndex < 0 || newIndex >= tags.length) {
@ -315,7 +321,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
return;
}
const tags = [...this.state.tags, tag];
const tags = [ ...this.state.tags, tag ];
this.setState({
tags,
}, () => this.props.onChange(tags));
@ -362,10 +368,10 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
}
private getColorPickerPortal = () => {
const { selectedTag } = this.state;
const {selectedTag} = this.state;
const showColorPicker = this.state.tagOperation === TagOperationMode.ColorPicker;
return (
<AlignPortal align={{ points: ["tr", "tl"] }} target={() => this.headerRef.current}>
<AlignPortal align={{points: [ "tr", "tl" ]}} target={() => this.headerRef.current}>
<div className="tag-input-colorpicker-container">
{
showColorPicker &&
@ -395,6 +401,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
{...prop}
key={prop.tag.name}
labels={this.setTagLabels(prop.tag.name)}
showOriginLabels={this.state.showOriginLabels}
ref={(item) => this.setTagItemRef(item, prop.tag)}
onLabelEnter={this.props.onLabelEnter}
onLabelLeave={this.props.onLabelLeave}
@ -413,9 +420,34 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
}
private createTagItemProps = (): ITagInputItemProps[] => {
const { tags, selectedTag, tagOperation } = this.state;
const {tags, selectedTag, tagOperation, onlyCurrentPageTags} = this.state;
const selectedRegionTagSet = this.getSelectedRegionTagSet();
if (onlyCurrentPageTags) {
const labels = this.props.labels.filter(item => item.value[ 0 ].page === this.props.pageNumber)
.map(item => item.label);
if (labels.length) {
return tags.filter(tag => labels.find(a => a === tag.name))
.map<ITagInputItemProps>(tag => {
return {
tag,
index: tags.findIndex((t) => isNameEqual(t.name, tag.name)),
isLocked: this.props.lockedTags
&& this.props.lockedTags.findIndex((str) => isNameEqual(tag.name, str)) > -1,
isRenaming: selectedTag && isNameEqual(selectedTag.name, tag.name)
&& tagOperation === TagOperationMode.Rename,
isSelected: selectedTag && isNameEqual(selectedTag.name, tag.name),
appliedToSelectedRegions: selectedRegionTagSet.has(tag.name),
onClick: this.onTagItemClick,
onRename: this.onTagRename,
} as ITagInputItemProps;
});
}
return [];
}
return tags.map((tag) => (
{
tag,
@ -453,7 +485,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
tagOperation: TagOperationMode.Rename,
});
} else if (props.clickedDropDown) {
const { selectedTag } = this.state;
const {selectedTag} = this.state;
const showContextualMenu = !selectedTag || !isNameEqual(selectedTag.name, tag.name)
|| this.state.tagOperation !== TagOperationMode.ContextualMenu;
const tagOperation = showContextualMenu ? TagOperationMode.ContextualMenu : TagOperationMode.None;
@ -462,7 +494,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
tagOperation,
});
} else if (props.clickedColor) {
const { selectedTag, tagOperation } = this.state;
const {selectedTag, tagOperation} = this.state;
const showColorPicker = tagOperation !== TagOperationMode.ColorPicker;
const newTagOperation = showColorPicker ? TagOperationMode.ColorPicker : TagOperationMode.None;
this.setState({
@ -470,27 +502,27 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
tagOperation: newTagOperation,
});
} else { // Select tag
const { selectedTag, tagOperation: oldTagOperation } = this.state;
const {selectedTag, tagOperation: oldTagOperation} = this.state;
const selected = selectedTag && isNameEqual(selectedTag.name, tag.name);
const tagOperation = selected ? oldTagOperation : TagOperationMode.None;
let deselect = selected && oldTagOperation === TagOperationMode.None;
// Only fire click event if a region is selected
const { selectedRegions, onTagClick, labels } = this.props;
const {selectedRegions, onTagClick, labels} = this.props;
if (selectedRegions && selectedRegions.length && onTagClick) {
const { category } = selectedRegions[0];
const { format, type, documentCount, name } = tag;
const {category} = selectedRegions[ 0 ];
const {format, type, documentCount, name} = tag;
const tagCategory = this.getTagCategory(type);
const isTagLabelTypeDrawnRegion = this.labelAssignedDrawnRegion(labels, tag.name);
const labelAssigned = this.labelAssigned(labels, name);
if (labelAssigned && ((category === FeatureCategory.DrawnRegion) !== isTagLabelTypeDrawnRegion)) {
if (isTagLabelTypeDrawnRegion) {
toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, { otherCatagory: category }));
toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, {otherCatagory: category}));
} else if (tagCategory === FeatureCategory.Checkbox) {
toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, { otherCatagory: FeatureCategory.Checkbox }));
toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, {otherCatagory: FeatureCategory.Checkbox}));
} else {
toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, { otherCatagory: FeatureCategory.Text }));
toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, {otherCatagory: FeatureCategory.Text}));
}
return;
} else if (tagCategory === category || category === FeatureCategory.DrawnRegion ||
@ -502,7 +534,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
onTagClick(tag);
deselect = false;
} else {
toast.warn(strings.tags.warnings.notCompatibleTagType, { autoClose: 7000 });
toast.warn(strings.tags.warnings.notCompatibleTagType, {autoClose: 7000});
}
}
this.setState({
@ -586,7 +618,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
format: FieldFormat.NotSpecified,
documentCount: 0,
};
if (newTag.name.length && ![...this.state.tags, newTag].containsDuplicates((t) => t.name)) {
if (newTag.name.length && ![ ...this.state.tags, newTag ].containsDuplicates((t) => t.name)) {
this.addTag(newTag);
} else if (!newTag.name.length) {
toast.warn(strings.tags.warnings.emptyName);
@ -611,7 +643,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
}
private onHideContextualMenu = () => {
this.setState({ tagOperation: TagOperationMode.None });
this.setState({tagOperation: TagOperationMode.None});
}
private getContextualMenuItems = (): IContextualMenuItem[] => {

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

@ -21,6 +21,7 @@ describe("Tag Input Item", () => {
onRename: jest.fn(),
onLabelEnter: jest.fn(),
onLabelLeave: jest.fn(),
showOriginLabels:false,
};
}

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

@ -1,13 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import React, { MouseEvent } from "react";
import { FontIcon, IconButton } from "@fluentui/react";
import { ITag, ILabel, FieldType, FieldFormat } from "../../../../models/applicationState";
import { strings } from "../../../../common/strings";
import TagInputItemLabel from "./tagInputItemLabel";
import { tagIndexKeys } from "./tagIndexKeys";
import {FontIcon, IconButton} from "@fluentui/react";
import _ from "lodash";
import React, {Fragment, MouseEvent} from "react";
import {strings} from "../../../../common/strings";
import {FieldFormat, FieldType, ILabel, ITag} from "../../../../models/applicationState";
import {tagIndexKeys} from "./tagIndexKeys";
import TagInputItemLabel from "./tagInputItemLabel";
export interface ITagClickProps {
ctrlKey?: boolean;
@ -27,6 +27,8 @@ export interface ITagInputItemProps {
index: number;
/** Labels owned by the tag */
labels: ILabel[];
/** show Origin Labels or not */
showOriginLabels: boolean;
/** Tag is currently renaming */
isRenaming: boolean;
/** Tag is currently locked for application */
@ -42,7 +44,7 @@ export interface ITagInputItemProps {
onLabelEnter: (label: ILabel) => void;
onLabelLeave: (label: ILabel) => void;
onTagChanged?: (oldTag: ITag, newTag: ITag) => void;
onTagDoubleClick?: (label:ILabel) => void;
onTagDoubleClick?: (label: ILabel) => void;
}
export interface ITagInputItemState {
@ -81,14 +83,8 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
const style: any = {
background: this.props.tag.color,
};
const confidence = _.get(this.props, "labels[0].confidence", null);
return (
<div className={"tag-item-block"}>
{confidence &&
<div className="tag-item-confidence">
{confidence}
</div>
}
<div
className={"tag-color"}
style={style}
@ -127,7 +123,7 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
e.stopPropagation();
const clickedDropDown = true;
this.props.onClick(this.props.tag, { clickedDropDown });
this.props.onClick(this.props.tag, {clickedDropDown});
}
private onColorClick = (e: MouseEvent) => {
@ -136,7 +132,7 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
const ctrlKey = e.ctrlKey || e.metaKey;
const altKey = e.altKey;
const clickedColor = true;
this.props.onClick(this.props.tag, { ctrlKey, altKey, clickedColor });
this.props.onClick(this.props.tag, {ctrlKey, altKey, clickedColor});
}
private onNameClick = (e: MouseEvent) => {
@ -144,12 +140,12 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
const ctrlKey = e.ctrlKey || e.metaKey;
const altKey = e.altKey;
this.props.onClick(this.props.tag, { ctrlKey, altKey });
this.props.onClick(this.props.tag, {ctrlKey, altKey});
}
private onNameDoubleClick = (e:MouseEvent) => {
private onNameDoubleClick = (e: MouseEvent) => {
e.stopPropagation();
const { labels } = this.props;
const {labels} = this.props;
if (labels.length > 0) {
this.props.onTagDoubleClick(labels[0]);
}
@ -206,7 +202,7 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
title={strings.tags.toolbar.contextualMenu}
ariaLabel={strings.tags.toolbar.contextualMenu}
className="tag-input-toolbar-iconbutton ml-2"
iconProps={{ iconName: "ChevronDown" }}
iconProps={{iconName: "ChevronDown"}}
onClick={this.onDropdownClick} />
</div>
</div>
@ -214,15 +210,47 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
}
private renderTagDetail = () => {
let confidence = _.get(this.props, "labels[0].confidence", null);
if (confidence > .995) {
confidence = 0.995;
}
const revised = _.get(this.props, "labels[0].revised", false);
return this.props.labels.map((label, idx) =>
<TagInputItemLabel
key={idx}
label={label}
onLabelEnter={this.props.onLabelEnter}
onLabelLeave={this.props.onLabelLeave}
/>);
<Fragment key={idx}>
<div className="tag-item-label-container">
{(confidence||revised)&&
<div className="tag-item-label-container-item1">
{confidence &&
<div className="tag-item-confidence">
{confidence}
</div>
}
{revised &&
<FontIcon iconName="StatusCircleCheckmark" className="ms-Icon-25px" />
}
</div>
}
<div className="tag-item-label-container-item2">
{ this.props.showOriginLabels && label.originValue &&
<TagInputItemLabel
label={label}
isOrigin={true}
value={label.originValue}
prefixText="Auto-labeled: "
/>
}
<TagInputItemLabel
label={label}
value={label.value}
isOrigin={false}
onLabelEnter={this.props.onLabelEnter}
onLabelLeave={this.props.onLabelLeave}
prefixText={revised?"Revised: ":undefined}
/>
</div>
</div>
</Fragment>);
}
private onInputRef = (element: HTMLInputElement) => {
this.inputElement = element;
if (element) {
@ -274,20 +302,20 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
}
private isTypeOrFormatSpecified = () => {
const { tag } = this.props;
const {tag} = this.props;
return (tag.type && tag.type !== FieldType.String) ||
(tag.format && tag.format !== FieldFormat.NotSpecified);
}
private handleMouseEnter = () => {
const { labels } = this.props;
const {labels} = this.props;
if (labels.length > 0) {
this.props.onLabelEnter(labels[0]);
}
}
private handleMouseLeave = () => {
const { labels } = this.props;
const {labels} = this.props;
if (labels.length > 0) {
this.props.onLabelLeave(labels[0]);
}

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

@ -2,13 +2,16 @@
// Licensed under the MIT license.
import React from "react";
import { ILabel, IFormRegion } from "../../../../models/applicationState";
import { FontIcon } from "@fluentui/react";
import {ILabel, IFormRegion} from "../../../../models/applicationState";
import {FontIcon} from "@fluentui/react";
export interface ITagInputItemLabelProps {
label: ILabel;
onLabelEnter: (label: ILabel) => void;
onLabelLeave: (label: ILabel) => void;
value: IFormRegion[];
isOrigin: boolean;
onLabelEnter?: (label: ILabel) => void;
onLabelLeave?: (label: ILabel) => void;
prefixText?:string
}
export interface ITagInputItemLabelState {}
@ -17,7 +20,7 @@ export default class TagInputItemLabel extends React.Component<ITagInputItemLabe
public render() {
const texts = [];
let hasEmptyTextValue = false;
this.props.label.value.forEach((formRegion: IFormRegion, idx) => {
this.props.value.forEach((formRegion: IFormRegion, idx) => {
if (formRegion.text === "") {
hasEmptyTextValue = true;
} else {
@ -27,14 +30,14 @@ export default class TagInputItemLabel extends React.Component<ITagInputItemLabe
const text = texts.join(" ");
return (
<div
className={"tag-item-label flex-center px-2"}
className={[this.props.isOrigin ? "tag-item-label-origin" : "tag-item-label", "flex-center", "px-2"].join(" ")}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
<div className="flex-center">
{text}
{this.props.prefixText} {text}
{hasEmptyTextValue &&
<FontIcon className="pr-1 pl-1" iconName="RectangleShape"/>
<FontIcon className="pr-1 pl-1" iconName="RectangleShape" />
}
</div>
</div>
@ -42,10 +45,14 @@ export default class TagInputItemLabel extends React.Component<ITagInputItemLabe
}
private handleMouseEnter = () => {
this.props.onLabelEnter(this.props.label);
if (this.props.onLabelEnter) {
this.props.onLabelEnter(this.props.label);
}
}
private handleMouseLeave = () => {
this.props.onLabelLeave(this.props.label);
if (this.props.onLabelLeave) {
this.props.onLabelLeave(this.props.label);
}
}
}

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

@ -5,7 +5,7 @@ $tagInputWidth: 275px;
$tagColorWidth: 8px;
$tagLinkWidth: 22px;
$tagItemWidth: $tagInputWidth - $tagColorWidth;
$tagTextWidth: $tagItemWidth - 55px;
$tagTextWidth: $tagItemWidth - 95px;
$tagTextLinkedWidth: $tagTextWidth - $tagLinkWidth;
$tagContextualMenuWidth: $tagItemWidth - 8px;
$tagColorPickerHeight: 480px;

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

@ -2,9 +2,10 @@
// Licensed under the MIT license.
import React from "react";
import { IconButton } from "@fluentui/react";
import { strings } from "../../../../common/strings";
import { ITag } from "../../../../models/applicationState";
import {IconButton} from "@fluentui/react";
import {strings} from "../../../../common/strings";
import {ITag} from "../../../../models/applicationState";
import {constants} from "../../../../common/constants";
enum Categories {
General,
@ -29,6 +30,8 @@ export interface ITagInputToolbarProps {
onDelete: (tag: ITag) => void;
/** Function to call when one of the re-order buttons is clicked */
onReorder: (tag: ITag, displacement: number) => void;
onOnlyCurrentPageTags: (onlyCurrentPageTags: boolean) => void;
onShowOriginLabels?: (showOrigin: boolean) => void;
searchingTags: boolean;
}
@ -40,7 +43,17 @@ interface ITagInputToolbarItemProps {
accelerators?: string[];
}
export default class TagInputToolbar extends React.Component<ITagInputToolbarProps> {
interface ITagInputToolbarItemState {
tagFilterToggled: boolean;
showOriginLabels: boolean;
}
export default class TagInputToolbar extends React.Component<ITagInputToolbarProps, ITagInputToolbarItemState> {
state = {
tagFilterToggled: false,
showOriginLabels: constants.showOriginLabelsByDefault,
};
public render() {
return (
<div className="tag-input-toolbar">
@ -57,6 +70,18 @@ export default class TagInputToolbar extends React.Component<ITagInputToolbarPro
category: Categories.General,
handler: this.handleAdd,
},
{
displayName: this.state.tagFilterToggled ? strings.tags.toolbar.showAllTags : strings.tags.toolbar.onlyShowCurrentPageTags,
icon: this.state.tagFilterToggled ? "ClearFilter" : "Filter",
category: Categories.General,
handler: this.handleOnlyCurrentPageTags,
},
{
displayName: this.state.showOriginLabels ? strings.tags.toolbar.hideOriginLabels : strings.tags.toolbar.showOriginLabels,
icon: this.state.showOriginLabels ? "GroupList" : "GroupedList",
category: Categories.General,
handler: this.handleShowOriginLabels,
},
{
displayName: strings.tags.toolbar.search,
icon: "Search",
@ -109,7 +134,7 @@ export default class TagInputToolbar extends React.Component<ITagInputToolbarPro
const moveModifierClassName = moveModifierClassNames.join(" ");
const renameModifierClassName = renameModifierClassNames.join(" ");
return(
return (
this.getToolbarItems().map((itemConfig, index) => {
if (itemConfig.category === Categories.General) {
return (
@ -161,6 +186,19 @@ export default class TagInputToolbar extends React.Component<ITagInputToolbarPro
private handleAdd = () => {
this.props.onAddTags();
}
private handleOnlyCurrentPageTags = () => {
this.setState({tagFilterToggled: !this.state.tagFilterToggled}, () => {
this.props.onOnlyCurrentPageTags(this.state.tagFilterToggled);
});
}
private handleShowOriginLabels = () => {
this.setState({showOriginLabels: !this.state.showOriginLabels}, () => {
if (this.props.onShowOriginLabels) {
this.props.onShowOriginLabels(this.state.showOriginLabels);
}
});
}
private handleSearch = () => {
this.props.onSearchTags();

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

@ -0,0 +1,89 @@
import {Customizer, ICustomizations, IModalStyles, Modal, PrimaryButton, Slider} from "@fluentui/react";
import React from "react";
import {ModalBody} from "reactstrap";
import {constants} from "../../../../common/constants";
import {getDarkGreyTheme, getPrimaryGreenTheme, getPrimaryGreyTheme} from "../../../../common/themes";
interface IBatchSizeModalProps {
onConfirm?: (batchSize: number) => void;
}
interface IBatchSizeModalState {
showModal: boolean;
batchSize: number;
}
export class BatchSizeModal extends React.Component<IBatchSizeModalProps, IBatchSizeModalState>{
state = {
showModal: false,
batchSize: 3
};
onConfirm = () => {
this.setState({showModal: false});
if (this.props.onConfirm) {
this.props.onConfirm(this.state.batchSize);
}
}
onCancel = () => {
this.setState({showModal: false});
}
onBatchSizeChange = (value: number) => {
this.setState({
batchSize: value
});
}
openModal() {
this.setState({
batchSize: constants.autoLabelBatchSizeMin,
showModal: true,
});
}
render() {
const dark: ICustomizations = {
settings: {
theme: getDarkGreyTheme(),
},
scopedSettings: {},
};
const styles: Partial<IModalStyles> = {
main: {
width: "400px!important",
}
};
return (
<Customizer {...dark}>
<Modal
isOpen={this.state.showModal}
isModeless={false}
containerClassName="modal-container"
styles={styles}
>
<h4>Set Auto Labeling Batch Size</h4>
<ModalBody>
<Slider value={this.state.batchSize}
min={constants.autoLabelBatchSizeMin}
max={constants.autoLabelBatchSizeMax}
onChange={this.onBatchSizeChange} />
</ModalBody>
<div className="modal-buttons-container">
<PrimaryButton
className="model-confirm"
theme={getPrimaryGreenTheme()}
onClick={this.onConfirm}>
Ok
</PrimaryButton>
<PrimaryButton
className="modal-cancel"
theme={getPrimaryGreyTheme()}
onClick={this.onCancel}
>Cancel</PrimaryButton>
</div>
</Modal>
</Customizer>
);
}
}

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

@ -40,14 +40,14 @@
.prev {
position: absolute;
top: 50%;
left: 50px;
left: 0;
margin-left: 10px;
}
.next {
position: absolute;
top: 50%;
right: 50px;
right: 0;
margin-right: 10px;
}

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

@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import React, { ReactElement } from "react";
import React, { ReactElement, RefObject } from "react";
import { Spinner, SpinnerSize } from "@fluentui/react/lib/Spinner";
import { Label } from "@fluentui/react/lib/Label";
import { IconButton } from "@fluentui/react/lib/Button";
@ -39,6 +39,7 @@ import { AutoLabelingStatus, PredictService } from "../../../../services/predict
import { AssetService } from "../../../../services/assetService";
import { interpolate, strings } from "../../../../common/strings";
import { toast } from "react-toastify";
import {BatchSizeModal} from "./batchSizeModal";
pdfjsLib.GlobalWorkerOptions.workerSrc = constants.pdfjsWorkerSrc(pdfjsLib.version);
@ -62,8 +63,9 @@ export interface ICanvasProps extends React.Props<Canvas> {
onRunningAutoLabelingStatusChanged?: (isRunning: boolean) => void;
onTagChanged?: (oldTag: ITag, newTag: ITag) => void;
runOcrForAllDocs?: (runForAllDocs: boolean) => void;
runAutoLabelingOnNextBatch?: () => Promise<void>;
runAutoLabelingOnNextBatch?: (batchSize: number) => Promise<void>;
onAssetDeleted?: () => void;
onPageLoaded?: (pageNumber: number) => void;
}
export interface ICanvasState {
@ -81,7 +83,7 @@ export interface ICanvasState {
errorTitle?: string;
errorMessage: string;
ocrStatus: OcrStatus;
autoLableingStatus: AutoLabelingStatus;
autoLabelingStatus: AutoLabelingStatus;
layers: any;
tableIconTooltip: any;
hoveringFeature: string;
@ -141,7 +143,7 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
isError: false,
errorMessage: undefined,
ocrStatus: OcrStatus.done,
autoLableingStatus: AutoLabelingStatus.none,
autoLabelingStatus: AutoLabelingStatus.none,
layers: { text: true, tables: true, checkboxes: true, label: true, drawnRegions: true },
tableIconTooltip: { display: "none", width: 0, height: 0, top: 0, left: 0 },
hoveringFeature: null,
@ -172,6 +174,8 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
private tableIDToIndexMap: object;
autoLabelingBatchSizeModal: RefObject<BatchSizeModal> = React.createRef();
public componentDidMount = async () => {
this.ocrService = new OCRService(this.props.project);
const asset = this.state.currentAsset.asset;
@ -257,7 +261,7 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
handleAssetDeleted={this.props.onAssetDeleted}
handleRunOcrForAllDocuments={this.runOcrForAllDocuments}
handleRunAutoLabelingOnCurrentDocument={this.runAutoLabelingOnCurrentDocument}
handleRunAutoLabelingForRestDocuments={this.runAutoLabelingForRestDocuments}
handleRunAutoLabelingOnMultipleUnlabeledDocuments={this.runAutoLabelingOnMultipleUnlabeledDocuments}
handleToggleDrawRegionMode={this.handleToggleDrawRegionMode}
connectionType={this.props.project.sourceConnection.providerType}
drawRegionMode={this.state.drawRegionMode}
@ -339,11 +343,11 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
<div className="canvas-ocr-loading">
<div className="canvas-ocr-loading-spinner">
<Label className="p-0" ></Label>
<Spinner size={SpinnerSize.large} label="Running OCR..." ariaLive="assertive" labelPosition="right" />
<Spinner size={SpinnerSize.large} label="Running Layout..." ariaLive="assertive" labelPosition="right" />
</div>
</div>
}
{this.state.autoLableingStatus === AutoLabelingStatus.running &&
{this.state.autoLabelingStatus === AutoLabelingStatus.running &&
<div className="canvas-ocr-loading">
<div className="canvas-ocr-loading-spinner">
<Label className="p-0" ></Label>
@ -361,6 +365,10 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
errorMessage: undefined,
})}
/>
<BatchSizeModal
ref={this.autoLabelingBatchSizeModal}
onConfirm={this.confirmRunAutoLabelingOnMultipleUnlabeledDocuments}
/>
</div>
);
}
@ -385,10 +393,13 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
this.setAutoLabelingStatus(AutoLabelingStatus.done);
}
}
private runAutoLabelingForRestDocuments = async () => {
this.setState({ autoLableingStatus: AutoLabelingStatus.running });
await this.props.runAutoLabelingOnNextBatch();
this.setState({ autoLableingStatus: AutoLabelingStatus.done });
private runAutoLabelingOnMultipleUnlabeledDocuments = async () => {
this.autoLabelingBatchSizeModal.current.openModal();
}
private confirmRunAutoLabelingOnMultipleUnlabeledDocuments = async (batchSize: number) => {
this.setState({ autoLabelingStatus: AutoLabelingStatus.running });
await this.props.runAutoLabelingOnNextBatch(batchSize);
this.setState({ autoLabelingStatus: AutoLabelingStatus.done });
}
public updateSize() {
@ -537,8 +548,8 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
}
private deleteRegions = (regions: IRegion[]) => {
this.deleteRegionsFromSelectedRegionIds(regions);
this.deleteRegionsFromAsset(regions);
this.deleteRegionsFromSelectedRegionIds(regions);
this.deleteRegionsFromImageMap(regions);
}
@ -1196,10 +1207,11 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
}
});
}
private setAutoLabelingStatus = (autoLableingStatus: AutoLabelingStatus) => {
this.setState({ autoLableingStatus }, () => {
private setAutoLabelingStatus = (autoLabelingStatus: AutoLabelingStatus) => {
this.setState({ autoLabelingStatus }, () => {
if (this.props.onRunningAutoLabelingStatusChanged) {
this.props.onRunningAutoLabelingStatusChanged(autoLableingStatus === AutoLabelingStatus.running);
this.props.onRunningAutoLabelingStatusChanged(autoLabelingStatus === AutoLabelingStatus.running);
}
})
}
@ -1297,6 +1309,9 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
currentPage: pageNumber,
pdfFile: pdf,
});
if (this.props.onPageLoaded) {
this.props.onPageLoaded(pageNumber);
}
}
}
@ -1370,7 +1385,19 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
&& this.props.selectedAsset.labelData.labels.map(label => ({
...label, value: []
}))) || [];
const selectedRegions = this.getSelectedRegions();
if (selectedRegions.length > 0) {
const intersectionResult = _.intersection(selectedRegions, regions);
if (intersectionResult.length === 0) {
const relatedLabels = labels.find(label => selectedRegions.find(sr => sr.tags.find(t => t === label.label)));
const originLabel = this.props.selectedAsset!.labelData!.labels.find(a => a.label === relatedLabels.label);
if (relatedLabels&&originLabel&&relatedLabels.confidence) {
delete relatedLabels.confidence;
relatedLabels.revised = true;
relatedLabels.originValue = [...originLabel.value];
}
}
}
regions.forEach((region) => {
const labelType = this.getLabelType(region.category);
const boundingBox = region.id.split(",").map(parseFloat);
@ -1382,8 +1409,11 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
region.tags.forEach((tag) => {
const label = labels.find(label => label.label === tag);
if (label) {
const originLabel = this.props.selectedAsset!.labelData!.labels.find(a=>a.label === tag);
if (label.confidence && region.changed) {
delete label.confidence;
label.revised = true;
label.originValue = [...originLabel.value];
}
label.value.push(formRegion);
} else {

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

@ -15,7 +15,7 @@ interface ICanvasCommandBarProps {
handleRunOcr?: () => void;
handleRunOcrForAllDocuments?: () => void;
handleRunAutoLabelingOnCurrentDocument?: () => void;
handleRunAutoLabelingForRestDocuments?: () => void;
handleRunAutoLabelingOnMultipleUnlabeledDocuments?: () => void;
handleLayerChange?: (layer: string) => void;
handleToggleDrawRegionMode?: () => void;
handleAssetDeleted?: () => void;
@ -153,11 +153,9 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
if (props.parentPage === strings.editorPage.title) {
commandBarFarItems.push({
key: "additionalActions",
text: "Actions",
title: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.text,
// This needs an ariaLabel since it's icon-only
ariaLabel: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.text,
className: "additional-action-dropdown",
iconProps: { iconName: "More" },
subMenuProps: {
items: [
{
@ -188,14 +186,14 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
},
},
{
key: "runAutoLabelingForRestDocuments",
text: interpolate(strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runAutoLabelingOnNotLabelingDocuments, { batchSize: constants.autoLabelBatchSize }),
key: "runAutoLabelingOnMultipleUnlabeledDocuments",
text: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runAutoLabelingOnMultipleUnlabeledDocuments,
iconProps: { iconName: "Tag" },
disabled: disableAutoLabeling,
title: props.project.predictModelId ? "" :
strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.noPredictModelOnProject,
onClick: () => {
if (props.handleRunAutoLabelingForRestDocuments) props.handleRunAutoLabelingForRestDocuments();
if (props.handleRunAutoLabelingOnMultipleUnlabeledDocuments) props.handleRunAutoLabelingOnMultipleUnlabeledDocuments();
},
},
{

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

@ -96,6 +96,7 @@ export interface IEditorPageState {
errorMessage?: string;
tableToView: object;
tableToViewId: string;
pageNumber: number;
}
function mapStateToProps(state: IApplicationState) {
@ -132,6 +133,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
hoveredLabel: null,
tableToView: null,
tableToViewId: null,
pageNumber: 1
};
private tagInputRef: RefObject<TagInput>;
@ -231,11 +233,11 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
<div>
<Spinner
size={SpinnerSize.small}
label="Running OCR"
label="Running Layout"
ariaLive="off"
labelPosition="right"
/>
</div> : "Run OCR on unvisited documents"
</div> : "Run Layout on unvisited documents"
}
</PrimaryButton>
</div>}
@ -278,6 +280,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
setTableToView={this.setTableToView}
closeTableView={this.closeTableView}
runOcrForAllDocs={this.loadOcrForNotVisited}
onPageLoaded={this.onPageLoaded}
runAutoLabelingOnNextBatch={this.runAutoLabelingOnNextBatch}
appSettings={this.props.appSettings}
>
@ -296,6 +299,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
lockedTags={this.state.lockedTags}
selectedRegions={this.state.selectedRegions}
labels={labels}
pageNumber={this.state.pageNumber}
onChange={this.onTagsChanged}
onLockedTagsChange={this.onLockedTagsChanged}
onTagClick={this.onTagClicked}
@ -358,7 +362,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
/>
<PreventLeaving
when={isRunningOCRs || isCanvasRunningOCR}
message={"An OCR operation is currently in progress, are you sure you want to leave?"}
message={"An Layout operation is currently in progress, are you sure you want to leave?"}
/>
<PreventLeaving
when={isCanvasRunningAutoLabeling}
@ -497,7 +501,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
*/
private handleTagHotKey = (event: KeyboardEvent): void => {
const tag = this.getTagFromKeyboardEvent(event);
const selection = this.canvas.current.getSelectedRegions();
const selection = this.canvas?.current?.getSelectedRegions();
if (tag && selection.length) {
const { format, type, documentCount, name } = tag;
@ -627,6 +631,10 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
await this.props.actions.saveProject(project, true, false);
}
private onPageLoaded = async (pageNumber: number) => {
this.setState({ pageNumber });
}
private onLockedTagsChanged = (lockedTags: string[]) => {
this.setState({ lockedTags });
}
@ -753,7 +761,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
}
}
}
private runAutoLabelingOnNextBatch = async () => {
private runAutoLabelingOnNextBatch = async (batchSize: number) => {
if (this.isBusy()) {
return;
}
@ -764,7 +772,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
if (this.state.assets) {
this.setState({ isRunningAutoLabelings: true });
const unlabeledAssetsBatch = [];
for (let i = 0; i < this.state.assets.length && unlabeledAssetsBatch.length < constants.autoLabelBatchSize; i++) {
for (let i = 0; i < this.state.assets.length && unlabeledAssetsBatch.length < batchSize; i++) {
const asset = this.state.assets[i];
if (asset.state === AssetState.NotVisited || asset.state === AssetState.Visited) {
unlabeledAssetsBatch.push(asset);

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

@ -142,7 +142,7 @@ export default class EditorSideBar extends React.Component<IEditorSideBarProps,
switch (asset.state) {
case AssetState.Tagged:
return (
<span title={_.startCase(AssetLabelingState[asset.labelingState])}
<span title={_.capitalize(_.lowerCase(AssetLabelingState[asset.labelingState]))}
className={["badge", "badge-tagged", getBadgeTaggedClass(asset.labelingState)].join(" ")}>
<FontIcon iconName="Tag" />
</span>

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

@ -489,18 +489,13 @@ export default class ModelComposePage extends React.Component<IModelComposePageP
let nextPageList = nextPage.nextList;
nextPageList = nextPageList.filter((model) => recentModelIds.indexOf(model.modelId) === -1);
const newList = currentList.concat(nextPageList);
this.allModels = newList;
const newCols = this.state.columns;
newCols.forEach((ncol) => {
ncol.isSorted = false;
ncol.isSortedDescending = true;
});
const appendedList = currentList.concat(nextPageList);
const currerntlySortedColumn: IColumn = this.state.columns.find((column) => column.isSorted);
const appendedAndSortedList = this.copyAndSort(appendedList, currerntlySortedColumn.fieldName!, currerntlySortedColumn.isSortedDescending);
this.allModels = appendedAndSortedList;
this.setState({
modelList: newList,
modelList: appendedAndSortedList,
nextLink: nextPage.nextLink,
columns: newCols,
}, () => {
this.setState({
isLoading: false,
@ -557,7 +552,7 @@ export default class ModelComposePage extends React.Component<IModelComposePageP
private handleColumnClick = (event: React.MouseEvent<HTMLElement>, column: IColumn): void => {
const {columns, modelList} = this.state;
const newColumns: IColumn[] = columns.slice();
const currColumn: IColumn = newColumns.filter((col) => column.key === col.key)[0];
const currColumn: IColumn = newColumns.find((col) => column.key === col.key);
newColumns.forEach((newCol: IColumn) => {
if (newCol === currColumn) {
currColumn.isSortedDescending = !currColumn.isSortedDescending;

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

@ -2,16 +2,29 @@
// Licensed under the MIT license.
import React from "react";
import { FontIcon } from "@fluentui/react";
import { constants } from "../../../common/constants";
import {FontIcon} from "@fluentui/react";
import {constants} from "../../../common/constants";
import axios from 'axios';
import "./statusBar.scss";
import { IProject } from "../../../models/applicationState";
export interface IStatusBarProps {
project: IProject;
}
interface IStatusBarState {
commitHash?: string;
}
export class StatusBar extends React.Component<IStatusBarProps, IStatusBarState> {
componentDidMount() {
const commitInfoUrl = require("../../../git-commit-info.txt");
axios.get(commitInfoUrl).then(res => {
// match the git commit hash
const commitHash = /commit ([0-9a-fA-F]{7})/.exec(res?.data)[1];
this.setState({ commitHash: commitHash || "" });
});
}
export class StatusBar extends React.Component<IStatusBarProps> {
// export class StatusBar extends React.Component<IStatusBarProps> {
public render() {
return (
<div className="status-bar">
@ -29,7 +42,7 @@ export class StatusBar extends React.Component<IStatusBarProps> {
<li>
<a href="https://github.com/microsoft/OCR-Form-Tools/blob/master/CHANGELOG.md" target="blank" rel="noopener noreferrer">
<FontIcon iconName="BranchMerge" />
<span>{constants.appVersionRaw}-1f33130</span>
<span>{constants.appVersion}-{this.state?.commitHash}</span>
</a>
</li>
</ul>

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

@ -55,6 +55,7 @@ export function registerIcons() {
StatusCircleCheckmark: "\uF13E",
CircleRing: "\uEA3A",
Filter: "\uE71C",
ClearFilter: "\uEF8F",
Table: "\uED86",
MapLayers: "\uE81E",
BookAnswers: "\uF8A4",
@ -74,6 +75,9 @@ export function registerIcons() {
RectangleShape: "\uF1A9",
Rotate90CounterClockwise: "\uF80E",
Rotate90Clockwise: "\uF80D",
AzureAPIManagement: "\uF37F", },
AzureAPIManagement: "\uF37F",
GroupedList: "\uEF74",
GroupList: "\uF168",
},
});
}

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

@ -155,7 +155,7 @@ export class OCRService {
setTimeout(checkSucceeded, interval, resolve, reject);
} else {
// Didn't succeeded after too much time, reject
reject(new Error("Timed out for getting OCR results"));
reject(new Error("Timed out for getting Layout results"));
}
});
};

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

@ -11753,13 +11753,20 @@ serialize-javascript@^2.1.2:
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61"
integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==
serialize-javascript@^3.0.0, serialize-javascript@^3.1.0:
serialize-javascript@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-3.1.0.tgz#8bf3a9170712664ef2561b44b691eafe399214ea"
integrity sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==
dependencies:
randombytes "^2.1.0"
serialize-javascript@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4"
integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==
dependencies:
randombytes "^2.1.0"
serve-index@^1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239"