Support supervised table labeling/training/prediction with new data schema. (#828)

* added new icons

* feat: support table tag creation

* feat: support labeling

* fix: zoomIn keyboar shortcut for macOS

* fix: appId

* fix: icons scr url (#593)

* Alex krasn/rtl refactor to functional components (#594)

* refactor to funct componenet - tableTagConfig.tsx

* tagInputItemLabel.tsx- refactor to functional component

* fix format error detected by tslint

* support dropdown options (#603)

* support dropdown options

* add format filter on cell type choice

* reset format on type change

* add default theme to dropdowns

* refactor: introduce and lift state of table body

* Alex krasn/rtl create table view styles (#620)

* styles (resize pane WIP)

* styles

* enable feature (#621)

* Alex krasn/rtl support validation of user input on config table (#611)

* -merge with changes

* feat: enable user input validation

* add strings

* fix wording

* - on save show only generic error message

* custom error message for textfield

* reset type and format of rows/columnst on save + refactor

* refactor: support interfaces and lift state for labeling

* feat: persist table labeling

* feat: enable deleting headers (#627)

* feat: table view styles (#629)

* feat: table view styles

* add styles for 'cells'

* feat: table preview (#630)

* Alex krasn/rtl eanble reordering of headers (#631)

* enable reordering

* fix checkmark and addButton styles

* pipe line test fix

* styles fix

* fix resize pane width

* fix: allow deleting and reassigning of table regions

* feat: add table label description

* fix: support selecting regions without opening table labeling

* fix: react unique keys errors

* fix: selection behaviour + refactor

* disable move btns when a selection reached list's edges

* disable delete btns when it's just one row/col

* makes inputs placeholder less visible

* support large tables in tableConfig + support resize pane (#634)

* support large tables in tableConfig + support resize pane

* cleanup

* support large tables in tableLabeling

* fix: buttons container nesting

* fix: setup initial container width, and setup min and max headers width

* add reconfigure btn to tableLabeling.tsx + some styling

* add HOW TO label the table

* add strings to tableLabeling.tsx

* disable "table" type as an option for headers type

* fix: spelling

* feat: not allow tag type change if it is tableTag

* disable tagtype and format from table tag dropdown submenu

* delete table type options for all other tags (not table)

* fix: support reconfigure button

* fix: scroll unnecessary scrollbar in tableLabeling

* refactor: rename + types

* support row dynamic table in tableTagConfig

* support table name in tableTagLabeling.tsx

* support view of row dynamic tables in tableTagLabeling.tsx table view

* add reconfig button to tag menu items

* add table tag color bar under table name in table labeling view

* add "Add row" btn for labeling row dynamic tables

* fix: wording and strings for Confirm message on reconfigure table

* fix: trim all inputted names during config validation

* fix: on resize right splitpane to width less than min allowed

* support rowDynamic table in tableLabeling table view

* fix: table labeling view styles

* add row indices for tableLabel table view

* add indices in tableConfig + support some reconfig func

* fix: dont show "Row #" for fixed tables in tableTagLabeling.tsx

* fix: restore right pane size for basic input mode

* feat: support row dynamic labeling

* fix: tableName validation erorr

* fix: show one row for rowDynmic in preview

* refactor: validateInputAndSave

* refactor: support adding rows for row dynamic tables

* refactor: support row dynamic labeling

* fix: trim rows

* merge master into RTL branch (#647)

* merge master into RTL branch

* fix an API icon

* add stings to tableConfig

* fix: typo in tableTagToAdd

* tableLabeling: refactor, styles, strings

* tableLabeling: add spanish strings

* refactor: optimize inputs validation

* fix: TableElements enum spelling

* fix: use correct table format values

* fix: spelling

* enable reconfigure button from tag menu dropdown

* refactor: remove not needed customizer

* fix: conditional Enum for headersFormatAndTypeOptions

* feat: support reconfigure table map

* feat: cross out columns when renaming

* fix: scrollbar visibilty + style adjustments

* fix: scroll issue on open

* feat: support tracking changed and deleted headers

* autofocus on last added input

* fix: delete extra colon

* fix: class name for renamed header  + forgoten variable

* enable reconfigure [cancel] button

* styles for renamed headers

* display original col/row name on reconfigure

* fix: delete autofocus on btns

* fix: revert accidentally deleted ref + whitespaces

* display table name in preview

* fix: support deleted headers and style change

* feat: support reconfig deleting fields

* feat: support deleting tables

* fix margin + and Lunk styles

* restore deleted row/col: Link => ActionButton

* fix: splitpane and labelng/config styles

* add: tag color for reconfigure

* fix: tableName color for config

* feat: support saving reconfigure

* analyze results

* fix: delete red table border

* fix reconfigure name validation and toast message

* fix on reconfigure projectAction assets update

* add higlight table results in analyze

* feat: redraw canvas with changed regions

* fix: add reconfigure support for tag name change

* handle long row/col names without spaces

* temp fix for unique column names in analyzeOutput.json

* feat: handle long col and row names in tableTagConfig

* feat: enable drawn region icon and selection marks

* feat: add progress to table reconfigure

* feat: support rowKeys and columnKeys for analysis

* feat: support tracking column and row count

* fix: reconfigure progess spinner styles

* feat: highlight table cell bounding box on mouse enter

* fix: reconfigure state on close()

* fix: commnet out testing config in predictPage

* fix: check for rowKeys in updatedAssetMetadata

* fix: on reconfigure when adding columns (rdt)

* new row/col headers icons

* fix: update asset after reconfigure

* fix: check if theres tags

* fix: update document bounding boxes after reconfigure

* fix: handle region reset after reconfigure

* refactor: hardcode to preview.3 api

* feat: support rename of table tag

* merge with master as of 11/16

* feat: enable table support for labeledTags filter

* fix: table labeling issues

* fix issue of "Trained label doesn't show as manually labeled after revisioni"

(cherry picked from commit d17e8a0c7a)

* fix: display analyze results

* Merge branch 'stew-ro/support-table-labeling' of https://github.com/microsoft/OCR-Form-Tools into rtl_merge_with_master

* support new schema for prediction

* support highlighting of tableTag and tableCell in editor page

* display table in tag view and support highlight after schema change

* feat: support row/col header selectionMark logic

* display column header after schema change in prediction results

* handle empty rows after schema change predict

* use fieldType Object and Array for new schema

* write new schema to field.json

* fix on 'definitions' undefiened on reconfigure

* support writing to field.json and removing rowKeys and columnKey logic

* fix: remove label type when invalid

* fix: use default rows and columns if field is null

* feat: support label files for new schema

* support encoding

* fix decoding and encoding logic

* fix remove label with empy value

* update taginput toolbar

* change row dynamic starting index to zero

* support reconfigure after schema change

* support drawn region logic for table labeling

* support encoding for refactoring table

* support rename and delete tag

* support empty table for analysis

* fix: support empty tables for analysis

* Fix pipeline build failures.

Co-authored-by: alex-krasn <v-alexkr@microsoft.com>
Co-authored-by: stew-ro <v-stewro@microsoft.com>
Co-authored-by: alex-krasn <64093224+alex-krasn@users.noreply.github.com>
Co-authored-by: Alex Chen <v-yongbc@microsoft.com>
Co-authored-by: Starain chen <v-stachen@microsoft.com>
Co-authored-by: Alexander Krasnorutskiy <alexk@Alexanders-MBP.domain>
This commit is contained in:
Buddha Wang 2021-01-01 20:38:06 +08:00 коммит произвёл GitHub
Родитель ae4ce9a122
Коммит c279a3f784
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
49 изменённых файлов: 4464 добавлений и 996 удалений

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

@ -1,373 +1,373 @@
# FoTT Changelog
## What's new in Form Recognizer?
Click [here](https://docs.microsoft.com/en-us/azure/cognitive-services/form-recognizer/whats-new) to see what's new in Form Recognizer.
## Released conatiner's currently referenced commit
2.1-Preview's released container image, tracked by the `latest-preview` image tag in our [docker hub repository](https://hub.docker.com/_/microsoft-azure-cognitive-services-custom-form-labeltool), currently references **2.1-preview.2-b6b9a2f (12-10-2020)**
## Commit history
### 2.1-preview.2-b6b9a2f (12-10-2020)
* update appVersion to 2.1.2 ([#808](https://github.com/microsoft/OCR-Form-Tools/commit/b6b9a2f131485d08541a1e85f6af59ebfbeca773))
* add locale in prebuiltPredictPage ([#772](https://github.com/microsoft/OCR-Form-Tools/commit/06d9c16a7c1fe64a95d835878dcb8dabb8c7e485)) ([#776](https://github.com/microsoft/OCR-Form-Tools/commit/06d9c16a7c1fe64a95d835878dcb8dabb8c7e485))
* Stew ro/cherry pick 347e21e 2b6ead7 ([#766](https://github.com/microsoft/OCR-Form-Tools/commit/ca59cee26587e1aee49507abd75c0683d67f541f))
* Cherry pick 34ce14d a7ccb34 ([#763](https://github.com/microsoft/OCR-Form-Tools/commit/244c23df700791794990eb6f7e196bcf9ff9c844))
* Stew ro/cherry pick ab5a8a8 abfffbb ([#760](https://github.com/microsoft/OCR-Form-Tools/commit/93b7a2d4d7688cda4baa4cfd704b2666df524174))
* refactor: disable api version selection ([#755](https://github.com/microsoft/OCR-Form-Tools/commit/be1f18db0b7073dad106f443f343aa221dea7fc6))
* refactor: disable draw region button ([#756](https://github.com/microsoft/OCR-Form-Tools/commit/8816a85761a795f3f0b34b87360236cadd80a735))
* feat: support null text values in analyze results ([#744](https://github.com/microsoft/OCR-Form-Tools/commit/0ddb7f1275d6f195c2af9f0b7053987e01a5d677))
* feat: support rowspan and column span for layout tables ([#754](https://github.com/microsoft/OCR-Form-Tools/commit/6994ac929146e25370d1461e4e502773de3d5503))
* Update changelog ([#750](https://github.com/microsoft/OCR-Form-Tools/commit/acba3966ce960e474dfd3d97510b07c108e7b39f))
* check whether the label data is null ([#753](https://github.com/microsoft/OCR-Form-Tools/commit/f6ef41ac1500e52f3714eda6e99187e016e1b223))
* fix issue of 773 ([#740](https://github.com/microsoft/OCR-Form-Tools/commit/9d35e79393cdb0de678e9ec6b850daf5a4df5c96))
### 2.1-preview.1-2e50498 (11-09-2020)
* fix: enable api version selection ([#736](https://github.com/microsoft/OCR-Form-Tools/commit/2e5049883bd1550ba80210edca7db4233d7a15fa))
* fix: labeling doesn't work via shortcuts on the new project or empty tags ([#677](https://github.com/microsoft/OCR-Form-Tools/commit/f11291940b776ceb8ba7708e6f58dc2572f7b01b))
* fix: remove setting project state in project form on change ([#732](https://github.com/microsoft/OCR-Form-Tools/commit/25eb59bfa85b755cd877b02ffda71d0cec70a106))
* handle training state logical ([#731](https://github.com/microsoft/OCR-Form-Tools/commit/569adf161ad89106ab1fbf51429841c5955e0e4b))
* fix issue of "After running Layout an all documents FoTT sometimes does not ends" ([#723](https://github.com/microsoft/OCR-Form-Tools/commit/6203e2cd814c95e2bef165f3fb518a566166c26d))
* set includeTextDetails=true in prebuilt predict ([#722](https://github.com/microsoft/OCR-Form-Tools/commit/ba04cebb63a5b05a369ba954a99dfdc7c9bb9b41))
* fix issue of "Auto-labeling while switching assets in asset preview causes an error" ([#721](https://github.com/microsoft/OCR-Form-Tools/commit/59fe4e2778a644335da9766fd1382d56086220c1))
* feat: support api version config ([#717](https://github.com/microsoft/OCR-Form-Tools/commit/c81b2323aaa2b26b0bc0f7922de1e12445fbb627))
* update homepage style ([#724](https://github.com/microsoft/OCR-Form-Tools/commit/fc769f41c169083098f9250c9ce18ca4881cc336))
* issuefix: update getBoundingBox ([#730](https://github.com/microsoft/OCR-Form-Tools/commit/f0cb5db337364b2f0355928616d1f7d9637a454a))
* clone with lodash cloneDeep ([#728](https://github.com/microsoft/OCR-Form-Tools/commit/d6bca5fcf2262a467ede781916a01f50d805b30f))
* remain auto label state while no label data ([#727](https://github.com/microsoft/OCR-Form-Tools/commit/a79d556a3935e387b0798ecfda8de9c8b1538250))
* deep copy asset metadata ([#725](https://github.com/microsoft/OCR-Form-Tools/commit/ba8c1100e9adf517e35ec50fd513383d9e84d630))
* Yongbing chen/receipt predicting ([#626](https://github.com/microsoft/OCR-Form-Tools/commit/e638cd8e3be8926e966a5afc86fb53ac0f092977))
### 2.1-preview.1-32cfaea (11-06-2020)
* Starain chen/clean autolabel data while training ([#712](https://github.com/microsoft/OCR-Form-Tools/commit/32cfaea023e96c8aa00560a3f30134683ee25757))
* fix issue of deleting tag ([#703](https://github.com/microsoft/OCR-Form-Tools/commit/282d55700ea9fdf4cac2b0f20901e8ff6115819e))
### 2.1-preview.1-c7ed086 (11-04-2020)
* Update README.md([#??](https://github.com/microsoft/OCR-Form-Tools/commit/c7ed08612876af8bb619a080f6740fceabb4e67c))
* Update README.md([#??](https://github.com/microsoft/OCR-Form-Tools/commit/d696b8a25438590fb44c5159b3142b17178f25d2))
* fix: use constant if no api version specified ([#684](https://github.com/microsoft/OCR-Form-Tools/commit/8ccdab83f079d976f6521bc08c50d917900483c0))
* auto labeled tag design & replacing between text with draw region ([#670](https://github.com/microsoft/OCR-Form-Tools/commit/757e0dd85b3c69c6642674e48e9d3549807fecbd))
### 2.1-preview.1-aab6938 (11-03-2020)
* Fix the issue that git-commit-info.txt could be override ([#683](https://github.com/microsoft/OCR-Form-Tools/commit/aab69380a8e1f7f113011a7c6b6ed406c4329555))
* fix: use existing git hash when not in git repository ([#682](https://github.com/microsoft/OCR-Form-Tools/commit/586fbb0ce51c27ae42ca857a372e8e8d5dea21d1))
* Stew ro/use api version selected in project settings ([#678](https://github.com/microsoft/OCR-Form-Tools/commit/bed69a3f64b0da7590ca3c54e8de369844c6bcd9))
* refactor: change drawn region icon ([#675](https://github.com/microsoft/OCR-Form-Tools/commit/5614da2681bb8fadf9d3db3ff95aa62362d00175))
### 2.1-preview.1-3485d33 (10-30-2020)
* feat: add bmp support for analyze page ([#672](https://github.com/microsoft/OCR-Form-Tools/commit/3485d33eca96321cf667c5c8eba22cc60af42e23))
* Alex krasn/bugfix on hotkeys when canvas not loaded yet ([#664](https://github.com/microsoft/OCR-Form-Tools/commit/b0404c6276f8fe55292c929e2ca431ed31ef6442))
### 2.1-preview.1-7166cda (10-29-2020)
* fix: use node to update status bar with latest git commit ([#671](https://github.com/microsoft/OCR-Form-Tools/commit/7166cdae5763a93feee52842af8e2246fedbf818))
* change OCR to Layout in UI (Actions) ([#666](https://github.com/microsoft/OCR-Form-Tools/commit/ac604b6bd43eb4c3ba8929a97b308c833d0e6c13))
* Yongbing chen/hitl update notify message ([#651](https://github.com/microsoft/OCR-Form-Tools/commit/0fa559a4b28c6648eaa17ec047ebb9caabbdc9c7))
### 2.1-preview.1-6d775ae (10-27-2020)
* Yongbing chen/ui adjustment with designers feedback ([#662](https://github.com/microsoft/OCR-Form-Tools/commit/6d775ae8d4495ca31d110e500b86d3c0eed6a954))
### 2.1-preview.1-c86b6de (10-23-2020)
* Fix the issue that git-commit-info.txt could be override ([#668](https://github.com/microsoft/OCR-Form-Tools/commit/c86b6de35ecd5d004dfb64f8f857d06f0557a00d))
* Xinxl/fix hash ([#667](https://github.com/microsoft/OCR-Form-Tools/commit/cb27cbd74ff890dc7e13865d89ca5e16b0807fbb))
### 2.1-preview.1-0aae169 (10-22-2020)
* Alex krasn/fix confidence level bar styles ([#657](https://github.com/microsoft/OCR-Form-Tools/commit/0aae1690351f3de27114e6cbebd2c077be8e9016))
### 2.1-preview.1-d644459 (10-21-2020)
* refactor: change error styling and wording for project sharing ([#653](https://github.com/microsoft/OCR-Form-Tools/commit/d644459e4c9b1f82b1ed2d5b537960b0f16184da))
* fix: sort models after loading next page in model compose ([#659](https://github.com/microsoft/OCR-Form-Tools/commit/9818d6301ef613155951381598f9ad4cf8ff6e3c))
* Alex krasn/serialize javascript vulnerability ([#612](https://github.com/microsoft/OCR-Form-Tools/commit/66b03303b1325634371ebdb3923acaa6722be89f))
* update asset labelingState when load local project ([#660](https://github.com/microsoft/OCR-Form-Tools/commit/1aa3daaeeb1c8a4773e7b6236fc6462335e410f9))
### 2.1-preview.1-28c54fc (10-20-2020)
* fix: check for local connections ([#654](https://github.com/microsoft/OCR-Form-Tools/commit/28c54fcc31defe1c4ebcf685675768b99c8e00c8))
* get last commit hash code in current branch and show on status bar ([#642](https://github.com/microsoft/OCR-Form-Tools/commit/88c547995d31f945177da70141f997e441b3259c))
* new feature: tags in current page ([#640](https://github.com/microsoft/OCR-Form-Tools/commit/af5396fe8e63b88b90d16953e17ce2006afe782e))
### 2.1-preview.1-6c1ee2b (10-16-2020)
* adjust editor view offset ([#646](https://github.com/microsoft/OCR-Form-Tools/commit/6c1ee2b6b4f1bcf28b1c9081b21f0a8783518c80))
### 2.1-preview.1-b92e4b3 (10-15-2020)
* reword asset states ([#644](https://github.com/microsoft/OCR-Form-Tools/commit/b92e4b3d5a786a852c319c05697eea331c147cee))
### 2.1-preview.1-4544e52 (10-14-2020)
* feat: support apiVersion selection from project settings ([#641](https://github.com/microsoft/OCR-Form-Tools/commit/4544e5255cf2356a4ddf353f7a63994c1a0865da))
### 2.1-preview.1-94f12bb (10-13-2020)
* new feature: highlight current tag ([#628](https://github.com/microsoft/OCR-Form-Tools/commit/94f12bb4e925a86fdfba8e25d8b0346169daea1e))
* new feature: human in the loop auto labeling ([#571](https://github.com/microsoft/OCR-Form-Tools/commit/c1f227daa3decd52320f58d151755b206280cedd))
### 2.1-preview.1-7d1f871 (10-10-2020)
* Update CHANGELOG.md([#??](https://github.com/microsoft/OCR-Form-Tools/commit/7d1f87193b3917f2140ab9bcce04c64e7aceb823))
### 2.1-preview.1-1f33130 (10-09-2020)
* fix: support image map interactions for container releases([#639](https://github.com/microsoft/OCR-Form-Tools/commit/e015973aee152b8a8b22fc2fe32ce80bdd2b46ea))
### 2.1-preview.1-6d4e93b (10-07-2020)
* Fix: use file type library for mime type validation ([#636](https://github.com/microsoft/OCR-Form-Tools/commit/6d4e93bca8a4e3d677c765ed5596bde502766e2e))
### 2.1-preview.1-355ca0b (09-30-2020)
* feat: add spinner in saving project, can avoid multiple commit ([#617](https://github.com/microsoft/OCR-Form-Tools/commit/355ca0b156b2d44aafd2eaaccf2fc52385c7f5f8))
### 2.1-preview.1-53044f7 (09-29-2020)
* fix: refresh currentProjects when load project ([#615](https://github.com/microsoft/OCR-Form-Tools/commit/53044f72dd9c9c72557c74c00605ba05ee50205d))
* sync related region color when tag color changed ([#598](https://github.com/microsoft/OCR-Form-Tools/commit/3044cc51a9166877bb4f01f28753171b82c04ccd))
* feat: add current list item style ([#601](https://github.com/microsoft/OCR-Form-Tools/commit/3e503e75513e44e6a90bd013d8dd15c3096cd7e9))
* fix: remove project from app if security token does not exist ([#468](https://github.com/microsoft/OCR-Form-Tools/commit/730e1963a06f038a4efa9750fcef4be6f15a8460))
### 2.1-preview.1-d859d38 (09-27-2020)
* fix ,update document state when preview (#317) ([#471](https://github.com/microsoft/OCR-Form-Tools/commit/d859d38ecc1f96b194ffa130a1840f5a7d9b1a9b))
* refactor: change the confidence value format to percentage ([#461](https://github.com/microsoft/OCR-Form-Tools/commit/e806b4e0dfcc68e6408e2130a46a318637a482a8))
### 2.1-preview.1-7a3f7a7 (09-25-2020)
* security: upgrade node-forge ([#622](https://github.com/microsoft/OCR-Form-Tools/commit/7a3f7a773c8b01f443afaad89d7974a5bbb0b869))
* fix: disable move tag and support renaming when searching ([#618](https://github.com/microsoft/OCR-Form-Tools/commit/cac1e8e6cfb2805a6540f9e80d564a0ff8be81c7))
### 2.1-preview.1-4163edc (09-23-2020)
* docs: add latest tag reference to changelog ([#608](https://github.com/microsoft/OCR-Form-Tools/commit/4163edc18bc65234e263703fc829d2f297953385))
* fix: use region instead of drawnRegion for labelType in label file ([#582](https://github.com/microsoft/OCR-Form-Tools/commit/ffafc200249a1c47698fedb279b4b55cef0190ba))
* docs: update readme with docker hub info ([#604](https://github.com/microsoft/OCR-Form-Tools/commit/63bbea076d598d0286095fa0eca48d8c9d0ed706))
* fix: remove opening browser for yarn start ([#605](https://github.com/microsoft/OCR-Form-Tools/commit/f6c4dc3585df71d09252a28f65e835a594389118))
* fix: update changelog updater script ([#607](https://github.com/microsoft/OCR-Form-Tools/commit/7c4848c3a72259562c0461f0e2eadfb4a660fa64))
### 2.1-preview.1-f2db74e (09-17-2020)
* docs: udpate changlog with docker image reference ([#590](https://github.com/microsoft/OCR-Form-Tools/commit/f2db74e322c32338eba3b2df06c01a51cfb7ebc1))
### 2.1-preview.1-1a6b78e (09-16-2020)
* fix: normalize folder path starting with a period ([#592](https://github.com/microsoft/OCR-Form-Tools/commit/1a6b78e054235da3188aafbe65636a8c18b439bf))
* fix: change label folder uri title ([#588](https://github.com/microsoft/OCR-Form-Tools/commit/7e4233e568d94817e23dda5ef5513b9ee7475d11))
### 2.1-preview.1-6a1ced5 (09-15-2020)
* fix: initialize drag pan for analyze page ([#586](https://github.com/microsoft/OCR-Form-Tools/commit/6a1ced5a0bfb03ceba515faddbfa010ac8451460))
* fix: zoomIn keyboar shortcut for macOS ([#581](https://github.com/microsoft/OCR-Form-Tools/commit/5afeebfee28e10e390f073990f90348c5117475f))
* fix: appId ([#584](https://github.com/microsoft/OCR-Form-Tools/commit/e053b151441e956641ed05c29106d02358a40792))
* fix: remove escape quote from release script ([#579](https://github.com/microsoft/OCR-Form-Tools/commit/bd5d51e8e15809b95f15bc495f7d0f91fecfc22d))
* Stew ro/support drag pan for release ([#576](https://github.com/microsoft/OCR-Form-Tools/commit/77620eccd21d564473c81b43341f59de22339248))
### 2.1-preview.1-0633507 (09-14-2020)
* Update README.md([#??](https://github.com/microsoft/OCR-Form-Tools/commit/0633507aa767f996add313ced06c2365c5f240c8))
### 2.1-preview.1-8d2286f (09-13-2020)
* persist trainPage inputs in localStorage ([#568](https://github.com/microsoft/OCR-Form-Tools/commit/8d2286f50236e41fe5540dbb9b161ea88bbf2d7a))
### 2.1-preview.1-bb23e31 (09-11-2020)
* build(deps): bump node-fetch from 2.6.0 to 2.6.1 ([#575](https://github.com/microsoft/OCR-Form-Tools/commit/bb23e3199c5721338241c8c5ccc0bda104fd15f8))
* fix: support multiple env files ([#574](https://github.com/microsoft/OCR-Form-Tools/commit/cf64a8ddde05e7e73cad37d271f5d6dfa61c5d7f))
* fix: "Azure blob storage" error on on premise scenario ([#572](https://github.com/microsoft/OCR-Form-Tools/commit/46f0bc59f3a531c366bc2c7cec955d2cb6ed7cd6))
* fix ([#563](https://github.com/microsoft/OCR-Form-Tools/commit/28c792e10692e3cc1f511852ffc9fdbc8dcdda8a))
### 2.1-preview.1-7e828ff (09-10-2020)
* fix: allow training with placeholder ([#569](https://github.com/microsoft/OCR-Form-Tools/commit/7e828ff02ff8a22b64b4b7d16787d77afb76af62))
* docs: update changelog ([#564](https://github.com/microsoft/OCR-Form-Tools/commit/1dec72c5df3206554a1e0864b65cc769835785fd))
* Yongbing chen/human in the loop ([#517](https://github.com/microsoft/OCR-Form-Tools/commit/be9d56481510e3033bcd705743c1ee9aeee20522))
* fix: support project folder in project settings for local file system ([#559](https://github.com/microsoft/OCR-Form-Tools/commit/b92b73bb8076f9b9bb55dd38fcd223b7b93eaa2e))
* feat: enable canvas rotation ([#553](https://github.com/microsoft/OCR-Form-Tools/commit/c27a110251df1fc7a595524846e20fd09c79f915))
* fix: handle tag is undefined error ([#557](https://github.com/microsoft/OCR-Form-Tools/commit/7e4d3fbbc3a2bf925c126cd2b3f493cca48e7a62))
### 2.1-preview.1-193520e (09-08-2020)
* fix: accept selection of only .fott files for open local project ([#554](https://github.com/microsoft/OCR-Form-Tools/commit/193520e2c3e40b58c3612507efc2249aaf4e9d05))
* fix: use default shared folder for label URI when training ([#551](https://github.com/microsoft/OCR-Form-Tools/commit/656de2ff07c2083affc2adf52f1a56c5a9c024b8))
* fix: show label folder uri while training ([#539](https://github.com/microsoft/OCR-Form-Tools/commit/0ad389c06328cd6428653bb7d94d5af716e02ab7))
* feat: add canvas command bar to analyze page with only zoom buttons ([#549](https://github.com/microsoft/OCR-Form-Tools/commit/895b52740cd1e8d61b5b021e6c0d992f44ce8052))
### 2.1-preview.1-4852c84 (09-05-2020)
* fix buttons styles - makes them more visible ([#526](https://github.com/microsoft/OCR-Form-Tools/commit/4852c8429d25b5569c3335b014da5972cbcc6162))
### 2.1-preview.1-343ea16 (09-04-2020)
* refactor: remove array for drawn region labels ([#542](https://github.com/microsoft/OCR-Form-Tools/commit/343ea16e18199ab5098395ae8b7a164cd8bab55e))
* fix: add key prop to region icon ([#540](https://github.com/microsoft/OCR-Form-Tools/commit/87b69093f2d35d91a2a939c46ac66ba4d22a5cb7))
### 2.1-preview.1-b370c9a (09-02-2020)
* fix: resize canvas on asset preview resize ([#535](https://github.com/microsoft/OCR-Form-Tools/commit/b370c9a9bcf7da416944c626f6d4fd7bd29088bb))
### 2.1-preview.1-de1c304 (08-31-2020)
* refactor: upgrade tsconfig es2017 to esnext ([#531](https://github.com/microsoft/OCR-Form-Tools/commit/de1c30410b860c9576108f076ec4dd8273e61a79))
### 2.1-preview.1-530545c (08-28-2020)
* fix: remove existing bounding boxes from document on analyze ([#523](https://github.com/microsoft/OCR-Form-Tools/commit/6a1aedfb89b0499a0f4782e16ccbd8a06887841d))
* feat: enable download JSON of trained model ([#513](https://github.com/microsoft/OCR-Form-Tools/commits/master))
### 2.1-preview.1-529a0e8 (08-27-2020)
* fix: show loading indicator while loading model info ([#514](https://github.com/microsoft/OCR-Form-Tools/commit/529a0e819f4cb405e290f34d18d15c487a7bcfad))
* docs: update telemetry disclaimer ([#521](https://github.com/microsoft/OCR-Form-Tools/pull/521))
* fix: disable clearing of drawn regions on analyze page ([#518](https://github.com/microsoft/OCR-Form-Tools/commit/298d7c97da1278996d2ee6020d3face0785bc4eb))
### 2.1-preview.1-b2d9a0b (08-26-2020)
* docs: notice that telemetry is disabled ([#501](https://github.com/microsoft/OCR-Form-Tools/commit/b2d9a0b008ebf350dfcb5fe897fc5dfe0d4d5cb6))
### 2.1-preview.1-d9db4ee (08-24-2020)
* refactor: upgrade storage-blob to v12.1.2 ([#509](https://github.com/microsoft/OCR-Form-Tools/commit/d9db4ee027240a82feef5b54e5e406c3793d8050))
* feat: support region labeling ([#481](https://github.com/microsoft/OCR-Form-Tools/commit/dd78ed06761a341908bdb1b09e73fd1f2868431c))
* feat: support adding model to recent models from compose page ([#510](https://github.com/microsoft/OCR-Form-Tools/commit/65fc92b5737ceea14ff89aa78052be26835ad0ae))
### 2.1-preview.1-2402cba (08-17-2020)
* fix: notify error message when open project with invalid security token ([#506](https://github.com/microsoft/OCR-Form-Tools/commit/2402cbaf73eba47ad188f851227c04cd44a208d4))
### 2.1-preview.1-a8ef8fa (08-17-2020)
* fix: don't allow create or update connection with duplicate name ([#486](https://github.com/microsoft/OCR-Form-Tools/commit/a8ef8fab603b3d2c08c533cb5dfe67da117942a0))
### 2.1-preview.1-530545c (08-14-2020)
* fix: "failed to fetch()" error ([#491](https://github.com/microsoft/OCR-Form-Tools/commit/530545c7cd2b4a3ff444e9c7e1f40c68d4a7376c))
* fix: sync layer visibility ([#497](https://github.com/microsoft/OCR-Form-Tools/commit/bea552b28acb9b652ffaedf40009d6df5a3197ef))
* refactor: disable telemetry service ([#498](https://github.com/microsoft/OCR-Form-Tools/commit/6e3628cf174f954693380aab6ebd2dabe027ac6d))
* fix: change share class name for adblocker chrome extension ([#492](https://github.com/microsoft/OCR-Form-Tools/commit/aa8a73afc6344f3164e79f236d5fa4bb0f64d364))
### 2.1-preview.1-da405b3 (08-10-2020)
* fix: restrict tag type through hot keys ([#482](https://github.com/microsoft/OCR-Form-Tools/commit/da405b354428b829e895a35a020736b1d88c153f))
* docs: add share project description to README ([#488](https://github.com/microsoft/OCR-Form-Tools/commit/7ee215f735a84aaa30201748d19207bcc6a05580))
### 2.1-preview.1-29d1f93 (08-07-2020)
* fix: handle multi selection of non-compatible types with multi-selection tool ([#487](https://github.com/microsoft/OCR-Form-Tools/commit/29d1f93a290e55fdd84f8cf2ee9a914fed702beb))
### 2.1-preview.1-cef225f (08-06-2020)
* fix: handle undefined image map error ([#462](https://github.com/microsoft/OCR-Form-Tools/commit/cc9e9bfc8fe00bb0ed154edb791446f28060af4e))
* fix: handle undefined image map error ([#479](https://github.com/microsoft/OCR-Form-Tools/commit/cef225f3346628e79c46e799303400965f1d3c96))
### 2.1-preview.1-76945df (08-05-2020)
* fix: use english for telemetry reporting ([#472](https://github.com/microsoft/OCR-Form-Tools/commit/76945df3bdf9caba3ba13f4541e17e75b9574b33))
* fix: resolve unhandled exeptions and new message for OCR service on 400 ([#470](https://github.com/microsoft/OCR-Form-Tools/commit/76381bc659a365ead19387b933485530d2d5edc3))
* feature: enable popup with composed model info ([#460](https://github.com/microsoft/OCR-Form-Tools/commit/c1f5d803f047e5ca0d18fea6383b3baf56d116ff))
### 2.1-preview.1-f4d53ce (08-03-2020)
* fix: bump elliptic from 6.5.2 to 6.5.3 ([#469](https://github.com/microsoft/OCR-Form-Tools/commit/f4d53cec967194445885bd3748096f0a3ce10715))
* feat: add modelCompose icon and created time ([#466](https://github.com/microsoft/OCR-Form-Tools/commit/2fa32ef5f77ec7bb44bf42e9fc0a5fdf7f0330c3))
### 2.1-preview.1-78996ea (07-31-2020)
* refactor: relocate share button ([#464](https://github.com/microsoft/OCR-Form-Tools/commit/78996ea65616b28d7471b59f4f16f254d7d33127))
### 2.1-preview.1-0e1b637 (07-29-2020)
* feat: show only ready models in the list ([#459](https://github.com/microsoft/OCR-Form-Tools/commit/0e1b637003f289c56955342f44963003c1543436))
### 2.1-preview.1-84f8285 (07-27-2020)
* fix: show message on model composition fail ([#457](https://github.com/microsoft/OCR-Form-Tools/commit/84f82859122ff298bcfcca78e821e8bfe437bb78))
* refactor: add background on popup table ([#446](https://github.com/microsoft/OCR-Form-Tools/commit/27f60df5617da2efba8ffdd601233e0c0f4c8e3e))
### 2.1-preview.1-79264e3 (07-24-2020)
* fix: handle rejection for security token not found when opening projects ([#441](https://github.com/microsoft/OCR-Form-Tools/commit/79264e3fddfb2c80b88bf8ca21df1e869082ffcf))
* fix: show more refined error message for model not found analysis error ([#454](https://github.com/microsoft/OCR-Form-Tools/commit/1cb4133dca0092559e7524dfad8c0bf54502dc81))
* feat: support group selection of words with drawn bounding box ([#447](https://github.com/microsoft/OCR-Form-Tools/commit/b4332a926b1925024a33731a90d303c0b171935b))
* feat: add apiVersion to telemetry ([#448](https://github.com/microsoft/OCR-Form-Tools/commit/55be5427e4a2f9c8cf393d446049527c55f841d4))
* fix: margin for filenames in asset preview ([#451](https://github.com/microsoft/OCR-Form-Tools/commit/fe8258f9c7ceba663a66708b19bc0e6556e777ad))
* docs: add telemetry disclaimer to readme ([#449](https://github.com/microsoft/OCR-Form-Tools/commit/87356a1cf6678bb9494e83178bf6282ca366921f))
### 2.1-preview.1-9b5b99d (07-23-2020)
* docs: add get-sas.png (https://github.com/microsoft/OCR-Form-Tools/commit/9b5b99d5468661481ae8165593d5a74471366429)
* doc: add a screenshot of getting SAS token (https://github.com/microsoft/OCR-Form-Tools/commit/87b1062125ed106ff73c036e33f1bf7a5f2c3def)
* fix: handle undefined error for pdf asset preview memory cleaning ([#442](https://github.com/microsoft/OCR-Form-Tools/commit/9b5b99d5468661481ae8165593d5a74471366429))
* fix: remove duplicate models in model composed model list ([#439](https://github.com/microsoft/OCR-Form-Tools/commit/7fcc9ccfdb6634326ddd6cbfe99b423300b94131))
* feat: enable internal telemetry ([#431](https://github.com/microsoft/OCR-Form-Tools/commit/41294c8aa19c82643fe0df669c21a0112668e0dd))
### 2.1-preview.1-f4b4d5d (07-21-2020)
* fix: use table for model selection info ([#438](https://github.com/microsoft/OCR-Form-Tools/commit/f4b4d5ded4b7e0ff2116ba3b8f97e49fbf30b7c0))
* fix: reset model name after training ([#434](https://github.com/microsoft/OCR-Form-Tools/commit/ed919a016b150d0938aee25b5550bacf29f04e83))
* fix: wait for loadeding project with sharing project ([#435](https://github.com/microsoft/OCR-Form-Tools/commit/fc4cb96d2a9d0920c3bbbd9c2000fb4b1b7ac9c0))
### 2.1-preview.1-46dbb2b (07-20-2020)
* fix: handle no recent models for model compose ([#432](https://github.com/microsoft/OCR-Form-Tools/commit/46dbb2be9ee6100a8f3e6a443ad5e734c60954bb))
* refactor: use new model compose icon ([#425](https://github.com/microsoft/OCR-Form-Tools/commit/932fb3fd7f84636e97035f4cafadc87cff18b3b3))
* fix: support long model names for model selection ([#427](https://github.com/microsoft/OCR-Form-Tools/commit/a0fa2daf4cd3286f7f58dc2919fd202115e8d5be))
* feat add recent models to top of model compose page's list ([#430](https://github.com/microsoft/OCR-Form-Tools/commit/cf8de6be61b95bfe8c937946df71ea81aecb35f9))
* fix: check valid connection ([#428](https://github.com/microsoft/OCR-Form-Tools/commit/9cb6c5830afddc9317ffdfe6927b581c4d39ba39))
### 2.1-preview.1-162a766 (07-17-2020)
* refactor: make confidence results same as JSON results ([#409](https://github.com/microsoft/OCR-Form-Tools/commit/162a7660cfe32b72c4954a147269c5d2b7f55a08))
* fix: prevent user from leaving page while composing ([#422](https://github.com/microsoft/OCR-Form-Tools/commit/63e179d0152d2f8f2ee764443785efa24e5f7dce))
* feat: support model selection ([#419](https://github.com/microsoft/OCR-Form-Tools/commit/b4c4cc5a8a980aaa6530e7a4a5a1c43e77494c75))
* feat: share project ([#344](https://github.com/microsoft/OCR-Form-Tools/commit/d059580cfefa053670c45c5d8ec7bf250bc4db27))
### 2.1-preview.1-89be3ac (07-15-2020)
* fix: on assetFormat undefined ([#413](https://github.com/microsoft/OCR-Form-Tools/commit/89be3ac5b614e91607d7fb8065ad32b69886040d))
* fix: make sure token names are unique ([#404](https://github.com/microsoft/OCR-Form-Tools/commit/d8fa6141cff4d00ba22e95ef4f5dcc9102e1c1c2))
* fix: model info enclosing element error on [#407](https://github.com/microsoft/OCR-Form-Tools/issues/407) ([#408](https://github.com/microsoft/OCR-Form-Tools/commit/8cc421c3fee0e781211efb0aeb2b345075012daa))
* fix: display composed icon for composed model with attribute ([#399](https://github.com/microsoft/OCR-Form-Tools/commit/18fb4d71052b9355c8d5a4f7dde956ba17ca30fa))
### 2.1-preview.1-b67191c (07-09-2020)
* fix: don't allow choosing not-ready models for compose ([#394](https://github.com/microsoft/OCR-Form-Tools/commit/b67191cdbc872b9004be30aa4b4dfde9a88dfe37))
* feat: track five most recent project models ([#395](https://github.com/microsoft/OCR-Form-Tools/commit/05850603d51a6786c8b6e8b4a553db020df56158))
### 2.1-preview.1-abc6376 (07-08-2020)
* feat: enable model info in analyze results ([#383](https://github.com/microsoft/OCR-Form-Tools/commit/abc63767e97dd28a6bb9028e03f2225e6ac0f1ab))
* fix: check invalid provider options before project actions ([#390](https://github.com/microsoft/OCR-Form-Tools/commit/212647d4327d9e18e9248a2d39086eeaab404979))
### 2.1-preview.1-a334cfc (07-07-2020)
* fix: hide extra scrollbars for model compose view ([#380](https://github.com/microsoft/OCR-Form-Tools/commit/a334cfc45fc5ab137682ad2b48dd0ec1585055dc))
* fix: handle version change state mutation error ([#382](https://github.com/microsoft/OCR-Form-Tools/commit/8991cc0c92f2f5cbd226f7e1c5c0825b7af8937c))
* fix: handle pdf worker terminated error ([#381](https://github.com/microsoft/OCR-Form-Tools/commit/adc0498c31bfd5ba57ab98c373e73575589ab1e1))
### 2.1-preview.1-7192170 (07-02-2020)
* feat: support release ([#361](https://github.com/microsoft/OCR-Form-Tools/commit/7192170d73d24a43e7fff18cd2c6bae7f208f1b0))
### 2.1-preview.1-978dabc (07-01-2020)
* feat: support document management ([#374](https://github.com/microsoft/OCR-Form-Tools/commit/978dabc3ba877ed4215865cba2a583fb785a2894))
### 2.1-preview.1-56a4b89 (06-30-2020)
* fix: wait until composed model is ready ([#369](https://github.com/microsoft/OCR-Form-Tools/commit/56a4b89f370f2fd72c6bc275376205e7fffe6a9e))
### 2.1-preview.1-6114d64 (06-23-2020)
* fix: update OCR version ([#335](https://github.com/microsoft/OCR-Form-Tools/commit/6114d6456b27a59335e534eef72cefd1b2f15737))
* feat: support electron for on premise solution ([#333](https://github.com/microsoft/OCR-Form-Tools/commit/ca0bd0c2ab46b7b587e5bfbc60c29b62bb325297))
### 2.1-preview.1-8297b18 (06-19-2020)
* refactor: put api version in constants ([#332](https://github.com/microsoft/OCR-Form-Tools/commit/8297b18a084be86bc4c986a1a332cb40bd807d1b))
### 2.1-preview.1-3b7f803 (06-18-2020)
* feat: enable model compose (preview) ([#328](https://github.com/microsoft/OCR-Form-Tools/commit/3b7f803407b82191706120bb9f12b82de1955704))
* fix: quick reordering tags ([#322](https://github.com/microsoft/OCR-Form-Tools/commit/3cc5267ef8617590adb3d4966f75cfed64604f00))
* feat: localization for canvas commandbar items ([#319](https://github.com/microsoft/OCR-Form-Tools/commit/253b9c90eb4923e7fde015a7216905fa32a8dcfa))
* feat: enable re-run OCR ([#297](https://github.com/microsoft/OCR-Form-Tools/commit/cbe9b0ed1c48f54c100b31b7f04706a969df2dd5))
* fix: capitalize python in analyze page ([#320](https://github.com/microsoft/OCR-Form-Tools/commit/96626636a96a3d19030df283ac794fa9c2aab18c))
* fix: fix spelling correction for string match ([#318](https://github.com/microsoft/OCR-Form-Tools/commit/28e53cefcf0bb462d547d6e38b24c480c03b946f))
* feature: keep prediction in UI ([#285](https://github.com/microsoft/OCR-Form-Tools/commit/dad98b9bd1d305a6bfeb2846ef4067da186ff801))
### 2.0.0-1c39800 (06-05-2020)
* feat: add description - how to delete info ([#292](https://github.com/microsoft/OCR-Form-Tools/commit/1c39800b1152f186dfc19834bb969abbc4fe0ac2))
* feat: enable download analyze script ([#304](https://github.com/microsoft/OCR-Form-Tools/commit/9c97ed0ff9b0aa72ec9a197fc92f3a5998135c36))
* fix: check ocrread results before getting image extent ([#296](https://github.com/microsoft/OCR-Form-Tools/commit/61dba02fc6f19eb854e1f499e475b1336e6171b9))
* feat: Add better error message for CORS ([#289](https://github.com/microsoft/OCR-Form-Tools/commit/8f210792b4d84e424b00499efb540b0e27e9fdad))
### 2.0.0-2760166 (05-30-2020)
* fix: fix mime check bug for jpeg/jpg and tiff ([#291](https://github.com/microsoft/OCR-Form-Tools/commit/2760166bcb809bbfdc207b01db49f00153318624))
* refactor: simplify shortcut descriptions ([#277](https://github.com/microsoft/OCR-Form-Tools/commit/db95b0e2510f6cef9bc7279fe0a19dce239c816e))
### 2.0.0-a5e4e07 (05-21-2020)
* feature: show table view when table icon is clicked ([#271](https://github.com/microsoft/OCR-Form-Tools/commit/a5e4e079d4c0d1c7c52e3b015c0ddf9b8601bbf2))
### 2.0.0-814276a (05-20-2020)
* fix: modify skip button according to feedback comments ([#264](https://github.com/microsoft/OCR-Form-Tools/commit/814276af6f4259844854798adf0c56bd606b2363))
* feature: keyboard shortcuts and tips ([#258](https://github.com/microsoft/OCR-Form-Tools/commit/37aa859a80dc0213a118313558ad21ba424008e7))
* feat: add electron mode from VoTT project ([#260](https://github.com/microsoft/OCR-Form-Tools/commit/2a3383d4a0f100a39ed40627bdffb9b48f78f5df))
* refactor: use forEach instead of map in handleFeatureSelect ([#259](https://github.com/microsoft/OCR-Form-Tools/commit/c1c590c463743d187fda2429a628e27c6c42012f))
### 2.0.0-0061645 (05-13-2020)
* build: update nginx base image version to 1.18.0-alpine ([#255](https://github.com/microsoft/OCR-Form-Tools/commit/0061645871806595e4fe2ab5991cc494afa26b31))
* fix: assign empty string when predict item's fieldName is undefined ([#254](https://github.com/microsoft/OCR-Form-Tools/commit/d4d919f678b1f162f48c87ee5223281e57945a0a))
* fix: overlaping left split pane ([#252](https://github.com/microsoft/OCR-Form-Tools/commit/2e8c351f74c385b8627ee6ea39f974e5e048ea8d))
* refactor: change predict to analyze in UI while keeping predict term ([#147](https://github.com/microsoft/OCR-Form-Tools/commit/c9aa58e36a10a35083249a8080c2cfb9fccf3733))
### 2.0.0-7c7ba93 (05-07-2020)
* fix: check null value from post processed value ([#248](https://github.com/microsoft/OCR-Form-Tools/commit/a361189c527bfffd6417f90a2521ad40b2b3f205))
* feat: enable outputting to file for analyze script ([#246](https://github.com/microsoft/OCR-Form-Tools/commit/7c7ba937f140490775b788d63ef2c7ed63ca40f1))
### 2.0.0-9d91800 (05-06-2020)
* fix: prevent user from changing tag types when invalid ([#224](https://github.com/microsoft/OCR-Form-Tools/commit/d8823a33591db5c5dc9a0af753e007167218a3e3))
* fix: prevent user from adding multiple checkboxes to a single tag ([#224](https://github.com/microsoft/OCR-Form-Tools/commit/d8823a33591db5c5dc9a0af753e007167218a3e3))
* fix: display error when inputted SAS doesn't contain token ([#243](https://github.com/microsoft/OCR-Form-Tools/commit/9826ca8504549f23057c9cad1baebc5e9d1f6fe7))
### 2.0.0-25d3298 (05-04-2020)
* feat: track document count for tags ([#231](https://github.com/microsoft/OCR-Form-Tools/commit/70a6e43dc54239cdc153d5d328b17c1dfa0f085f))
* fix: display error when inputted service URI contains path or query ([#234](https://github.com/microsoft/OCR-Form-Tools/commit/04a16961b37ad5b5d01fc4c93addaaf69cbf0e72))
* feat: add link in status bar to CHANGELOG ([#233](https://github.com/microsoft/OCR-Form-Tools/commit/e66646a13263239213580378bbd2d8462d7e22b6))
### 2.0.0-f6c8ffa (05-01-2020)
* refactor: change checkbox to selectionMark ([#223](https://github.com/microsoft/OCR-Form-Tools/commit/f6c8ffad6edf23f6241f314e9456da92bc1a8402))
### 2.0.0-f3e42f6 (04-30-2020)
* feat: display post-processed value in analyzed results ([#229](https://github.com/microsoft/OCR-Form-Tools/commit/f3e42f6e8e9e934f1a241921dbe4a1e8d311bb46))
### 2.0.0-f068866 (04-28-2020)
* fix: hide sprin in tag input control when open an empty folder ([#220](https://github.com/microsoft/OCR-Form-Tools/commit/f0688668df2e676fce9749fad8ec9d39e56697cf))
* perf: cache images, reduce canvas size, and fix memory leak for asset preview ([#218](https://github.com/microsoft/OCR-Form-Tools/commit/e8ad9a3bebf2a1ae210e0e1fa3eebba564592c4c))
### 2.0.0-595a512 (04-24-2020)
* fix: align rotated picture asset with OCR result
### 2.0.0-51c02cc (04-20-2020)
* fix: scrollbar fix when page size changes
* fix: Add split pane to fix too long tag name is invisible in right sidebar
### 2.0.0-202fb2f (04-20-2020)
* perf: improve assets loading performance and fix some bugs
### 2.0.0-bce554e (04-16-2020)
* perf: improve Azure Blob file list performance
* feat: support URL upload for predicting file
### 2.0.0-ef18425 (04-09-2020)
* feat: enable checkbox labeling (preview)
# FoTT Changelog
## What's new in Form Recognizer?
Click [here](https://docs.microsoft.com/en-us/azure/cognitive-services/form-recognizer/whats-new) to see what's new in Form Recognizer.
## Released conatiner's currently referenced commit
2.1-Preview's released container image, tracked by the `latest-preview` image tag in our [docker hub repository](https://hub.docker.com/_/microsoft-azure-cognitive-services-custom-form-labeltool), currently references **2.1-preview.1-1f33130 (10-09-2020)**
## Commit history
### 2.1-preview.2-b6b9a2f (12-10-2020)
* update appVersion to 2.1.2 ([#808](https://github.com/microsoft/OCR-Form-Tools/commit/b6b9a2f131485d08541a1e85f6af59ebfbeca773))
* add locale in prebuiltPredictPage ([#772](https://github.com/microsoft/OCR-Form-Tools/commit/06d9c16a7c1fe64a95d835878dcb8dabb8c7e485)) ([#776](https://github.com/microsoft/OCR-Form-Tools/commit/06d9c16a7c1fe64a95d835878dcb8dabb8c7e485))
* Stew ro/cherry pick 347e21e 2b6ead7 ([#766](https://github.com/microsoft/OCR-Form-Tools/commit/ca59cee26587e1aee49507abd75c0683d67f541f))
* Cherry pick 34ce14d a7ccb34 ([#763](https://github.com/microsoft/OCR-Form-Tools/commit/244c23df700791794990eb6f7e196bcf9ff9c844))
* Stew ro/cherry pick ab5a8a8 abfffbb ([#760](https://github.com/microsoft/OCR-Form-Tools/commit/93b7a2d4d7688cda4baa4cfd704b2666df524174))
* refactor: disable api version selection ([#755](https://github.com/microsoft/OCR-Form-Tools/commit/be1f18db0b7073dad106f443f343aa221dea7fc6))
* refactor: disable draw region button ([#756](https://github.com/microsoft/OCR-Form-Tools/commit/8816a85761a795f3f0b34b87360236cadd80a735))
* feat: support null text values in analyze results ([#744](https://github.com/microsoft/OCR-Form-Tools/commit/0ddb7f1275d6f195c2af9f0b7053987e01a5d677))
* feat: support rowspan and column span for layout tables ([#754](https://github.com/microsoft/OCR-Form-Tools/commit/6994ac929146e25370d1461e4e502773de3d5503))
* Update changelog ([#750](https://github.com/microsoft/OCR-Form-Tools/commit/acba3966ce960e474dfd3d97510b07c108e7b39f))
* check whether the label data is null ([#753](https://github.com/microsoft/OCR-Form-Tools/commit/f6ef41ac1500e52f3714eda6e99187e016e1b223))
* fix issue of 773 ([#740](https://github.com/microsoft/OCR-Form-Tools/commit/9d35e79393cdb0de678e9ec6b850daf5a4df5c96))
### 2.1-preview.1-2e50498 (11-09-2020)
* fix: enable api version selection ([#736](https://github.com/microsoft/OCR-Form-Tools/commit/2e5049883bd1550ba80210edca7db4233d7a15fa))
* fix: labeling doesn't work via shortcuts on the new project or empty tags ([#677](https://github.com/microsoft/OCR-Form-Tools/commit/f11291940b776ceb8ba7708e6f58dc2572f7b01b))
* fix: remove setting project state in project form on change ([#732](https://github.com/microsoft/OCR-Form-Tools/commit/25eb59bfa85b755cd877b02ffda71d0cec70a106))
* handle training state logical ([#731](https://github.com/microsoft/OCR-Form-Tools/commit/569adf161ad89106ab1fbf51429841c5955e0e4b))
* fix issue of "After running Layout an all documents FoTT sometimes does not ends" ([#723](https://github.com/microsoft/OCR-Form-Tools/commit/6203e2cd814c95e2bef165f3fb518a566166c26d))
* set includeTextDetails=true in prebuilt predict ([#722](https://github.com/microsoft/OCR-Form-Tools/commit/ba04cebb63a5b05a369ba954a99dfdc7c9bb9b41))
* fix issue of "Auto-labeling while switching assets in asset preview causes an error" ([#721](https://github.com/microsoft/OCR-Form-Tools/commit/59fe4e2778a644335da9766fd1382d56086220c1))
* feat: support api version config ([#717](https://github.com/microsoft/OCR-Form-Tools/commit/c81b2323aaa2b26b0bc0f7922de1e12445fbb627))
* update homepage style ([#724](https://github.com/microsoft/OCR-Form-Tools/commit/fc769f41c169083098f9250c9ce18ca4881cc336))
* issuefix: update getBoundingBox ([#730](https://github.com/microsoft/OCR-Form-Tools/commit/f0cb5db337364b2f0355928616d1f7d9637a454a))
* clone with lodash cloneDeep ([#728](https://github.com/microsoft/OCR-Form-Tools/commit/d6bca5fcf2262a467ede781916a01f50d805b30f))
* remain auto label state while no label data ([#727](https://github.com/microsoft/OCR-Form-Tools/commit/a79d556a3935e387b0798ecfda8de9c8b1538250))
* deep copy asset metadata ([#725](https://github.com/microsoft/OCR-Form-Tools/commit/ba8c1100e9adf517e35ec50fd513383d9e84d630))
* Yongbing chen/receipt predicting ([#626](https://github.com/microsoft/OCR-Form-Tools/commit/e638cd8e3be8926e966a5afc86fb53ac0f092977))
### 2.1-preview.1-32cfaea (11-06-2020)
* Starain chen/clean autolabel data while training ([#712](https://github.com/microsoft/OCR-Form-Tools/commit/32cfaea023e96c8aa00560a3f30134683ee25757))
* fix issue of deleting tag ([#703](https://github.com/microsoft/OCR-Form-Tools/commit/282d55700ea9fdf4cac2b0f20901e8ff6115819e))
### 2.1-preview.1-c7ed086 (11-04-2020)
* Update README.md([#??](https://github.com/microsoft/OCR-Form-Tools/commit/c7ed08612876af8bb619a080f6740fceabb4e67c))
* Update README.md([#??](https://github.com/microsoft/OCR-Form-Tools/commit/d696b8a25438590fb44c5159b3142b17178f25d2))
* fix: use constant if no api version specified ([#684](https://github.com/microsoft/OCR-Form-Tools/commit/8ccdab83f079d976f6521bc08c50d917900483c0))
* auto labeled tag design & replacing between text with draw region ([#670](https://github.com/microsoft/OCR-Form-Tools/commit/757e0dd85b3c69c6642674e48e9d3549807fecbd))
### 2.1-preview.1-aab6938 (11-03-2020)
* Fix the issue that git-commit-info.txt could be override ([#683](https://github.com/microsoft/OCR-Form-Tools/commit/aab69380a8e1f7f113011a7c6b6ed406c4329555))
* fix: use existing git hash when not in git repository ([#682](https://github.com/microsoft/OCR-Form-Tools/commit/586fbb0ce51c27ae42ca857a372e8e8d5dea21d1))
* Stew ro/use api version selected in project settings ([#678](https://github.com/microsoft/OCR-Form-Tools/commit/bed69a3f64b0da7590ca3c54e8de369844c6bcd9))
* refactor: change drawn region icon ([#675](https://github.com/microsoft/OCR-Form-Tools/commit/5614da2681bb8fadf9d3db3ff95aa62362d00175))
### 2.1-preview.1-3485d33 (10-30-2020)
* feat: add bmp support for analyze page ([#672](https://github.com/microsoft/OCR-Form-Tools/commit/3485d33eca96321cf667c5c8eba22cc60af42e23))
* Alex krasn/bugfix on hotkeys when canvas not loaded yet ([#664](https://github.com/microsoft/OCR-Form-Tools/commit/b0404c6276f8fe55292c929e2ca431ed31ef6442))
### 2.1-preview.1-7166cda (10-29-2020)
* fix: use node to update status bar with latest git commit ([#671](https://github.com/microsoft/OCR-Form-Tools/commit/7166cdae5763a93feee52842af8e2246fedbf818))
* change OCR to Layout in UI (Actions) ([#666](https://github.com/microsoft/OCR-Form-Tools/commit/ac604b6bd43eb4c3ba8929a97b308c833d0e6c13))
* Yongbing chen/hitl update notify message ([#651](https://github.com/microsoft/OCR-Form-Tools/commit/0fa559a4b28c6648eaa17ec047ebb9caabbdc9c7))
### 2.1-preview.1-6d775ae (10-27-2020)
* Yongbing chen/ui adjustment with designers feedback ([#662](https://github.com/microsoft/OCR-Form-Tools/commit/6d775ae8d4495ca31d110e500b86d3c0eed6a954))
### 2.1-preview.1-c86b6de (10-23-2020)
* Fix the issue that git-commit-info.txt could be override ([#668](https://github.com/microsoft/OCR-Form-Tools/commit/c86b6de35ecd5d004dfb64f8f857d06f0557a00d))
* Xinxl/fix hash ([#667](https://github.com/microsoft/OCR-Form-Tools/commit/cb27cbd74ff890dc7e13865d89ca5e16b0807fbb))
### 2.1-preview.1-0aae169 (10-22-2020)
* Alex krasn/fix confidence level bar styles ([#657](https://github.com/microsoft/OCR-Form-Tools/commit/0aae1690351f3de27114e6cbebd2c077be8e9016))
### 2.1-preview.1-d644459 (10-21-2020)
* refactor: change error styling and wording for project sharing ([#653](https://github.com/microsoft/OCR-Form-Tools/commit/d644459e4c9b1f82b1ed2d5b537960b0f16184da))
* fix: sort models after loading next page in model compose ([#659](https://github.com/microsoft/OCR-Form-Tools/commit/9818d6301ef613155951381598f9ad4cf8ff6e3c))
* Alex krasn/serialize javascript vulnerability ([#612](https://github.com/microsoft/OCR-Form-Tools/commit/66b03303b1325634371ebdb3923acaa6722be89f))
* update asset labelingState when load local project ([#660](https://github.com/microsoft/OCR-Form-Tools/commit/1aa3daaeeb1c8a4773e7b6236fc6462335e410f9))
### 2.1-preview.1-28c54fc (10-20-2020)
* fix: check for local connections ([#654](https://github.com/microsoft/OCR-Form-Tools/commit/28c54fcc31defe1c4ebcf685675768b99c8e00c8))
* get last commit hash code in current branch and show on status bar ([#642](https://github.com/microsoft/OCR-Form-Tools/commit/88c547995d31f945177da70141f997e441b3259c))
* new feature: tags in current page ([#640](https://github.com/microsoft/OCR-Form-Tools/commit/af5396fe8e63b88b90d16953e17ce2006afe782e))
### 2.1-preview.1-6c1ee2b (10-16-2020)
* adjust editor view offset ([#646](https://github.com/microsoft/OCR-Form-Tools/commit/6c1ee2b6b4f1bcf28b1c9081b21f0a8783518c80))
### 2.1-preview.1-b92e4b3 (10-15-2020)
* reword asset states ([#644](https://github.com/microsoft/OCR-Form-Tools/commit/b92e4b3d5a786a852c319c05697eea331c147cee))
### 2.1-preview.1-4544e52 (10-14-2020)
* feat: support apiVersion selection from project settings ([#641](https://github.com/microsoft/OCR-Form-Tools/commit/4544e5255cf2356a4ddf353f7a63994c1a0865da))
### 2.1-preview.1-94f12bb (10-13-2020)
* new feature: highlight current tag ([#628](https://github.com/microsoft/OCR-Form-Tools/commit/94f12bb4e925a86fdfba8e25d8b0346169daea1e))
* new feature: human in the loop auto labeling ([#571](https://github.com/microsoft/OCR-Form-Tools/commit/c1f227daa3decd52320f58d151755b206280cedd))
### 2.1-preview.1-7d1f871 (10-10-2020)
* Update CHANGELOG.md([#??](https://github.com/microsoft/OCR-Form-Tools/commit/7d1f87193b3917f2140ab9bcce04c64e7aceb823))
### 2.1-preview.1-1f33130 (10-09-2020)
* fix: support image map interactions for container releases([#639](https://github.com/microsoft/OCR-Form-Tools/commit/e015973aee152b8a8b22fc2fe32ce80bdd2b46ea))
### 2.1-preview.1-6d4e93b (10-07-2020)
* Fix: use file type library for mime type validation ([#636](https://github.com/microsoft/OCR-Form-Tools/commit/6d4e93bca8a4e3d677c765ed5596bde502766e2e))
### 2.1-preview.1-355ca0b (09-30-2020)
* feat: add spinner in saving project, can avoid multiple commit ([#617](https://github.com/microsoft/OCR-Form-Tools/commit/355ca0b156b2d44aafd2eaaccf2fc52385c7f5f8))
### 2.1-preview.1-53044f7 (09-29-2020)
* fix: refresh currentProjects when load project ([#615](https://github.com/microsoft/OCR-Form-Tools/commit/53044f72dd9c9c72557c74c00605ba05ee50205d))
* sync related region color when tag color changed ([#598](https://github.com/microsoft/OCR-Form-Tools/commit/3044cc51a9166877bb4f01f28753171b82c04ccd))
* feat: add current list item style ([#601](https://github.com/microsoft/OCR-Form-Tools/commit/3e503e75513e44e6a90bd013d8dd15c3096cd7e9))
* fix: remove project from app if security token does not exist ([#468](https://github.com/microsoft/OCR-Form-Tools/commit/730e1963a06f038a4efa9750fcef4be6f15a8460))
### 2.1-preview.1-d859d38 (09-27-2020)
* fix ,update document state when preview (#317) ([#471](https://github.com/microsoft/OCR-Form-Tools/commit/d859d38ecc1f96b194ffa130a1840f5a7d9b1a9b))
* refactor: change the confidence value format to percentage ([#461](https://github.com/microsoft/OCR-Form-Tools/commit/e806b4e0dfcc68e6408e2130a46a318637a482a8))
### 2.1-preview.1-7a3f7a7 (09-25-2020)
* security: upgrade node-forge ([#622](https://github.com/microsoft/OCR-Form-Tools/commit/7a3f7a773c8b01f443afaad89d7974a5bbb0b869))
* fix: disable move tag and support renaming when searching ([#618](https://github.com/microsoft/OCR-Form-Tools/commit/cac1e8e6cfb2805a6540f9e80d564a0ff8be81c7))
### 2.1-preview.1-4163edc (09-23-2020)
* docs: add latest tag reference to changelog ([#608](https://github.com/microsoft/OCR-Form-Tools/commit/4163edc18bc65234e263703fc829d2f297953385))
* fix: use region instead of drawnRegion for labelType in label file ([#582](https://github.com/microsoft/OCR-Form-Tools/commit/ffafc200249a1c47698fedb279b4b55cef0190ba))
* docs: update readme with docker hub info ([#604](https://github.com/microsoft/OCR-Form-Tools/commit/63bbea076d598d0286095fa0eca48d8c9d0ed706))
* fix: remove opening browser for yarn start ([#605](https://github.com/microsoft/OCR-Form-Tools/commit/f6c4dc3585df71d09252a28f65e835a594389118))
* fix: update changelog updater script ([#607](https://github.com/microsoft/OCR-Form-Tools/commit/7c4848c3a72259562c0461f0e2eadfb4a660fa64))
### 2.1-preview.1-f2db74e (09-17-2020)
* docs: udpate changlog with docker image reference ([#590](https://github.com/microsoft/OCR-Form-Tools/commit/f2db74e322c32338eba3b2df06c01a51cfb7ebc1))
### 2.1-preview.1-1a6b78e (09-16-2020)
* fix: normalize folder path starting with a period ([#592](https://github.com/microsoft/OCR-Form-Tools/commit/1a6b78e054235da3188aafbe65636a8c18b439bf))
* fix: change label folder uri title ([#588](https://github.com/microsoft/OCR-Form-Tools/commit/7e4233e568d94817e23dda5ef5513b9ee7475d11))
### 2.1-preview.1-6a1ced5 (09-15-2020)
* fix: initialize drag pan for analyze page ([#586](https://github.com/microsoft/OCR-Form-Tools/commit/6a1ced5a0bfb03ceba515faddbfa010ac8451460))
* fix: zoomIn keyboar shortcut for macOS ([#581](https://github.com/microsoft/OCR-Form-Tools/commit/5afeebfee28e10e390f073990f90348c5117475f))
* fix: appId ([#584](https://github.com/microsoft/OCR-Form-Tools/commit/e053b151441e956641ed05c29106d02358a40792))
* fix: remove escape quote from release script ([#579](https://github.com/microsoft/OCR-Form-Tools/commit/bd5d51e8e15809b95f15bc495f7d0f91fecfc22d))
* Stew ro/support drag pan for release ([#576](https://github.com/microsoft/OCR-Form-Tools/commit/77620eccd21d564473c81b43341f59de22339248))
### 2.1-preview.1-0633507 (09-14-2020)
* Update README.md([#??](https://github.com/microsoft/OCR-Form-Tools/commit/0633507aa767f996add313ced06c2365c5f240c8))
### 2.1-preview.1-8d2286f (09-13-2020)
* persist trainPage inputs in localStorage ([#568](https://github.com/microsoft/OCR-Form-Tools/commit/8d2286f50236e41fe5540dbb9b161ea88bbf2d7a))
### 2.1-preview.1-bb23e31 (09-11-2020)
* build(deps): bump node-fetch from 2.6.0 to 2.6.1 ([#575](https://github.com/microsoft/OCR-Form-Tools/commit/bb23e3199c5721338241c8c5ccc0bda104fd15f8))
* fix: support multiple env files ([#574](https://github.com/microsoft/OCR-Form-Tools/commit/cf64a8ddde05e7e73cad37d271f5d6dfa61c5d7f))
* fix: "Azure blob storage" error on on premise scenario ([#572](https://github.com/microsoft/OCR-Form-Tools/commit/46f0bc59f3a531c366bc2c7cec955d2cb6ed7cd6))
* fix ([#563](https://github.com/microsoft/OCR-Form-Tools/commit/28c792e10692e3cc1f511852ffc9fdbc8dcdda8a))
### 2.1-preview.1-7e828ff (09-10-2020)
* fix: allow training with placeholder ([#569](https://github.com/microsoft/OCR-Form-Tools/commit/7e828ff02ff8a22b64b4b7d16787d77afb76af62))
* docs: update changelog ([#564](https://github.com/microsoft/OCR-Form-Tools/commit/1dec72c5df3206554a1e0864b65cc769835785fd))
* Yongbing chen/human in the loop ([#517](https://github.com/microsoft/OCR-Form-Tools/commit/be9d56481510e3033bcd705743c1ee9aeee20522))
* fix: support project folder in project settings for local file system ([#559](https://github.com/microsoft/OCR-Form-Tools/commit/b92b73bb8076f9b9bb55dd38fcd223b7b93eaa2e))
* feat: enable canvas rotation ([#553](https://github.com/microsoft/OCR-Form-Tools/commit/c27a110251df1fc7a595524846e20fd09c79f915))
* fix: handle tag is undefined error ([#557](https://github.com/microsoft/OCR-Form-Tools/commit/7e4d3fbbc3a2bf925c126cd2b3f493cca48e7a62))
### 2.1-preview.1-193520e (09-08-2020)
* fix: accept selection of only .fott files for open local project ([#554](https://github.com/microsoft/OCR-Form-Tools/commit/193520e2c3e40b58c3612507efc2249aaf4e9d05))
* fix: use default shared folder for label URI when training ([#551](https://github.com/microsoft/OCR-Form-Tools/commit/656de2ff07c2083affc2adf52f1a56c5a9c024b8))
* fix: show label folder uri while training ([#539](https://github.com/microsoft/OCR-Form-Tools/commit/0ad389c06328cd6428653bb7d94d5af716e02ab7))
* feat: add canvas command bar to analyze page with only zoom buttons ([#549](https://github.com/microsoft/OCR-Form-Tools/commit/895b52740cd1e8d61b5b021e6c0d992f44ce8052))
### 2.1-preview.1-4852c84 (09-05-2020)
* fix buttons styles - makes them more visible ([#526](https://github.com/microsoft/OCR-Form-Tools/commit/4852c8429d25b5569c3335b014da5972cbcc6162))
### 2.1-preview.1-343ea16 (09-04-2020)
* refactor: remove array for drawn region labels ([#542](https://github.com/microsoft/OCR-Form-Tools/commit/343ea16e18199ab5098395ae8b7a164cd8bab55e))
* fix: add key prop to region icon ([#540](https://github.com/microsoft/OCR-Form-Tools/commit/87b69093f2d35d91a2a939c46ac66ba4d22a5cb7))
### 2.1-preview.1-b370c9a (09-02-2020)
* fix: resize canvas on asset preview resize ([#535](https://github.com/microsoft/OCR-Form-Tools/commit/b370c9a9bcf7da416944c626f6d4fd7bd29088bb))
### 2.1-preview.1-de1c304 (08-31-2020)
* refactor: upgrade tsconfig es2017 to esnext ([#531](https://github.com/microsoft/OCR-Form-Tools/commit/de1c30410b860c9576108f076ec4dd8273e61a79))
### 2.1-preview.1-530545c (08-28-2020)
* fix: remove existing bounding boxes from document on analyze ([#523](https://github.com/microsoft/OCR-Form-Tools/commit/6a1aedfb89b0499a0f4782e16ccbd8a06887841d))
* feat: enable download JSON of trained model ([#513](https://github.com/microsoft/OCR-Form-Tools/commits/master))
### 2.1-preview.1-529a0e8 (08-27-2020)
* fix: show loading indicator while loading model info ([#514](https://github.com/microsoft/OCR-Form-Tools/commit/529a0e819f4cb405e290f34d18d15c487a7bcfad))
* docs: update telemetry disclaimer ([#521](https://github.com/microsoft/OCR-Form-Tools/pull/521))
* fix: disable clearing of drawn regions on analyze page ([#518](https://github.com/microsoft/OCR-Form-Tools/commit/298d7c97da1278996d2ee6020d3face0785bc4eb))
### 2.1-preview.1-b2d9a0b (08-26-2020)
* docs: notice that telemetry is disabled ([#501](https://github.com/microsoft/OCR-Form-Tools/commit/b2d9a0b008ebf350dfcb5fe897fc5dfe0d4d5cb6))
### 2.1-preview.1-d9db4ee (08-24-2020)
* refactor: upgrade storage-blob to v12.1.2 ([#509](https://github.com/microsoft/OCR-Form-Tools/commit/d9db4ee027240a82feef5b54e5e406c3793d8050))
* feat: support region labeling ([#481](https://github.com/microsoft/OCR-Form-Tools/commit/dd78ed06761a341908bdb1b09e73fd1f2868431c))
* feat: support adding model to recent models from compose page ([#510](https://github.com/microsoft/OCR-Form-Tools/commit/65fc92b5737ceea14ff89aa78052be26835ad0ae))
### 2.1-preview.1-2402cba (08-17-2020)
* fix: notify error message when open project with invalid security token ([#506](https://github.com/microsoft/OCR-Form-Tools/commit/2402cbaf73eba47ad188f851227c04cd44a208d4))
### 2.1-preview.1-a8ef8fa (08-17-2020)
* fix: don't allow create or update connection with duplicate name ([#486](https://github.com/microsoft/OCR-Form-Tools/commit/a8ef8fab603b3d2c08c533cb5dfe67da117942a0))
### 2.1-preview.1-530545c (08-14-2020)
* fix: "failed to fetch()" error ([#491](https://github.com/microsoft/OCR-Form-Tools/commit/530545c7cd2b4a3ff444e9c7e1f40c68d4a7376c))
* fix: sync layer visibility ([#497](https://github.com/microsoft/OCR-Form-Tools/commit/bea552b28acb9b652ffaedf40009d6df5a3197ef))
* refactor: disable telemetry service ([#498](https://github.com/microsoft/OCR-Form-Tools/commit/6e3628cf174f954693380aab6ebd2dabe027ac6d))
* fix: change share class name for adblocker chrome extension ([#492](https://github.com/microsoft/OCR-Form-Tools/commit/aa8a73afc6344f3164e79f236d5fa4bb0f64d364))
### 2.1-preview.1-da405b3 (08-10-2020)
* fix: restrict tag type through hot keys ([#482](https://github.com/microsoft/OCR-Form-Tools/commit/da405b354428b829e895a35a020736b1d88c153f))
* docs: add share project description to README ([#488](https://github.com/microsoft/OCR-Form-Tools/commit/7ee215f735a84aaa30201748d19207bcc6a05580))
### 2.1-preview.1-29d1f93 (08-07-2020)
* fix: handle multi selection of non-compatible types with multi-selection tool ([#487](https://github.com/microsoft/OCR-Form-Tools/commit/29d1f93a290e55fdd84f8cf2ee9a914fed702beb))
### 2.1-preview.1-cef225f (08-06-2020)
* fix: handle undefined image map error ([#462](https://github.com/microsoft/OCR-Form-Tools/commit/cc9e9bfc8fe00bb0ed154edb791446f28060af4e))
* fix: handle undefined image map error ([#479](https://github.com/microsoft/OCR-Form-Tools/commit/cef225f3346628e79c46e799303400965f1d3c96))
### 2.1-preview.1-76945df (08-05-2020)
* fix: use english for telemetry reporting ([#472](https://github.com/microsoft/OCR-Form-Tools/commit/76945df3bdf9caba3ba13f4541e17e75b9574b33))
* fix: resolve unhandled exeptions and new message for OCR service on 400 ([#470](https://github.com/microsoft/OCR-Form-Tools/commit/76381bc659a365ead19387b933485530d2d5edc3))
* feature: enable popup with composed model info ([#460](https://github.com/microsoft/OCR-Form-Tools/commit/c1f5d803f047e5ca0d18fea6383b3baf56d116ff))
### 2.1-preview.1-f4d53ce (08-03-2020)
* fix: bump elliptic from 6.5.2 to 6.5.3 ([#469](https://github.com/microsoft/OCR-Form-Tools/commit/f4d53cec967194445885bd3748096f0a3ce10715))
* feat: add modelCompose icon and created time ([#466](https://github.com/microsoft/OCR-Form-Tools/commit/2fa32ef5f77ec7bb44bf42e9fc0a5fdf7f0330c3))
### 2.1-preview.1-78996ea (07-31-2020)
* refactor: relocate share button ([#464](https://github.com/microsoft/OCR-Form-Tools/commit/78996ea65616b28d7471b59f4f16f254d7d33127))
### 2.1-preview.1-0e1b637 (07-29-2020)
* feat: show only ready models in the list ([#459](https://github.com/microsoft/OCR-Form-Tools/commit/0e1b637003f289c56955342f44963003c1543436))
### 2.1-preview.1-84f8285 (07-27-2020)
* fix: show message on model composition fail ([#457](https://github.com/microsoft/OCR-Form-Tools/commit/84f82859122ff298bcfcca78e821e8bfe437bb78))
* refactor: add background on popup table ([#446](https://github.com/microsoft/OCR-Form-Tools/commit/27f60df5617da2efba8ffdd601233e0c0f4c8e3e))
### 2.1-preview.1-79264e3 (07-24-2020)
* fix: handle rejection for security token not found when opening projects ([#441](https://github.com/microsoft/OCR-Form-Tools/commit/79264e3fddfb2c80b88bf8ca21df1e869082ffcf))
* fix: show more refined error message for model not found analysis error ([#454](https://github.com/microsoft/OCR-Form-Tools/commit/1cb4133dca0092559e7524dfad8c0bf54502dc81))
* feat: support group selection of words with drawn bounding box ([#447](https://github.com/microsoft/OCR-Form-Tools/commit/b4332a926b1925024a33731a90d303c0b171935b))
* feat: add apiVersion to telemetry ([#448](https://github.com/microsoft/OCR-Form-Tools/commit/55be5427e4a2f9c8cf393d446049527c55f841d4))
* fix: margin for filenames in asset preview ([#451](https://github.com/microsoft/OCR-Form-Tools/commit/fe8258f9c7ceba663a66708b19bc0e6556e777ad))
* docs: add telemetry disclaimer to readme ([#449](https://github.com/microsoft/OCR-Form-Tools/commit/87356a1cf6678bb9494e83178bf6282ca366921f))
### 2.1-preview.1-9b5b99d (07-23-2020)
* docs: add get-sas.png (https://github.com/microsoft/OCR-Form-Tools/commit/9b5b99d5468661481ae8165593d5a74471366429)
* doc: add a screenshot of getting SAS token (https://github.com/microsoft/OCR-Form-Tools/commit/87b1062125ed106ff73c036e33f1bf7a5f2c3def)
* fix: handle undefined error for pdf asset preview memory cleaning ([#442](https://github.com/microsoft/OCR-Form-Tools/commit/9b5b99d5468661481ae8165593d5a74471366429))
* fix: remove duplicate models in model composed model list ([#439](https://github.com/microsoft/OCR-Form-Tools/commit/7fcc9ccfdb6634326ddd6cbfe99b423300b94131))
* feat: enable internal telemetry ([#431](https://github.com/microsoft/OCR-Form-Tools/commit/41294c8aa19c82643fe0df669c21a0112668e0dd))
### 2.1-preview.1-f4b4d5d (07-21-2020)
* fix: use table for model selection info ([#438](https://github.com/microsoft/OCR-Form-Tools/commit/f4b4d5ded4b7e0ff2116ba3b8f97e49fbf30b7c0))
* fix: reset model name after training ([#434](https://github.com/microsoft/OCR-Form-Tools/commit/ed919a016b150d0938aee25b5550bacf29f04e83))
* fix: wait for loadeding project with sharing project ([#435](https://github.com/microsoft/OCR-Form-Tools/commit/fc4cb96d2a9d0920c3bbbd9c2000fb4b1b7ac9c0))
### 2.1-preview.1-46dbb2b (07-20-2020)
* fix: handle no recent models for model compose ([#432](https://github.com/microsoft/OCR-Form-Tools/commit/46dbb2be9ee6100a8f3e6a443ad5e734c60954bb))
* refactor: use new model compose icon ([#425](https://github.com/microsoft/OCR-Form-Tools/commit/932fb3fd7f84636e97035f4cafadc87cff18b3b3))
* fix: support long model names for model selection ([#427](https://github.com/microsoft/OCR-Form-Tools/commit/a0fa2daf4cd3286f7f58dc2919fd202115e8d5be))
* feat add recent models to top of model compose page's list ([#430](https://github.com/microsoft/OCR-Form-Tools/commit/cf8de6be61b95bfe8c937946df71ea81aecb35f9))
* fix: check valid connection ([#428](https://github.com/microsoft/OCR-Form-Tools/commit/9cb6c5830afddc9317ffdfe6927b581c4d39ba39))
### 2.1-preview.1-162a766 (07-17-2020)
* refactor: make confidence results same as JSON results ([#409](https://github.com/microsoft/OCR-Form-Tools/commit/162a7660cfe32b72c4954a147269c5d2b7f55a08))
* fix: prevent user from leaving page while composing ([#422](https://github.com/microsoft/OCR-Form-Tools/commit/63e179d0152d2f8f2ee764443785efa24e5f7dce))
* feat: support model selection ([#419](https://github.com/microsoft/OCR-Form-Tools/commit/b4c4cc5a8a980aaa6530e7a4a5a1c43e77494c75))
* feat: share project ([#344](https://github.com/microsoft/OCR-Form-Tools/commit/d059580cfefa053670c45c5d8ec7bf250bc4db27))
### 2.1-preview.1-89be3ac (07-15-2020)
* fix: on assetFormat undefined ([#413](https://github.com/microsoft/OCR-Form-Tools/commit/89be3ac5b614e91607d7fb8065ad32b69886040d))
* fix: make sure token names are unique ([#404](https://github.com/microsoft/OCR-Form-Tools/commit/d8fa6141cff4d00ba22e95ef4f5dcc9102e1c1c2))
* fix: model info enclosing element error on [#407](https://github.com/microsoft/OCR-Form-Tools/issues/407) ([#408](https://github.com/microsoft/OCR-Form-Tools/commit/8cc421c3fee0e781211efb0aeb2b345075012daa))
* fix: display composed icon for composed model with attribute ([#399](https://github.com/microsoft/OCR-Form-Tools/commit/18fb4d71052b9355c8d5a4f7dde956ba17ca30fa))
### 2.1-preview.1-b67191c (07-09-2020)
* fix: don't allow choosing not-ready models for compose ([#394](https://github.com/microsoft/OCR-Form-Tools/commit/b67191cdbc872b9004be30aa4b4dfde9a88dfe37))
* feat: track five most recent project models ([#395](https://github.com/microsoft/OCR-Form-Tools/commit/05850603d51a6786c8b6e8b4a553db020df56158))
### 2.1-preview.1-abc6376 (07-08-2020)
* feat: enable model info in analyze results ([#383](https://github.com/microsoft/OCR-Form-Tools/commit/abc63767e97dd28a6bb9028e03f2225e6ac0f1ab))
* fix: check invalid provider options before project actions ([#390](https://github.com/microsoft/OCR-Form-Tools/commit/212647d4327d9e18e9248a2d39086eeaab404979))
### 2.1-preview.1-a334cfc (07-07-2020)
* fix: hide extra scrollbars for model compose view ([#380](https://github.com/microsoft/OCR-Form-Tools/commit/a334cfc45fc5ab137682ad2b48dd0ec1585055dc))
* fix: handle version change state mutation error ([#382](https://github.com/microsoft/OCR-Form-Tools/commit/8991cc0c92f2f5cbd226f7e1c5c0825b7af8937c))
* fix: handle pdf worker terminated error ([#381](https://github.com/microsoft/OCR-Form-Tools/commit/adc0498c31bfd5ba57ab98c373e73575589ab1e1))
### 2.1-preview.1-7192170 (07-02-2020)
* feat: support release ([#361](https://github.com/microsoft/OCR-Form-Tools/commit/7192170d73d24a43e7fff18cd2c6bae7f208f1b0))
### 2.1-preview.1-978dabc (07-01-2020)
* feat: support document management ([#374](https://github.com/microsoft/OCR-Form-Tools/commit/978dabc3ba877ed4215865cba2a583fb785a2894))
### 2.1-preview.1-56a4b89 (06-30-2020)
* fix: wait until composed model is ready ([#369](https://github.com/microsoft/OCR-Form-Tools/commit/56a4b89f370f2fd72c6bc275376205e7fffe6a9e))
### 2.1-preview.1-6114d64 (06-23-2020)
* fix: update OCR version ([#335](https://github.com/microsoft/OCR-Form-Tools/commit/6114d6456b27a59335e534eef72cefd1b2f15737))
* feat: support electron for on premise solution ([#333](https://github.com/microsoft/OCR-Form-Tools/commit/ca0bd0c2ab46b7b587e5bfbc60c29b62bb325297))
### 2.1-preview.1-8297b18 (06-19-2020)
* refactor: put api version in constants ([#332](https://github.com/microsoft/OCR-Form-Tools/commit/8297b18a084be86bc4c986a1a332cb40bd807d1b))
### 2.1-preview.1-3b7f803 (06-18-2020)
* feat: enable model compose (preview) ([#328](https://github.com/microsoft/OCR-Form-Tools/commit/3b7f803407b82191706120bb9f12b82de1955704))
* fix: quick reordering tags ([#322](https://github.com/microsoft/OCR-Form-Tools/commit/3cc5267ef8617590adb3d4966f75cfed64604f00))
* feat: localization for canvas commandbar items ([#319](https://github.com/microsoft/OCR-Form-Tools/commit/253b9c90eb4923e7fde015a7216905fa32a8dcfa))
* feat: enable re-run OCR ([#297](https://github.com/microsoft/OCR-Form-Tools/commit/cbe9b0ed1c48f54c100b31b7f04706a969df2dd5))
* fix: capitalize python in analyze page ([#320](https://github.com/microsoft/OCR-Form-Tools/commit/96626636a96a3d19030df283ac794fa9c2aab18c))
* fix: fix spelling correction for string match ([#318](https://github.com/microsoft/OCR-Form-Tools/commit/28e53cefcf0bb462d547d6e38b24c480c03b946f))
* feature: keep prediction in UI ([#285](https://github.com/microsoft/OCR-Form-Tools/commit/dad98b9bd1d305a6bfeb2846ef4067da186ff801))
### 2.0.0-1c39800 (06-05-2020)
* feat: add description - how to delete info ([#292](https://github.com/microsoft/OCR-Form-Tools/commit/1c39800b1152f186dfc19834bb969abbc4fe0ac2))
* feat: enable download analyze script ([#304](https://github.com/microsoft/OCR-Form-Tools/commit/9c97ed0ff9b0aa72ec9a197fc92f3a5998135c36))
* fix: check ocrread results before getting image extent ([#296](https://github.com/microsoft/OCR-Form-Tools/commit/61dba02fc6f19eb854e1f499e475b1336e6171b9))
* feat: Add better error message for CORS ([#289](https://github.com/microsoft/OCR-Form-Tools/commit/8f210792b4d84e424b00499efb540b0e27e9fdad))
### 2.0.0-2760166 (05-30-2020)
* fix: fix mime check bug for jpeg/jpg and tiff ([#291](https://github.com/microsoft/OCR-Form-Tools/commit/2760166bcb809bbfdc207b01db49f00153318624))
* refactor: simplify shortcut descriptions ([#277](https://github.com/microsoft/OCR-Form-Tools/commit/db95b0e2510f6cef9bc7279fe0a19dce239c816e))
### 2.0.0-a5e4e07 (05-21-2020)
* feature: show table view when table icon is clicked ([#271](https://github.com/microsoft/OCR-Form-Tools/commit/a5e4e079d4c0d1c7c52e3b015c0ddf9b8601bbf2))
### 2.0.0-814276a (05-20-2020)
* fix: modify skip button according to feedback comments ([#264](https://github.com/microsoft/OCR-Form-Tools/commit/814276af6f4259844854798adf0c56bd606b2363))
* feature: keyboard shortcuts and tips ([#258](https://github.com/microsoft/OCR-Form-Tools/commit/37aa859a80dc0213a118313558ad21ba424008e7))
* feat: add electron mode from VoTT project ([#260](https://github.com/microsoft/OCR-Form-Tools/commit/2a3383d4a0f100a39ed40627bdffb9b48f78f5df))
* refactor: use forEach instead of map in handleFeatureSelect ([#259](https://github.com/microsoft/OCR-Form-Tools/commit/c1c590c463743d187fda2429a628e27c6c42012f))
### 2.0.0-0061645 (05-13-2020)
* build: update nginx base image version to 1.18.0-alpine ([#255](https://github.com/microsoft/OCR-Form-Tools/commit/0061645871806595e4fe2ab5991cc494afa26b31))
* fix: assign empty string when predict item's fieldName is undefined ([#254](https://github.com/microsoft/OCR-Form-Tools/commit/d4d919f678b1f162f48c87ee5223281e57945a0a))
* fix: overlaping left split pane ([#252](https://github.com/microsoft/OCR-Form-Tools/commit/2e8c351f74c385b8627ee6ea39f974e5e048ea8d))
* refactor: change predict to analyze in UI while keeping predict term ([#147](https://github.com/microsoft/OCR-Form-Tools/commit/c9aa58e36a10a35083249a8080c2cfb9fccf3733))
### 2.0.0-7c7ba93 (05-07-2020)
* fix: check null value from post processed value ([#248](https://github.com/microsoft/OCR-Form-Tools/commit/a361189c527bfffd6417f90a2521ad40b2b3f205))
* feat: enable outputting to file for analyze script ([#246](https://github.com/microsoft/OCR-Form-Tools/commit/7c7ba937f140490775b788d63ef2c7ed63ca40f1))
### 2.0.0-9d91800 (05-06-2020)
* fix: prevent user from changing tag types when invalid ([#224](https://github.com/microsoft/OCR-Form-Tools/commit/d8823a33591db5c5dc9a0af753e007167218a3e3))
* fix: prevent user from adding multiple checkboxes to a single tag ([#224](https://github.com/microsoft/OCR-Form-Tools/commit/d8823a33591db5c5dc9a0af753e007167218a3e3))
* fix: display error when inputted SAS doesn't contain token ([#243](https://github.com/microsoft/OCR-Form-Tools/commit/9826ca8504549f23057c9cad1baebc5e9d1f6fe7))
### 2.0.0-25d3298 (05-04-2020)
* feat: track document count for tags ([#231](https://github.com/microsoft/OCR-Form-Tools/commit/70a6e43dc54239cdc153d5d328b17c1dfa0f085f))
* fix: display error when inputted service URI contains path or query ([#234](https://github.com/microsoft/OCR-Form-Tools/commit/04a16961b37ad5b5d01fc4c93addaaf69cbf0e72))
* feat: add link in status bar to CHANGELOG ([#233](https://github.com/microsoft/OCR-Form-Tools/commit/e66646a13263239213580378bbd2d8462d7e22b6))
### 2.0.0-f6c8ffa (05-01-2020)
* refactor: change checkbox to selectionMark ([#223](https://github.com/microsoft/OCR-Form-Tools/commit/f6c8ffad6edf23f6241f314e9456da92bc1a8402))
### 2.0.0-f3e42f6 (04-30-2020)
* feat: display post-processed value in analyzed results ([#229](https://github.com/microsoft/OCR-Form-Tools/commit/f3e42f6e8e9e934f1a241921dbe4a1e8d311bb46))
### 2.0.0-f068866 (04-28-2020)
* fix: hide sprin in tag input control when open an empty folder ([#220](https://github.com/microsoft/OCR-Form-Tools/commit/f0688668df2e676fce9749fad8ec9d39e56697cf))
* perf: cache images, reduce canvas size, and fix memory leak for asset preview ([#218](https://github.com/microsoft/OCR-Form-Tools/commit/e8ad9a3bebf2a1ae210e0e1fa3eebba564592c4c))
### 2.0.0-595a512 (04-24-2020)
* fix: align rotated picture asset with OCR result
### 2.0.0-51c02cc (04-20-2020)
* fix: scrollbar fix when page size changes
* fix: Add split pane to fix too long tag name is invisible in right sidebar
### 2.0.0-202fb2f (04-20-2020)
* perf: improve assets loading performance and fix some bugs
### 2.0.0-bce554e (04-16-2020)
* perf: improve Azure Blob file list performance
* feat: support URL upload for predicting file
### 2.0.0-ef18425 (04-09-2020)
* feat: enable checkbox labeling (preview)

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

@ -43,6 +43,9 @@
"reactstrap": "^8.2.0",
"redux": "^4.0.4",
"redux-thunk": "^2.3.0",
"rfdc": "^1.1.4",
"rimraf": "^3.0.2",
"serialize-javascript": "^5.0.1",
"shortid": "^2.2.15",
"utif": "^3.1.0",
"vott-react": "^0.2.12"

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

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

@ -42,6 +42,8 @@ export const constants = {
autoLabelBatchSizeMax: 10,
autoLabelBatchSizeMin: 3,
showOriginLabelsByDefault: true,
fieldsSchema: "http://www.azure.com/schema/formrecognizer/fields.json",
labelsSchema: "http://www.azure.com/schema/formrecognizer/labels.json",
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*/
@ -271,7 +271,7 @@ export const english: IAppStrings = {
localFile: "Local file",
url: "URL",
},
layoutPredict:{
layoutPredict: {
layout: "Layout",
title: "Layout analyze",
inProgress: "Analysis in progress...",
@ -337,7 +337,43 @@ export const english: IAppStrings = {
autoLabel: "Auto-labeled: ",
revised: "Revised: ",
},
regionTableTags: {
configureTag: {
errors: {
atLeastOneColumn: "Please assign at least one column.",
atLeastOneRow: "Please assign at least one row.",
checkFields: "Please check if you filled out all required fields correctly.",
assignTagName: "Tag name cannot be empty",
notUniqueTagName: "Tag name should be unique",
emptyTagName: "Please assign name for your table tag.",
emptyName: "Name cannot be empty",
notUniqueName: "Name should be unique",
notCompatibleTableColOrRowType: "\${kind}\ type is not compatible with this type. If you want to change type of this \${kind}\, please remove or assign all labels which using this \${kind}\ in your project.",
}
},
tableLabeling: {
title: "Label table",
tableName: "Table name",
description: {
title: "To start labeling your table:",
stepOne: "Select the words on the document you want to label",
stepTwo: "Click the table cell you want to label selected words to",
},
buttons: {
done: "Done",
reconfigureTable: "Reconfigure table",
addRow: "Add row"
},
},
confirm: {
reconfigure: {
title: "Reconfigure tag",
message: "Are you sure you want to reconfigure this tag? \n It will be reconfigured for all documents.",
}
}
},
toolbar: {
addTable: "Add new table tag",
add: "Add new tag",
onlyShowCurrentPageTags: "Only show tags used in current page",
showAllTags: "Show all tags",
@ -524,7 +560,7 @@ export const english: IAppStrings = {
runOcrOnAllDocuments: "Run Layout on all documents",
runAutoLabelingCurrentDocument: "Auto-label the current document",
runAutoLabelingOnMultipleUnlabeledDocuments: "Auto-label multiple unlabeled documents",
noPredictModelOnProject: "Predict model not avaliable, please train the model first.",
noPredictModelOnProject: "Predict model not available, please train the model first.",
}
}
},
@ -620,8 +656,8 @@ export const english: IAppStrings = {
description: "Select all labels for a tag on document and press 'delete' key"
},
groupSelect: {
name: "Select multiple words by drawing a bounding box around encompased words",
description: "Press and hold the shift key. Then, click and hold left mouse button. Then, drag the pointer to draw the bounding box around encompased words"
name: "Select multiple words by drawing a bounding box around encompassed words",
description: "Press and hold the shift key. Then, click and hold left mouse button. Then, drag the pointer to draw the bounding box around encompassed words"
}
},
headers: {
@ -642,7 +678,7 @@ export const english: IAppStrings = {
},
genericRenderError: {
title: "Error Loading Application",
message: "An error occured while rendering the application. Please try again",
message: "An error occurred while rendering the application. Please try again",
},
projectInvalidSecurityToken: {
title: "Error loading project file",
@ -777,7 +813,7 @@ export const english: IAppStrings = {
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: "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.",
success: "Project token copied to clipboard and ready to share. Receiver 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*/
@ -336,7 +336,44 @@ export const spanish: IAppStrings = {
autoLabel: "Auto-etiquetado: ",
revised: "Revisado: ",
},
regionTableTags: {
configureTag: {
errors: {
atLeastOneColumn: "Asigne al menos una columna.",
atLeastOneRow: "Asigne al menos una fila.",
checkFields: "Verifique si completó todos los campos obligatorios correctamente.",
assignTagName: "El nombre de la etiqueta no puede estar vacío.",
notUniqueTagName: "El nombre de la etiqueta debe ser único",
emptyTagName: "Asigne un nombre para la etiqueta de su mesa.",
emptyName: "El nombre no puede estar vacío",
notUniqueName: "El nombre debe ser único",
notCompatibleTableColOrRowType: "El tipo $ {kind} no es compatible con este tipo. Si desea cambiar el tipo de este $ {kind}, elimine o asigne todas las etiquetas que usan este $ {kind} en su proyecto."
}
},
tableLabeling: {
title: "Tabla de etiquetas",
tableName: "Nombre de la tabla",
description: {
title: "Para comenzar a etiquetar su mesa:",
stepOne: "Seleccione las palabras del documento que desea etiquetar",
stepTwo: "Haga clic en la celda de la tabla a la que desea etiquetar las palabras seleccionadas",
},
buttons: {
done: "Hecho",
reconfigureTable: "Reconfigurar la tabla",
addRow: "Añadir fila"
}
},
confirm: {
reconfigure: {
title: "Reconfigurar etiqueta",
message: "¿Está seguro de que desea volver a configurar esta etiqueta?\n Se volverá a configurar para todos los documentos.",
}
}
},
toolbar: {
addTable: "Agregar nueva etiqueta",
add: "Agregar nueva etiqueta",
onlyShowCurrentPageTags: "Mostrar solo las etiquetas utilizadas en la página actual",
showAllTags: "Mostrar todas las etiquetas",

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

@ -489,6 +489,7 @@ export default class MockFactory {
saveAssetMetadataAndCleanEmptyLabel: jest.fn(()=> Promise.resolve()),
updateProjectTag: jest.fn(() => Promise.resolve()),
deleteProjectTag: jest.fn(() => Promise.resolve()),
reconfigureTableTag: jest.fn(() => Promise.resolve()),
};
}

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

@ -306,22 +306,23 @@ export interface IAppStrings {
color: string,
}
toolbar: {
add: string,
onlyShowCurrentPageTags: string,
showAllTags: string,
add: string;
addTable: string;
contextualMenu: string;
delete: string;
edit: string;
format: string;
lock: string;
moveDown: string;
moveUp: string;
rename: string;
search: string;
type: string;
vertiline: string;
onlyShowCurrentPageTags:string,
showAllTags:string,
showOriginLabels: string
hideOriginLabels: string,
contextualMenu: string,
delete: string,
edit: string,
format: string,
lock: string,
moveDown: string,
moveUp: string,
rename: string,
search: string,
type: string,
vertiline: string,
}
colors: {
white: string,
@ -346,12 +347,47 @@ export interface IAppStrings {
notCompatibleTagType: string,
checkboxPerTagLimit: string,
notCompatibleWithDrawnRegionTag: string,
replaceAllExitingLabels: string,
replaceAllExitingLabelsTitle: string,
replaceAllExitingLabels:string,
replaceAllExitingLabelsTitle:string,
},
preText: {
autoLabel: string,
revised: string,
preText:{
autoLabel:string,
revised:string,
}
regionTableTags: {
configureTag: {
errors: {
atLeastOneColumn: string,
atLeastOneRow: string,
checkFields: string,
assignTagName: string,
notUniqueTagName: string,
emptyTagName: string,
emptyName: string,
notUniqueName: string,
notCompatibleTableColOrRowType: string;
},
},
tableLabeling: {
title: string,
description: {
title: string,
stepOne: string,
stepTwo: string,
},
tableName: string,
buttons: {
done: string,
reconfigureTable: string,
addRow: string,
}
},
confirm: {
reconfigure: {
title: string,
message: string,
}
}
}
};
connections: {
@ -677,7 +713,7 @@ interface IErrorMetadata {
message: string,
}
interface IStrings extends LocalizedStringsMethods, IAppStrings { }
interface IStrings extends LocalizedStringsMethods, IAppStrings {}
export const strings: IStrings = new LocalizedStrings({
en: english,

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

@ -1,4 +1,4 @@
import {createTheme, IPalette} from "@fluentui/react";
import {createTheme, IPalette, DefaultPalette} from "@fluentui/react";
const rightPaneDefaultButtonPalette = {
themePrimary: "#E9ECEF",
@ -306,6 +306,7 @@ const subMenuPalette = {
const rightPaneDefaultButtonTheme = createTheme({palette: rightPaneDefaultButtonPalette});
const defaultDarkTheme = createTheme({palette: DarkDefaultPalette});
const defaultTheme = createTheme({palette: DefaultPalette});
const whiteTheme = createTheme({palette: whiteButtonPalette});
const redTheme = createTheme({palette: redButtonPalette});
const greenTheme = createTheme({palette: greenButtonPalette});
@ -356,6 +357,9 @@ export function getGreenWithWhiteBackgroundTheme() {
export function getDefaultDarkTheme() {
return defaultDarkTheme;
}
export function getDefaultTheme() {
return defaultTheme;
}
export function getSubMenuTheme() {
return subMenuTheme;

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

@ -2,9 +2,10 @@
// Licensed under the MIT license.
import Guard from "./guard";
import { IProject, ISecurityToken, IProviderOptions, ISecureString, ITag } from "../models/applicationState";
import { IProject, ISecurityToken, IProviderOptions, ISecureString, ITag, FieldType, FieldFormat } from "../models/applicationState";
import { encryptObject, decryptObject, encrypt, decrypt } from "./crypto";
import UTIF from "utif";
import { useState, useEffect } from 'react';
import {constants} from "./constants";
import _ from "lodash";
import JsZip from 'jszip';
@ -198,7 +199,7 @@ export async function throttle<T>(max: number, arr: T[], worker: (payload: T) =>
}
export function delay(ms: number) {
return new Promise((resolve) => {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, ms);
@ -361,6 +362,64 @@ export function fixedEncodeURIComponent(str: string) {
})
}
/**
* Filters tag's format according to chosen tag's type
* @param FieldType The json object
* @returns [] of corresponding tag's formats
*/
export function filterFormat(type: FieldType | string): any[] {
switch (type) {
case FieldType.String:
return [
FieldFormat.NotSpecified,
FieldFormat.Alphanumeric,
FieldFormat.NoWhiteSpaces,
];
case FieldType.Number:
return [
FieldFormat.NotSpecified,
FieldFormat.Currency,
];
case FieldType.Date:
return [
FieldFormat.NotSpecified,
FieldFormat.DMY,
FieldFormat.MDY,
FieldFormat.YMD,
];
case FieldType.Object:
case FieldType.Array:
return [
FieldFormat.NotSpecified,
];
default:
return [ FieldFormat.NotSpecified ];
}
}
/**
* UseDebounce - custom React hook for handling fast changing values, the hook re-call only if value or delay changes
* @param value The value to be changed
* @param delay - delay after which the change will be registered in milliseconds
*/
export function useDebounce(value: any, delay: number) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const delayHandler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// cleanup
return () => {
clearTimeout(delayHandler);
};
},
[value, delay]
);
return debouncedValue;
}
export function getAPIVersion(projectAPIVersion: string): string {
return (constants.enableAPIVersionSelection && projectAPIVersion) ? projectAPIVersion : constants.apiVersion;
}
@ -416,6 +475,20 @@ export function downloadFile(data: any, fileName: string, prefix?: string): void
fileLink.click();
}
export function getTagCategory (tagType: string) {
switch (tagType) {
case FieldType.SelectionMark:
case "checkbox":
return "checkbox";
case FieldType.Object:
return FieldType.Object;
case FieldType.Array:
return FieldType.Array;
default:
return "text";
}
}
export type zipData = {
fileName: string;
data: any;

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

@ -15,12 +15,12 @@
"unicode": "E4C7"
},
{
"name": "AddTo",
"unicode": "ECC8"
"name": "AddTable",
"unicode": "E4C6"
},
{
"name": "AlertSolid",
"unicode": "F331"
"name": "AddTo",
"unicode": "ECC8"
},
{
"name": "AutoEnhanceOff",
@ -30,6 +30,10 @@
"name": "AutoEnhanceOn",
"unicode": "E78D"
},
{
"name": "AlertSolid",
"unicode": "F331"
},
{
"name": "AzureAPIManagement",
"unicode": "F37F"
@ -122,6 +126,10 @@
"name": "Edit",
"unicode": "E70F"
},
{
"name": "EditTable",
"unicode": "E4C4"
},
{
"name": "FieldChanged",
"unicode": "F2C3"
@ -134,6 +142,10 @@
"name": "Filter",
"unicode": "E71C"
},
{
"name": "FixedColumnWidth",
"unicode": "E3EA"
},
{
"name": "GroupedList",
"unicode": "EF74"
@ -158,6 +170,14 @@
"name": "Info",
"unicode": "E946"
},
{
"name": "InsertColumnsRight",
"unicode": "F64B"
},
{
"name": "InsertRowsBelow",
"unicode": "F64D"
},
{
"name": "Insights",
"unicode": "E3AF"
@ -270,6 +290,18 @@
"name": "Table",
"unicode": "ED86"
},
{
"name": "TableBrandedColumn",
"unicode": "E3F1"
},
{
"name": "TableBrandedRow",
"unicode": "E3EE"
},
{
"name": "TableGroup",
"unicode": "F6D9"
},
{
"name": "Tag",
"unicode": "E8EC"
@ -290,6 +322,10 @@
"name": "Up",
"unicode": "E74A"
},
{
"name": "UpdateRestore",
"unicode": "E777"
},
{
"name": "View",
"unicode": "E890"
@ -307,4 +343,4 @@
"unicode": "E71F"
}
]
}
}

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

@ -119,11 +119,28 @@ export interface IFileInfo {
* @member color - User editable color associated to tag
*/
export interface ITag {
name: string,
color: string,
type: FieldType,
format: FieldFormat,
documentCount?: number,
name: string;
color: string;
type: FieldType;
format: FieldFormat;
documentCount?: number;
}
export interface ITableTag extends ITag {
fields?: ITableField[];
itemType?: string;
definition?: ITableDefinition,
visualizationHint?: TableVisualizationHint,
}
export enum TableHeaderTypeAndFormat {
Rows = "rows",
Columns = "columns"
}
export enum TableVisualizationHint {
Horizontal = "horizontal",
Vertical = "vertical",
}
/**
@ -217,7 +234,14 @@ export interface IRegion {
boundingBox?: IBoundingBox,
value?: string,
pageNumber: number,
isTableRegion?: boolean,
changed?: boolean,
}
export interface ITableRegion extends IRegion {
rowKey: string,
columnKey: string,
}
/**
@ -228,6 +252,8 @@ export interface ILabelData {
document: string,
labelingState?: AssetLabelingState;
labels: ILabel[],
tableLabels?: ITableLabel[],
$schema?: string,
}
/**
@ -244,6 +270,18 @@ export interface ILabel {
revised?: boolean;
}
export interface ITableLabel {
tableKey: string,
labels: ITableCellLabel[],
}
export interface ITableCellLabel {
rowKey: string,
columnKey: string,
value: IFormRegion[],
revised?: boolean;
}
/**
* @name - IFormRegion
* @description - Defines a region which consumed by FormRecognizer
@ -290,13 +328,39 @@ export interface ISecurityToken {
}
export interface IField {
fieldKey: string,
fieldType: FieldType,
fieldFormat: FieldFormat,
fieldKey: string;
fieldType: FieldType;
fieldFormat: FieldFormat;
}
export interface ITableKeyField extends IField {
documentCount?: number;
}
export interface ITableField extends IField {
itemType?: string;
fields?: ITableField[];
visualizationHint?: TableVisualizationHint;
}
export interface ITableDefinition extends IField {
itemType?: string;
fields?: ITableField[];
}
export interface ITableConfigItem {
name: string,
format: string,
type: string;
originalName?: string;
originalFormat?: string,
originalType?: string;
}
export interface IFieldInfo {
schema?: string,
fields: IField[],
definitions?: any,
}
export interface IRecentModel {
@ -434,12 +498,21 @@ export enum FieldType {
Time = "time",
Integer = "integer",
SelectionMark = "selectionMark",
Array = "array",
Object = "object",
}
export enum LabelType {
DrawnRegion = "region"
}
export enum TableElements {
rows = "rows",
row = "row",
columns = "columns",
column = "column",
}
export enum FieldFormat {
NotSpecified = "not-specified",
Currency = "currency",
@ -463,3 +536,15 @@ export enum ImageMapParent {
Predict = "predict",
Editor = "editor",
}
export enum TagInputMode {
Basic = "basic",
ConfigureTable = "configureTable",
LabelTable = "labelTable",
}
export enum AnalyzedTagsMode {
default = "default",
LoadingRecentModel = "loadingRecentModel",
ViewTable = "viewTable",
}

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

@ -256,7 +256,7 @@ export class AzureBlobStorage implements IStorageProvider {
return asset;
}
}
else{
else {
return null;
}
}

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

@ -0,0 +1,9 @@
@import './../../../../assets/sass/theme.scss';
.spinner-container {
display: flex;
width: 16rem;
height: 4rem;
align-items: center;
justify-content: center;
}

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

@ -11,9 +11,12 @@ import {
ITheme,
PrimaryButton,
DefaultButton,
SpinnerSize,
Spinner
} from "@fluentui/react";
import { MessageFormatHandler } from "../messageBox/messageBox";
import { getDarkTheme } from "../../../../common/themes";
import { getDarkTheme, getDefaultDarkTheme } from "../../../../common/themes";
import "./confirm.scss";
/**
* Properties for Confirm Component
@ -26,6 +29,7 @@ import { getDarkTheme } from "../../../../common/themes";
export interface IConfirmProps {
title?: string;
message: string | ReactElement<any> | MessageFormatHandler;
loadMessage?: string;
confirmButtonText?: string;
cancelButtonText?: string;
confirmButtonTheme?: ITheme;
@ -40,6 +44,7 @@ export interface IConfirmProps {
export interface IConfirmState {
params: any[];
hideDialog: boolean;
loading: boolean;
}
/**
@ -54,12 +59,14 @@ export default class Confirm extends React.Component<IConfirmProps, IConfirmStat
this.state = {
params: null,
hideDialog: true,
loading: false,
};
this.open = this.open.bind(this);
this.close = this.close.bind(this);
this.onConfirmClick = this.onConfirmClick.bind(this);
this.onCancelClick = this.onCancelClick.bind(this);
this.load = this.load.bind(this)
}
public render() {
@ -69,8 +76,9 @@ export default class Confirm extends React.Component<IConfirmProps, IConfirmStat
},
scopedSettings: {},
};
const { confirmButtonTheme } = this.props;
const { hideDialog } = this.state;
const { hideDialog, loading } = this.state;
return (
<Customizer {...dark}>
@ -78,15 +86,26 @@ export default class Confirm extends React.Component<IConfirmProps, IConfirmStat
<Dialog
hidden={hideDialog}
onDismiss={this.close}
dialogContentProps={{
dialogContentProps={!loading ? {
type: DialogType.normal,
title: this.props.title,
subText: this.getMessage(this.props.message),
}}
} : null}
modalProps={{
isBlocking: true,
}}
>
{loading && this.props.loadMessage &&
<div className="spinner-container">
<Spinner
label={this.props.loadMessage}
labelPosition="right"
theme={getDefaultDarkTheme()}
size={SpinnerSize.large}
/>
</div>
}
<DialogFooter>
<PrimaryButton
theme={confirmButtonTheme}
@ -114,12 +133,20 @@ export default class Confirm extends React.Component<IConfirmProps, IConfirmStat
* Close Confirm Dialog
*/
public close(): void {
this.setState({ hideDialog: true });
this.setState({ hideDialog: true, loading: false });
}
public load(): void {
this.setState({loading: true});
}
private onConfirmClick() {
this.props.onConfirm.apply(null, this.state.params);
this.close();
if (this.props.loadMessage) {
this.load();
} else {
this.close();
}
}
private onCancelClick() {

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

@ -13,6 +13,7 @@
}
.sourceDropdown {
width: 95px;
margin-bottom: 10px;
}
.local-file {
float: right;

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

@ -87,7 +87,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
private modify: Modify;
private snap: Snap;
private drawnFeatures: Collection = new Collection([], {unique: true});
private drawnFeatures: Collection = new Collection([], { unique: true });
public modifyStartFeatureCoordinates: any = {};
private imageExtent: number[];
@ -204,7 +204,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
onMouseEnter={this.handlePointerEnterImageMap}
className="map-wrapper"
>
<div style={{cursor: this.getCursor()}} id="map" className="map" ref={(el) => this.mapElement = el} />
<div style={{ cursor: this.getCursor() }} id="map" className="map" ref={(el) => this.mapElement = el} />
</div>
);
}
@ -483,7 +483,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
this.drawRegionVectorLayer?.getSource().clear();
this.drawnLabelVectorLayer?.getSource().clear();
this.drawnFeatures = new Collection([], {unique: true});
this.drawnFeatures = new Collection([], { unique: true });
this.drawRegionVectorLayer.getSource().on("addfeature", (evt) => {
this.pushToDrawnFeatures(evt.feature, this.drawnFeatures);

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

@ -0,0 +1,219 @@
@import "../../../../assets/sass/theme.scss";
@import "./tagInputSize.scss";
.config-view_container {
display: flex;
flex-direction: column;
width: 100%;
padding: 1px;
padding-right: 12px;
h5, h4 {
margin-left: 12px;
}
.table-name_input {
width: 100%;
padding-right: 10px;
}
.columns_container, .rows_container {
margin-top: 2rem;
width: 99%;
h5 {
margin-left: 0;
}
.columns, .rows {
width: 99%;
margin-right: 2%;
background-color: $darker-2;
}
.ms-DetailsRow-cellCheck {
margin-right: -12px;
margin-top: 23px;
}
.ms-List-cell:not(:last-child){
border-bottom: 1px solid $lighter-4;
}
.column-name_input, .row-name_input {
margin-top: 16px;
}
.input-label-original-name {
color: rgb(177, 177, 177);
margin-bottom: 2px;
margin-right: 2px;
text-align: left;
margin-top: -24px;
}
}
.list_header {
width: 100%;
.ms-Button-label {
margin: 0 -4px;
}
.list-headers {
text-align: left;
&_name {
text-align: left;
}
&_type {
text-align: left;
margin: 0 20px;
}
&_format {
text-align: left;
}
}
}
.add_button {
margin-top: 8px;
margin-left: 0px;
width: 140px
}
.control-buttons_container {
display: flex;
margin-top: 2rem;
margin-bottom: 1rem;
justify-content: flex-end;
.save {
margin-left: 1rem;
}
}
.preview_container {
margin-top: 2rem;
h5 {
margin-left: 0;
}
.tableName {
&-current {
padding: 0 1px 2px 1px;
}
&-original {
margin-bottom: 0px;
text-decoration: line-through;
color: rgb(177, 177, 177);
font-size: small;
}
}
.table_container {
overflow: auto;
margin-top: 1rem;
.table {
td, th {
border: 2px solid #8D8D8D;
color: white;
padding: 0;
margin: 0;
}
.header {
&_column, &_row {
min-width: 130px;
max-width: 200px;
background-color: $lighter-3;
border: 2px solid grey;
text-align: center;
padding: .125rem .5rem;
.value {
}
.renamed-value {
text-decoration: line-through;
color: rgb(177, 177, 177);
font-size: small;
}
}
&_empty {
border: 2px solid grey;
background-color: $lighter-3;
}
}
.table-cell {
text-align: center;
background-color: $darker-3;
color: rgba(255, 255, 255, 0.75);
height: 1.5rem;
}
.hidden {
border: none;
font-size: small;
font-weight: 300;
background-color: transparent;
text-align: right;
vertical-align: middle;
color: rgba(255, 255, 255, 0.45);
width: 3rem;
padding-right: 0.4rem;
}
}
}
.rowDynamic_message {
margin-bottom: 4px;
color: rgba(255, 255, 255, 0.8);
}
}
}
// ========= utility classes
.original-value, .renamed-value {
overflow: hidden;
text-overflow: ellipsis;
}
.ml-12px {
margin-left: 12px;
}
.ms-TextField-errorMessage {
padding: 0 0 .25rem .25rem;
span {
color: #db7272;
}
}
.ms-DetailsRow-cellCheck {
display: flex;
width: 22px;
height: 28px;
margin-right: -12px;
margin-top: 6px;
}
.renamed-header-value {
}
.original-table-name {
margin-left: 12px;
color: rgb(177, 177, 177);
margin-bottom: 2px;
}
.restore-button {
color: #2d8eac;
&:hover {
color: #6ac5e1;
}
&:active {
color: #91cee0;
}
}
.compact-row {
border-bottom: 0.5px solid $lighter-4;
}
.table-name-preview {
font-weight: bold;
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -0,0 +1,91 @@
@import "../../../../assets/sass/theme.scss";
.table-labeling_container {
display: flex;
flex-direction: column;
width: 100%;
max-width: 100%;
padding-right: .5rem;
h4 {
padding-left: 1.25rem;
}
.labeling-guideline {
margin: 1rem 1rem 2rem 1.5rem;
color: rgba(255, 255, 255, 0.6);
}
.table-name {
padding-left: 1.25rem;
span {
padding-bottom: .2rem;
}
}
.add-row-button_container {
margin-left: 1.5rem;
margin-bottom: 3rem;
margin-top: 1rem;
}
.table-view-container {
overflow-x: auto;
.viewed-table {
margin-bottom: 1rem;
.column_header {
text-overflow: ellipsis;
overflow: hidden;
min-width: 130px;
max-width: 200px;
background-color: $lighter-3;
border: 2px solid grey;
text-align: center;
padding: .125rem .25rem;
}
.row_header {
text-overflow: ellipsis;
overflow: hidden;
min-width: 130px;
max-width: 200px;
border: 2px solid grey;
background-color: $lighter-3;
text-align: center;
padding: .125rem .5rem;
}
.empty_header {
border: 2px solid grey;
background-color: $lighter-3;
}
.table-cell {
text-align: center;
background-color: $darker-3;
color: rgba(255, 255, 255, 0.75);
&:hover {
background-color: $lighter-1;
}
&:active {
background-color: $lighter-2;
}
}
.hidden {
border: none;
background-color: transparent;
text-align: center;
color: rgba(255, 255, 255, 0.45);
min-width: 12px;
max-width: 48px;
padding-right: 0.3rem;
}
}
}
.buttons-container {
display: flex;
padding: 1.5rem;
justify-content: space-between;
.button {
width: 160px;
&-reconfigure {
}
}
}
}

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

@ -0,0 +1,171 @@
import React from 'react';
import "./tableTagConfig.scss";
import { PrimaryButton, FontIcon, DefaultButton } from "@fluentui/react";
import { getPrimaryGreenTheme, getPrimaryBlueTheme } from '../../../../common/themes';
import { FieldFormat, FieldType, TagInputMode, IRegion, ITableTag, ITableRegion, IField, TableElements, ITableField, ITableKeyField, TableVisualizationHint } from '../../../../models/applicationState';
import "./tableTagLabeling.scss";
import { strings } from "../../../../common/strings";
interface ITableTagLabelingProps {
setTagInputMode: (addTableMode: TagInputMode, selectedTableTagToLabel?: ITableTag, selectedTableTagBody?: ITableRegion[][][]) => void;
selectedTag: ITableTag,
selectedRegions?: IRegion[];
onTagClick?: (tag: ITableTag) => void;
selectedTableTagBody: ITableRegion[][][];
handleTableCellClick: (iTableCellIndex: number, jTableCellIndex: number) => void;
handleTableCellMouseEnter: (regions: IRegion[]) => void
handleTableCellMouseLeave: () => void
addRowToDynamicTable: () => void;
splitPaneWidth?: number;
}
interface ITableTagLabelingState {
selectedRowIndex: number;
selectedColumnIndex: number;
rows: ITableKeyField[],
columns: ITableKeyField[],
selectedTableTagBody: any,
}
// @connect(mapStateToProps)
export default class TableTagLabeling extends React.Component<ITableTagLabelingProps> {
public state: ITableTagLabelingState = {
selectedRowIndex: null,
selectedColumnIndex: null,
rows: this.props.selectedTag.type === FieldType.Array || this.props.selectedTag?.visualizationHint === TableVisualizationHint.Vertical ? this.props.selectedTag.fields : this.props.selectedTag.definition.fields,
columns: this.props.selectedTag.type === FieldType.Array || this.props.selectedTag.visualizationHint === TableVisualizationHint.Vertical ? this.props.selectedTag.definition.fields : this.props.selectedTag.fields,
selectedTableTagBody: this.props.selectedTableTagBody,
};
public componentDidMount = async () => {
if (this.props.selectedTag.type === FieldType.Array) {
const rows = [{ fieldKey: "#0", fieldType: FieldType.String, fieldFormat: FieldFormat.NotSpecified }]
for (let i = 1; i < this.props.selectedTableTagBody.length; i++) {
rows.push({ fieldKey: "#" + i, fieldType: FieldType.String, fieldFormat: FieldFormat.NotSpecified });
}
this.setState({ rows });
}
}
public componentDidUpdate = async (prevProps: Readonly<ITableTagLabelingProps>, prevState: Readonly<ITableTagLabelingState>) => {
if (this.props.selectedTableTagBody.length !== prevProps.selectedTableTagBody.length) {
const rows = [{ fieldKey: "#0", fieldType: FieldType.String, fieldFormat: FieldFormat.NotSpecified }]
for (let i = 1; i < this.props.selectedTableTagBody.length; i++) {
rows.push({ fieldKey: "#" + i, fieldType: FieldType.String, fieldFormat: FieldFormat.NotSpecified });
}
this.setState({ rows });
}
}
public render() {
return (
<div className="table-labeling_container">
<h4 className="mt-2">{strings.tags.regionTableTags.tableLabeling.title}</h4>
<div className="labeling-guideline">
{strings.tags.regionTableTags.tableLabeling.description.title}
<ol>
<li>{strings.tags.regionTableTags.tableLabeling.description.stepOne}</li>
<li>{strings.tags.regionTableTags.tableLabeling.description.stepTwo}</li>
</ol>
</div>
<h5 className="mb-4 table-name">
<span style={{ borderBottom: `4px solid ${this.props.selectedTag.color}` }}>{`${strings.tags.regionTableTags.tableLabeling.tableName}: ${this.props.selectedTag.name}`}</span>
</h5>
{ (this.props.selectedTag.type === FieldType.Object && this.props.selectedTag.fields && this.props.selectedTag.definition.fields) || this.props.selectedTag.definition.fields ?
<div className="table-view-container">
<table className="viewed-table">
<tbody>
{this.getTableBody()}
</tbody>
</table>
</div>
:
<div>Missing fields. Please Reconfigure table.</div>
}
{this.props.selectedTag.type === FieldType.Array && <div className="add-row-button_container">
<PrimaryButton
theme={getPrimaryBlueTheme()}
className="add_button ml-6"
autoFocus={true}
onClick={this.addRow}
>
<FontIcon iconName="Add" className="mr-2" />
{strings.tags.regionTableTags.tableLabeling.buttons.addRow}
</PrimaryButton>
</div>}
<div className="buttons-container">
<PrimaryButton
className="button-done"
theme={getPrimaryGreenTheme()}
onClick={() => {
this.props.setTagInputMode(TagInputMode.Basic, null, null)
}}
>{strings.tags.regionTableTags.tableLabeling.buttons.done}
</PrimaryButton>
<DefaultButton
className="button-reconfigure"
theme={getPrimaryGreenTheme()}
onClick={() => { this.props.setTagInputMode(TagInputMode.ConfigureTable) }}
>{strings.tags.regionTableTags.tableLabeling.buttons.reconfigureTable}
</DefaultButton>
</div>
</div>
)
}
public getTableBody = () => {
const table = { rows: this.state.rows, columns: this.state.columns };
const selectedTableTagBody = this.props.selectedTableTagBody;
const isRowDynamic = this.props.selectedTag.type === FieldType.Array;
let tableBody = null;
if (table.rows && table.rows?.length !== 0 && table.columns.length !== 0) {
tableBody = [];
const rows = table[TableElements.rows];
const columns = table[TableElements.columns];
for (let i = 0; i < rows.length + 1; i++) {
const tableRow = [];
for (let j = 0; j < columns.length + 1; j++) {
if (i === 0 && j !== 0) {
tableRow.push(<th key={j} className={"column_header"}>{columns[j - 1].fieldKey}</th>);
} else if (j === 0 && i !== 0) {
tableRow.push(<th key={j} className={`row_header ${isRowDynamic ? "hidden" : ""}`}>{rows[i - 1].fieldKey}</th>);
} else if (j === 0 && i === 0) {
tableRow.push(<th key={j} className={`empty_header ${isRowDynamic ? "hidden" : ""}`} />);
} else {
tableRow.push(
<td
className={"table-cell"}
onClick={() => this.handleCellClick(i - 1, j - 1)} key={j}
onMouseEnter={() => this.handleTableCellMouseEnter(selectedTableTagBody[i - 1][j - 1])}
onMouseLeave={() => this.handleTableCellMouseLeave()}
>
{selectedTableTagBody[i - 1][j - 1]?.find((tableRegion) => tableRegion.value === "") && <FontIcon className="pr-1 pl-1" iconName="FieldNotChanged" />}
{selectedTableTagBody[i - 1][j - 1]?.map((tableRegion) => tableRegion.value).join(" ")}
</td>);
}
}
tableBody.push(<tr key={i}>{tableRow}</tr>);
}
}
return tableBody
}
private addRow = () => {
this.props.addRowToDynamicTable()
};
private handleCellClick = (iToChange: number, jToChange: number) => {
this.props.handleTableCellClick(iToChange, jToChange)
}
private handleTableCellMouseEnter = (regions: IRegion[]) => {
this.props.handleTableCellMouseEnter(regions)
}
private handleTableCellMouseLeave = () => {
this.props.handleTableCellMouseLeave();
}
}

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

@ -8,6 +8,8 @@
&-input {
display: flex;
flex-grow: 1;
overflow-y: auto;
max-width: 100% !important;
flex-direction: column;
user-select: none;
background: $lighter-1;
@ -36,6 +38,18 @@
&-container{
overflow-x: visible;
overflow-y: auto;
// padding: 0 0 0 100px;
// margin: 0 0 0 -100px;
&::before{
// content: " ";
// display: inline-block;
// position: absolute;
// width: 80px;
// height: 100%;
// left: -80px;
// background: linear-gradient(to right, #00000000 0%,#000000 100%);
}
};
}

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

@ -30,6 +30,16 @@ describe("Tag Input Component", () => {
labels: [],
onLabelEnter: jest.fn(),
onLabelLeave: jest.fn(),
tagInputMode: null,
selectedTableTagToLabel: null,
handleLabelTable: null,
addRowToDynamicTable: null,
reconfigureTableConfirm: null,
handleTableCellClick: null,
selectedTableTagBody: null,
splitPaneWidth: null,
handleTableCellMouseEnter: null,
handleTableCellMouseLeave: null
};
}

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

@ -2,30 +2,32 @@
// Licensed under the MIT license.
import {
ContextualMenu,
ContextualMenuItemType,
Customizer,
FontIcon,
IContextualMenuItem,
ICustomizations,
Spinner,
SpinnerSize
ContextualMenu, ContextualMenuItemType, Customizer,
FontIcon, IContextualMenuItem, ICustomizations,
Spinner, SpinnerSize, ChoiceGroup, IChoiceGroupOption
} from "@fluentui/react";
import { strings, interpolate } from "../../../../common/strings";
import { getDarkTheme, getPrimaryRedTheme } from "../../../../common/themes";
import { AlignPortal } from "../align/alignPortal";
import { filterFormat, getNextColor, getTagCategory } from "../../../../common/utils";
import {
IRegion, ITag, ILabel, FieldType, FieldFormat,
TagInputMode, FeatureCategory, ITableTag, ITableRegion,
ITableConfigItem, ITableKeyField, ITableLabel, TableElements, TableVisualizationHint
} from "../../../../models/applicationState";
import { ColorPicker } from "../colorPicker";
import "./tagInput.scss";
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, getPrimaryRedTheme} from "../../../../common/themes";
import {getNextColor} from "../../../../common/utils";
import {FeatureCategory, FieldFormat, FieldType, ILabel, IRegion, ITag} from "../../../../models/applicationState";
import React, { KeyboardEvent } from "react";
import { constants } from "../../../../common/constants";
import Confirm from "../../common/confirm/confirm";
import {AlignPortal} from "../align/alignPortal";
import {ColorPicker} from "../colorPicker";
import "../condensedList/condensedList.scss";
import "./tagInput.scss";
import TagInputItem, {ITagClickProps, ITagInputItemProps} from "./tagInputItem";
import TagInputItem, { ITagClickProps, ITagInputItemProps } from "./tagInputItem";
import TagInputToolbar from "./tagInputToolbar";
import { toast } from "react-toastify";
import TableTagConfig from "./tableTagConfig"
import TableTagLabeling from "./tableTagLabeling";
// tslint:disable-next-line:no-var-requires
const tagColors = require("../../common/tagColors.json");
@ -41,6 +43,7 @@ export enum TagOperationMode {
ColorPicker,
ContextualMenu,
Rename,
LabelTable,
}
export interface ITagInputProps {
@ -52,6 +55,9 @@ export interface ITagInputProps {
selectedRegions?: IRegion[];
/** The labels in the canvas */
labels: ILabel[];
encoded?: boolean;
/** The tableLabels in the canvas */
tableLabels?: ITableLabel[];
/** The doc current page number */
pageNumber: number;
/** Tags that are currently locked for editing experience */
@ -69,7 +75,7 @@ export interface ITagInputProps {
/** Function to call when tag is renamed */
onTagRename?: (oldTag: ITag, newTag: ITag, cancelCallback: () => void) => void;
/** Function to call when tag is deleted */
onTagDeleted?: (tagName: string) => void;
onTagDeleted?: (tagName: string, tagType: FieldType, tagFormat: FieldFormat) => void;
/** Always show tag input box */
showTagInputBox?: boolean;
/** Always show tag search box */
@ -80,6 +86,17 @@ export interface ITagInputProps {
onLabelLeave: (label: ILabel) => void;
/** Function to handle tag change */
onTagChanged?: (oldTag: ITag, newTag: ITag) => void;
setTagInputMode?: (tagInputMode: TagInputMode, selectedTableTagToLabel?: ITableTag) => void;
tagInputMode: TagInputMode;
selectedTableTagToLabel: ITableTag;
handleLabelTable: (tagInputMode: TagInputMode, selectedTableTagToLabel) => void;
addRowToDynamicTable: () => void;
reconfigureTableConfirm: (originalTagName: string, tagName: string, tagType: FieldType.Array | FieldType.Object, tagFormat: FieldFormat, visualizationHint: TableVisualizationHint, deletedColumns: ITableConfigItem[], deletedRows: ITableConfigItem[], newRows: ITableConfigItem[], newColumns: ITableConfigItem[]) => void;
handleTableCellClick: (iTableCellIndex, jTableCellIndex) => void;
selectedTableTagBody: ITableRegion[][][];
handleTableCellMouseEnter: (regions) => void;
handleTableCellMouseLeave: () => void;
splitPaneWidth: number;
onTagDoubleClick?: (label: ILabel) => void;
}
@ -94,31 +111,6 @@ export interface ITagInputState {
selectedTag: ITag;
}
function filterFormat(type: FieldType): FieldFormat[] {
switch (type) {
case FieldType.String:
return [
FieldFormat.NotSpecified,
FieldFormat.Alphanumeric,
FieldFormat.NoWhiteSpaces,
];
case FieldType.Number:
return [
FieldFormat.NotSpecified,
FieldFormat.Currency,
];
case FieldType.Date:
return [
FieldFormat.NotSpecified,
FieldFormat.DMY,
FieldFormat.MDY,
FieldFormat.YMD,
];
default:
return [ FieldFormat.NotSpecified ];
}
}
function isNameEqual(x: string, y: string) {
return x.trim().toLocaleLowerCase() === y.trim().toLocaleLowerCase();
}
@ -172,92 +164,136 @@ 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 (
<div className="tag-input">
<div ref={this.headerRef} className="tag-input-header p-2">
<span className="tag-input-title">{strings.tags.title}</span>
<TagInputToolbar
selectedTag={this.state.selectedTag}
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: "",
})}
searchingTags={this.state.searchQuery.length > 0}
onRenameTag={this.onRenameTag}
onLockTag={this.onLockTag}
onDelete={this.onDeleteTag}
onReorder={this.onReOrder}
/>
</div>
{this.props.tagsLoaded ?
if (this.props.tagInputMode === TagInputMode.ConfigureTable) {
return (
<div className="tag-input">
<div className="tag-input-header p-2">
<span className="tag-input-title">{strings.tags.title}</span>
</div>
<div className="tag-input-body-container">
<div className="tag-input-body">
{
this.state.searchTags &&
<div className="tag-input-text-input-row search-input">
<input
className="tag-search-box"
type="text"
onKeyDown={this.onSearchKeyDown}
onChange={(e) => this.setState({searchQuery: e.target.value})}
placeholder="Search tags"
autoFocus={true}
onFocus={() => this.setState({selectedTag: null, tagOperation: TagOperationMode.Rename})}
/>
<FontIcon iconName="Search" />
</div>
}
<div className="tag-input-items">
{this.renderTagItems()}
<Customizer {...dark}>
{
tagOperation === TagOperationMode.ContextualMenu && selectedTagRef &&
<ContextualMenu
className="tag-input-contextual-menu"
items={this.getContextualMenuItems()}
target={selectedTagRef}
onDismiss={this.onHideContextualMenu}
/>
}
</Customizer>
{this.getColorPickerPortal()}
</div>
{
this.state.addTags &&
<div className="tag-input-text-input-row new-tag-input">
<input
className="tag-input-box"
type="text"
onKeyDown={this.onAddTagKeyDown}
// Add mouse event
onBlur={this.onAddTagWithBlur}
placeholder="Add new tag"
autoFocus={true}
ref={this.inputRef}
/>
<FontIcon iconName="Tag" />
</div>
}
<TableTagConfig
setTagInputMode={this.props.setTagInputMode}
addTableTag={this.addTableTag}
splitPaneWidth={this.props.splitPaneWidth}
tableTag={this.props.selectedTableTagToLabel}
reconfigureTableConfirm={this.props.reconfigureTableConfirm}
selectedTableBody={this.props.selectedTableTagBody}
/>
</div>
</div>
:
<Spinner className="loading-tag" size={SpinnerSize.large} />
}
<Confirm
title={strings.tags.warnings.replaceAllExitingLabelsTitle}
ref={this.replaceConfirmRef}
message={strings.tags.warnings.replaceAllExitingLabels}
confirmButtonTheme={getPrimaryRedTheme()}
onConfirm={this.onReplaceConfirm}
/>
</div>
);
</div>
)
} else if (this.props.tagInputMode === TagInputMode.LabelTable) {
return (
<div className="tag-input">
<div className="tag-input-header p-2">
<span className="tag-input-title">{strings.tags.title}</span>
</div>
<TableTagLabeling
onTagClick={this.props.onTagClick}
selectedRegions={this.props.selectedRegions}
setTagInputMode={this.props.setTagInputMode}
selectedTag={this.props.selectedTableTagToLabel as ITableTag}
handleTableCellClick={this.props.handleTableCellClick}
handleTableCellMouseEnter={this.props.handleTableCellMouseEnter}
handleTableCellMouseLeave={this.props.handleTableCellMouseLeave}
selectedTableTagBody={this.props.selectedTableTagBody}
splitPaneWidth={this.props.splitPaneWidth}
addRowToDynamicTable={this.props.addRowToDynamicTable}
/>
</div>
)
} else {
return (
<div className="tag-input">
<div ref={this.headerRef} className="tag-input-header p-2">
<span className="tag-input-title">{strings.tags.title}</span>
<TagInputToolbar
selectedTag={this.state.selectedTag}
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: "",
})}
searchingTags={this.state.searchQuery.length > 0}
onRenameTag={this.onRenameTag}
onLockTag={this.onLockTag}
onDelete={this.onDeleteTag}
onReorder={this.onReOrder}
setTagInputMode={this.props.setTagInputMode}
/>
</div>
{this.props.tagsLoaded ?
<div className="tag-input-body-container">
<div className="tag-input-body">
{
this.state.searchTags &&
<div className="tag-input-text-input-row search-input">
<input
className="tag-search-box"
type="text"
onKeyDown={this.onSearchKeyDown}
onChange={(e) => this.setState({ searchQuery: e.target.value })}
placeholder="Search tags"
autoFocus={true}
onFocus={() => this.setState({ selectedTag: null, tagOperation: TagOperationMode.Rename })}
/>
<FontIcon iconName="Search" />
</div>
}
<div className="tag-input-items">
{this.renderTagItems()}
<Customizer {...dark}>
{
tagOperation === TagOperationMode.ContextualMenu && selectedTagRef &&
<ContextualMenu
className="tag-input-contextual-menu"
items={this.getContextualMenuItems()}
target={selectedTagRef}
onDismiss={this.onHideContextualMenu}
/>
}
</Customizer>
{this.getColorPickerPortal()}
</div>
{
this.state.addTags &&
<div className="tag-input-text-input-row new-tag-input">
<input
className="tag-input-box"
type="text"
onKeyDown={this.onAddTagKeyDown}
// Add mouse event
onBlur={this.onAddTagWithBlur}
placeholder="Add new tag"
autoFocus={true}
ref={this.inputRef}
/>
<FontIcon iconName="Tag" />
</div>
}
</div>
</div>
:
<Spinner className="loading-tag" size={SpinnerSize.large} />
}
<Confirm
title={strings.tags.warnings.replaceAllExitingLabelsTitle}
ref={this.replaceConfirmRef}
message={strings.tags.warnings.replaceAllExitingLabels}
confirmButtonTheme={getPrimaryRedTheme()}
onConfirm={this.onReplaceConfirm}
/>
</div>
);
}
}
public triggerNewTagBlur() {
if (this.inputRef.current) {
@ -372,14 +408,14 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
if (!tag) {
return;
}
this.props.onTagDeleted(tag.name);
this.props.onTagDeleted(tag.name, tag.type, tag.format);
}
private getColorPickerPortal = () => {
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 &&
@ -414,6 +450,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
onLabelEnter={this.props.onLabelEnter}
onLabelLeave={this.props.onLabelLeave}
onTagChanged={this.props.onTagChanged}
handleLabelTable={this.props.handleLabelTable}
onTagDoubleClick={this.props.onTagDoubleClick}
/>);
}
@ -423,21 +460,29 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
return item;
}
private setTagLabels = (key: string): ILabel[] => {
return this.props.labels.filter((label) => label.label === key);
private setTagLabels = (key: string): any[] => {
const labels = this.props.labels.filter((label) => {
if (this.props.encoded) {
return label.label.replace(/\~1/g, "/").replace(/\~0/g, "~") === key;
} else {
return label.label === key;
}
})
const tableLables = this.props.tableLabels.filter((label) => label.tableKey === key);
return [...labels, ...tableLables]
}
private createTagItemProps = (): ITagInputItemProps[] => {
const {tags, selectedTag, tagOperation, onlyCurrentPageTags} = 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);
const tableLabels = this.props.tableLabels.filter(item => item.labels[0]?.value[0]?.page === this.props.pageNumber).map(item => item.tableKey);
const labeledTags = [...labels, ...tableLabels];
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))
if (labeledTags.length) {
return tags.filter(tag => labeledTags.find(a => a === tag.name))
.map<ITagInputItemProps>(tag => {
return {
tag,
@ -518,19 +563,22 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
// Only fire click event if a region is selected
const {selectedRegions, onTagClick, labels} = this.props;
if (selectedRegions && selectedRegions.length && onTagClick) {
const {category} = selectedRegions[ 0 ];
const {format, type, documentCount, name} = tag;
const tagCategory = this.getTagCategory(type);
const { category } = selectedRegions[0];
const { format, type, documentCount, name } = tag;
const tagCategory = getTagCategory(type);
const isTagLabelTypeDrawnRegion = this.labelAssignedDrawnRegion(labels, tag.name);
const labelAssigned = this.labelAssigned(labels, name);
if (labelAssigned && ((category === FeatureCategory.DrawnRegion) !== isTagLabelTypeDrawnRegion)) {
if(category===FeatureCategory.Checkbox&&isTagLabelTypeDrawnRegion){
toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, {otherCatagory: FeatureCategory.Checkbox}));
}else if (isTagLabelTypeDrawnRegion) {
if ((tag.type === FieldType.Object || tag.type === FieldType.Array) && this.props.selectedRegions?.length) {
this.props.handleLabelTable(TagInputMode.LabelTable, tag)
deselect = false;
} else if (labelAssigned && ((category === FeatureCategory.DrawnRegion) !== isTagLabelTypeDrawnRegion)) {
if (category === FeatureCategory.Checkbox && isTagLabelTypeDrawnRegion) {
toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, { otherCatagory: FeatureCategory.Checkbox }));
} else if (isTagLabelTypeDrawnRegion) {
this.replaceConfirmRef.current.open(tag, props);
} 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 {
this.replaceConfirmRef.current.open(tag, props);
}
@ -541,14 +589,14 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
toast.warn(strings.tags.warnings.checkboxPerTagLimit);
return;
}
if(tagCategory===FeatureCategory.Checkbox&&category!==FeatureCategory.Checkbox){
if (tagCategory === FeatureCategory.Checkbox && category !== FeatureCategory.Checkbox) {
toast.warn(strings.tags.warnings.notCompatibleTagType);
return;
}
onTagClick(tag);
deselect = false;
} else {
toast.warn(strings.tags.warnings.notCompatibleTagType, {autoClose: 7000});
toast.warn(strings.tags.warnings.notCompatibleTagType, { autoClose: 7000 });
}
}
this.setState({
@ -597,16 +645,6 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
}
}
public getTagCategory = (tagType: string) => {
switch (tagType) {
case FieldType.SelectionMark:
case "checkbox":
return "checkbox";
default:
return "text";
}
}
private onSearchKeyDown = (event: KeyboardEvent): void => {
if (event.key === "Escape") {
this.setState({
@ -653,6 +691,21 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
}
}
private addTableTag = (tableConfig: any) => {
const newTag: ITableTag = {
name: tableConfig.name,
color: getNextColor(this.state.tags),
type: tableConfig.type,
format: tableConfig.format,
documentCount: 0,
itemType: tableConfig.itemType,
fields: tableConfig.fields,
definition: tableConfig.definition,
visualizationHint: tableConfig.visualizationHint,
};
this.addTag(newTag);
}
private validateTagLength = (tag: ITag) => {
if (!tag.name.trim().length) {
throw new Error(strings.tags.warnings.emptyName);
@ -669,7 +722,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
}
private onHideContextualMenu = () => {
this.setState({tagOperation: TagOperationMode.None});
this.setState({ tagOperation: TagOperationMode.None });
}
private getContextualMenuItems = (): IContextualMenuItem[] => {
@ -686,8 +739,11 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
},
text: tag.type ? tag.type : strings.tags.toolbar.type,
subMenuProps: {
items: this.getTypeSubMenuItems(),
items: this.getTypeSubMenuItems()
},
submenuIconProps: {
iconName: tag.type !== FieldType.Object ? "ChevronRight" : ""
}
},
{
key: "format",
@ -698,6 +754,18 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
subMenuProps: {
items: this.getFormatSubMenuItems(),
},
submenuIconProps: {
iconName: tag.type !== FieldType.Object && tag.type !== FieldType.Array ? "ChevronRight" : ""
}
},
{
key: "reconfigureTable",
iconProps: {
iconName: "EditTable",
},
text: strings.tags.regionTableTags.tableLabeling.buttons.reconfigureTable,
onClick: () => this.props.setTagInputMode(TagInputMode.ConfigureTable, tag as ITableTag),
},
{
key: "divider_1",
@ -738,23 +806,28 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
onClick: this.onMenuItemClick,
},
];
return menuItems;
return tag.type === FieldType.Object || tag.type === FieldType.Array ? menuItems : menuItems.filter((item) => (item.key !== "reconfigureTable"));
// return menuItems;
}
private isTypeCompatibleWithTag = (tag, type) => {
// If free tag we can assign any type
if (tag && tag.documentCount <= 0) {
if (tag && tag.documentCount <= 0 && tag.type !== FieldType.Object && tag.type !== FieldType.Array) {
return true;
}
const tagType = this.getTagCategory(tag.type);
const menuItemType = this.getTagCategory(type);
const tagType = getTagCategory(tag.type);
const menuItemType = getTagCategory(type);
return tagType === menuItemType;
}
// here
private getTypeSubMenuItems = (): IContextualMenuItem[] => {
const tag = this.state.selectedTag;
const types = Object.values(FieldType);
let types = Object.values(FieldType);
if (tag.type === FieldType.Object || tag.type === FieldType.Array) {
return []
} else {
types = types.filter((i) => i !== FieldType.Array && i !== FieldType.Object)
}
return types.map((type) => {
const isCompatible = this.isTypeCompatibleWithTag(tag, type);
return {
@ -771,6 +844,9 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
private getFormatSubMenuItems = (): IContextualMenuItem[] => {
const tag = this.state.selectedTag;
const formats = filterFormat(tag.type);
if (tag.type === FieldType.Object || tag.type === FieldType.Array) {
return []
}
return formats.map((format) => {
return {

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

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

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

@ -1,12 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import {FontIcon, IconButton} from "@fluentui/react";
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 React, { Fragment, MouseEvent } from "react";
import { strings } from "../../../../common/strings";
import { FieldFormat, FieldType, ILabel, ITableLabel, ITag, TagInputMode } from "../../../../models/applicationState";
import { tagIndexKeys } from "./tagIndexKeys";
import TagInputItemLabel from "./tagInputItemLabel";
export interface ITagClickProps {
@ -41,9 +41,11 @@ export interface ITagInputItemProps {
onClick: (tag: ITag, props: ITagClickProps) => void;
/** Apply new name to tag */
onRename: (oldTag: ITag, newName: string, cancelCallback: () => void) => void;
onLabelEnter: (label: ILabel) => void;
onLabelEnter: (label: ILabel|ITableLabel) => void;
onLabelLeave: (label: ILabel) => void;
onTagChanged?: (oldTag: ITag, newTag: ITag) => void;
handleLabelTable: (tagInputMode: TagInputMode, selectedTableTagToLabel) => void;
addRowToDynamicTable: () => void;
onTagDoubleClick?: (label: ILabel) => void;
}
@ -185,20 +187,23 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
<FontIcon iconName="Link" className="pl-1" />
}
<div className="tag-name-body">
<input
ref={this.onInputRef}
style={{display: this.state.isRenaming ? "block" : "none"}}
className={`tag-name-editor ${this.getContentClassName()}`}
type="text"
defaultValue={this.props.tag.name}
onKeyDown={(e) => this.onInputKeyDown(e)}
onBlur={this.onInputBlur}
autoFocus={true}
/>
{!this.state.isRenaming && <span title={spanValue} className={this.getContentClassName()}>
{spanValue}
</span>}
{
this.state.isRenaming
?
<input
ref={this.onInputRef}
className={`tag-name-editor ${this.getContentClassName()}`}
type="text"
defaultValue={this.props.tag.name}
onKeyDown={(e) => this.onInputKeyDown(e)}
onBlur={this.onInputBlur}
autoFocus={true}
/>
:
<span title={this.props.tag.name} className={this.getContentClassName()}>
{this.props.tag.name}
</span>
}
</div>
<div className={"tag-icons-container"}>
{(displayIndex !== null)
@ -211,7 +216,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>
@ -219,46 +224,65 @@ 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) =>
<Fragment key={idx}>
<div className="tag-item-label-container">
{(confidence||revised) &&
<div className="tag-item-label-container-item1">
{!revised && confidence &&
<div className="tag-item-confidence">
{confidence}
</div>
if (this.props.tag.type === FieldType.Object || this.props.tag.type === FieldType.Array) {
return (
<div
className={"tag-item-label px-2"}
onClick={() => {
this.props.handleLabelTable(TagInputMode.LabelTable, this.props.tag);
this.props.onLabelLeave(this.props.labels[0]);
}}
onMouseEnter={() => this.props.onLabelEnter(this.props.labels[0])}
onMouseLeave={() => this.props.onLabelLeave(this.props.labels[0])}
>
<FontIcon
className="pr-1 pl-1" iconName="Table"
/>
Click to assign labels
</div>
);
} else {
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) =>
<Fragment key={idx}>
<div className="tag-item-label-container">
{(confidence || revised) &&
<div className="tag-item-label-container-item1">
{!revised && 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={strings.tags.preText.autoLabel}/>
}
{revised &&
<FontIcon iconName="StatusCircleCheckmark" className="ms-Icon-25px" />
{(label.originValue?.length > 0 || label.value?.length > 0) &&
<TagInputItemLabel
label={label}
value={label.value}
isOrigin={false}
onLabelEnter={this.props.onLabelEnter}
onLabelLeave={this.props.onLabelLeave}
prefixText={revised ? strings.tags.preText.revised : undefined}/>
}
</div>
}
<div className="tag-item-label-container-item2">
{this.props.showOriginLabels && label.originValue &&
<TagInputItemLabel
label={label}
isOrigin={true}
value={label.originValue}
prefixText={strings.tags.preText.autoLabel}
/>
}
{(label.originValue?.length > 0 || label.value?.length > 0) && <TagInputItemLabel
label={label}
value={label.value}
isOrigin={false}
onLabelEnter={this.props.onLabelEnter}
onLabelLeave={this.props.onLabelLeave}
prefixText={revised ? strings.tags.preText.revised : undefined}
/>}
</div>
</div>
</Fragment>);
</Fragment>);
}
}
private onInputRef = (element: HTMLInputElement) => {
this.inputElement = element;
@ -312,7 +336,7 @@ 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);
}

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

@ -2,11 +2,12 @@
// Licensed under the MIT license.
import React from "react";
import {ILabel, IFormRegion} from "../../../../models/applicationState";
import {FontIcon} from "@fluentui/react";
import { ILabel, IFormRegion, ITag } from "../../../../models/applicationState";
import { FontIcon } from "@fluentui/react";
export interface ITagInputItemLabelProps {
label: ILabel;
tag?: ITag;
value: IFormRegion[];
isOrigin: boolean;
onLabelEnter?: (label: ILabel) => void;
@ -14,28 +15,40 @@ export interface ITagInputItemLabelProps {
prefixText?:string
}
export interface ITagInputItemLabelState {}
export interface ITagInputItemLabelState { }
export default class TagInputItemLabel extends React.Component<ITagInputItemLabelProps, ITagInputItemLabelState> {
public render() {
const texts = [];
let hasEmptyTextValue = false;
this.props.value?.forEach((formRegion: IFormRegion, idx) => {
if (formRegion.text === "") {
hasEmptyTextValue = true;
} else {
texts.push(formRegion.text);
}
})
const text = texts.join(" ");
export default function TagInputItemLabel(props: ITagInputItemLabelProps) {
const { label, onLabelEnter, onLabelLeave, tag = null , value} = props
const texts = [];
let hasEmptyTextValue = false;
value?.forEach((formRegion: IFormRegion, idx) => {
if (formRegion.text === "") {
hasEmptyTextValue = true;
} else {
texts.push(formRegion.text);
}
})
const text = texts.join(" ");
const handleMouseEnter = () => {
if (props.onLabelEnter) {
onLabelEnter(label);
}
};
const handleMouseLeave = () => {
if (props.onLabelLeave) {
onLabelLeave(label);
}
};
return (
<div
className={[this.props.isOrigin ? "tag-item-label-origin" : "tag-item-label", "flex-center", "px-2"].join(" ")}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={[props.isOrigin ? "tag-item-label-origin" : "tag-item-label", "flex-center", "px-2"].join(" ")}
>
<div className="flex-center">
{text ? this.props.prefixText : undefined} {text}
{text ? props.prefixText : undefined} {text}
{hasEmptyTextValue &&
<FontIcon className="pr-1 pl-1" iconName="FieldNotChanged" />
}
@ -43,16 +56,3 @@ export default class TagInputItemLabel extends React.Component<ITagInputItemLabe
</div>
);
}
private handleMouseEnter = () => {
if (this.props.onLabelEnter) {
this.props.onLabelEnter(this.props.label);
}
}
private handleMouseLeave = () => {
if (this.props.onLabelLeave) {
this.props.onLabelLeave(this.props.label);
}
}
}

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

@ -2,9 +2,9 @@
// 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 { ITableRegion, ITableTag, ITag, TagInputMode } from "../../../../models/applicationState";
import {constants} from "../../../../common/constants";
enum Categories {
@ -20,6 +20,8 @@ export interface ITagInputToolbarProps {
selectedTag: ITag;
/** Function to call when add tags button is clicked */
onAddTags: () => void;
setTagInputMode?: (tagInputMode: TagInputMode, selectedTableTagToLabel?: ITableTag, selectedTableTagBody?: ITableRegion[][][]) => void;
/** Function to call when search tags button is clicked */
onSearchTags: () => void;
/** Function to call when lock tags button is clicked */
@ -70,6 +72,16 @@ export default class TagInputToolbar extends React.Component<ITagInputToolbarPro
category: Categories.General,
handler: this.handleAdd,
},
{
displayName: strings.tags.toolbar.addTable,
icon: "AddTable",
category: Categories.General,
handler: this.handleAddTable,
},
{
displayName: strings.tags.toolbar.vertiline,
category: Categories.Separator,
},
{
displayName: this.state.tagFilterToggled ? strings.tags.toolbar.showAllTags : strings.tags.toolbar.onlyShowCurrentPageTags,
icon: this.state.tagFilterToggled ? "ClearFilter" : "Filter",
@ -200,6 +212,10 @@ export default class TagInputToolbar extends React.Component<ITagInputToolbarPro
});
}
private handleAddTable = () => {
this.props.setTagInputMode(TagInputMode.ConfigureTable, null, null);
}
private handleSearch = () => {
this.props.onSearchTags();
}

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

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

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

@ -43,6 +43,7 @@ describe("Editor Canvas", () => {
lockedTags: [],
hoveredLabel: null,
appSettings: null,
highlightedTableCell: null,
};
const assetPreviewProps: IAssetPreviewProps = {

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

@ -9,7 +9,7 @@ import {
EditorMode, IAssetMetadata,
IProject, IRegion, RegionType,
AssetType, ILabelData, ILabel,
ITag, IAsset, IFormRegion, FeatureCategory, FieldType, FieldFormat, LabelType, AssetLabelingState, APIVersionPatches, AssetState
ITag, IAsset, IFormRegion, FeatureCategory, FieldType, FieldFormat, ImageMapParent, LabelType, ITableRegion, ITableTag, ITableLabel, ITableCellLabel, AssetLabelingState, APIVersionPatches, TableVisualizationHint, AssetState
} from "../../../../models/applicationState";
import CanvasHelpers from "./canvasHelpers";
import { AssetPreview } from "../../common/assetPreview/assetPreview";
@ -51,7 +51,7 @@ export interface ICanvasProps extends React.Props<Canvas> {
editorMode: EditorMode;
project: IProject;
lockedTags: string[];
hoveredLabel: ILabel;
hoveredLabel: ILabel | any;
isRunningOCRs?: boolean;
children?: ReactElement<AssetPreview>;
setTableToView?: (tableToView: object, tableToViewId: string) => void;
@ -64,9 +64,11 @@ export interface ICanvasProps extends React.Props<Canvas> {
onRunningAutoLabelingStatusChanged?: (isRunning: boolean) => void;
onTagChanged?: (oldTag: ITag, newTag: ITag) => void;
runOcrForAllDocs?: (runForAllDocs: boolean) => void;
handleLabelTable?: () => void;
runAutoLabelingOnNextBatch?: (batchSize: number) => Promise<void>;
onAssetDeleted?: () => void;
onPageLoaded?: (pageNumber: number) => void;
highlightedTableCell: any;
}
export interface ICanvasState {
@ -127,6 +129,7 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
project: null,
lockedTags: [],
hoveredLabel: null,
highlightedTableCell: null,
appSettings: null,
};
@ -211,12 +214,14 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
await this.loadOcr();
this.loadLabelData(asset);
});
} else if (this.isLabelDataChanged(this.props, prevProps)
|| (prevProps.project
&& this.needUpdateAssetRegionsFromTags(prevProps.project.tags, this.props.project.tags))) {
} else if (
this.isLabelDataChanged(this.props, prevProps)
|| this.isTableLabelDataChanged(this.props, prevProps)
|| (prevProps.project && this.needUpdateAssetRegionsFromTags(prevProps.project.tags, this.props.project.tags))) {
this.setState({
currentAsset: this.props.selectedAsset
}, () => {
const newRegions = this.convertLabelDataToRegions(this.props.selectedAsset.labelData);
this.updateAssetRegions(newRegions);
this.redrawAllFeatures();
@ -228,12 +233,18 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
});
}
if (this.props.hoveredLabel !== prevProps.hoveredLabel) {
if (this.props.hoveredLabel !== prevProps.hoveredLabel || this.props.highlightedTableCell !== prevProps.highlightedTableCell) {
this.imageMap.getAllLabelFeatures().map(this.updateHighlightStatus);
this.imageMap.getAllDrawnLabelFeatures().map(this.updateHighlightStatus);
}
}
public temp = () => {
const newRegions = this.convertLabelDataToRegions(this.props.selectedAsset.labelData);
this.updateAssetRegions(newRegions);
this.redrawAllFeatures();
}
public render = () => {
const hostStyles: Partial<ITooltipHostStyles> = {
root: {
@ -245,6 +256,7 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
display: this.state.tableIconTooltip.display,
},
};
return (
<div style={{ width: "100%", height: "100%" }}>
<KeyboardBinding
@ -399,10 +411,10 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
const result = await predictService.getPrediction(assetPath);
const assetService = new AssetService(this.props.project);
const assetMetadata = assetService.getAssetPredictMetadata(asset, result);
if(assetMetadata) {
if (assetMetadata) {
await this.props.onAssetMetadataChanged(assetMetadata);
}
} catch(err){
} catch (err) {
this.setState({
isError: true,
errorTitle: err.title,
@ -442,8 +454,8 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
* Toggles tag on all selected regions
* @param selectedTag Tag name
*/
public applyTag = (tag: string) => {
const selectedRegions = this.getSelectedRegions();
public applyTag = (tag: string, rowIndex?: number, columnIndex?: number) => {
const selectedRegions: IRegion[] = this.getSelectedRegions();
const regionsEmpty = !selectedRegions || !selectedRegions.length;
if (!tag || regionsEmpty) {
return;
@ -453,16 +465,47 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
return;
}
let regions: IRegion[] = [];
const inputTag: ITag[] = this.props.project.tags.filter((t) => t.name === tag);
if (selectedRegions.length > 0) {
const labelsData = this.state.currentAsset.labelData;
if (labelsData) {
const relatedLabel = labelsData.labels.find((label) => label.label === tag);
let relatedLabel;
if (inputTag[0].type === FieldType.Array || inputTag[0].type === FieldType.Object) {
let rowKey;
let columnKey;
if (inputTag[0].type === FieldType.Array) {
rowKey = rowIndex.toString();
columnKey = (inputTag as ITableTag[])[0].definition.fields[columnIndex].fieldKey;
relatedLabel = labelsData.labels.find((label) => label.label === (this.encodeLabelString(tag) + "/" + this.encodeLabelString(rowKey) + "/" + this.encodeLabelString(columnKey)));
} else {
if ((inputTag as ITableTag[])[0].visualizationHint === TableVisualizationHint.Vertical) {
rowKey = (inputTag as ITableTag[])[0].fields[rowIndex].fieldKey;
columnKey = (inputTag as ITableTag[])[0].definition.fields[columnIndex].fieldKey;
relatedLabel = labelsData.labels.find((label) => label.label === (this.encodeLabelString(tag) + "/" + this.encodeLabelString(rowKey) + "/" + this.encodeLabelString(columnKey)));
} else {
rowKey = (inputTag as ITableTag[])[0].definition.fields[rowIndex].fieldKey;
columnKey = (inputTag as ITableTag[])[0].fields[columnIndex].fieldKey;
relatedLabel = labelsData.labels.find((label) => label.label === (this.encodeLabelString(tag) + "/" + this.encodeLabelString(columnKey) + "/" + this.encodeLabelString(rowKey)));
}
}
} else {
if (labelsData.$schema === constants.labelsSchema) {
relatedLabel = labelsData.labels.find((label) => label.label === this.encodeLabelString(tag));
} else {
relatedLabel = labelsData.labels.find((label) => label.label === tag);
}
}
if (relatedLabel &&
(((relatedLabel.labelType === null || relatedLabel.labelType === undefined) && (selectedRegions[0].category === FeatureCategory.DrawnRegion))
|| (relatedLabel.labelType !== null && relatedLabel.labelType !== undefined && relatedLabel.labelType !== selectedRegions[0].category))) {
regions = this.convertLabelToRegion(relatedLabel)
regions = this.convertLabelToRegion(relatedLabel, labelsData?.$schema === constants.labelsSchema);
regions.forEach((region) => {
region.tags = [];
if (region.isTableRegion) {
delete (region as ITableRegion).isTableRegion;
delete (region as ITableRegion).columnKey;
delete (region as ITableRegion).rowKey;
}
const regionIndex = this.state.currentAsset.regions.findIndex(r => r.id === region.id);
if (regionIndex !== -1) {
this.state.currentAsset.regions.splice(regionIndex, 1, region);
@ -473,11 +516,29 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
}
const transformer: (tags: string[], tag: string) => string[] = CanvasHelpers.setSingleTag;
const inputTag = this.props.project.tags.filter((t) => t.name === tag);
for (const selectedRegion of selectedRegions) {
selectedRegion.tags = transformer(selectedRegion.tags, tag);
}
if (inputTag[0].type === FieldType.Array || inputTag[0].type === FieldType.Object) {
for (const selectedRegion of selectedRegions as ITableRegion[]) {
if (inputTag[0].type === FieldType.Array) {
selectedRegion.rowKey = "#" + (rowIndex);
selectedRegion.columnKey = (inputTag as ITableTag[])[0].definition.fields[columnIndex].fieldKey;
} else {
if ((inputTag as ITableTag[])[0].visualizationHint === TableVisualizationHint.Vertical) {
selectedRegion.rowKey = (inputTag as ITableTag[])[0].fields[rowIndex].fieldKey;
selectedRegion.columnKey = (inputTag as ITableTag[])[0].definition.fields[columnIndex].fieldKey;
} else {
selectedRegion.rowKey = (inputTag as ITableTag[])[0].definition.fields[rowIndex].fieldKey;
selectedRegion.columnKey = (inputTag as ITableTag[])[0].fields[columnIndex].fieldKey;
}
}
selectedRegion.isTableRegion = true;
}
}
this.updateRegions([...selectedRegions, ...regions]);
this.selectedRegionIds = [];
@ -486,7 +547,11 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
}
if (selectedRegions.length === 1 && selectedRegions[0].category === FeatureCategory.Checkbox) {
this.setTagType(inputTag[0], FieldType.SelectionMark);
if (inputTag[0].type === FieldType.Object || inputTag[0].type === FieldType.Array) {
// selection mark logic placeholder
} else {
this.setTagType(inputTag[0], FieldType.SelectionMark);
}
} else if (selectedRegions[0].category === FeatureCategory.DrawnRegion) {
selectedRegions.forEach((selectedRegion) => {
this.imageMap.removeDrawnRegionFeature(this.imageMap.getDrawnRegionFeatureByID(selectedRegion.id));
@ -702,8 +767,8 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
currentAsset.labelData.labelingState = this.state.currentAsset.labelData.labelingState;
}
}
if(currentAsset.labelData?.labelingState!==AssetLabelingState.AutoLabeledAndAdjusted
&&(!currentAsset.labelData||currentAsset.labelData.labels?.findIndex(label=>label.value.length>0)<0)){
if (currentAsset.labelData?.labelingState !== AssetLabelingState.AutoLabeledAndAdjusted
&& (!currentAsset.labelData || currentAsset.labelData.labels?.findIndex(label => label.value.length > 0) < 0)) {
delete currentAsset.labelData?.labelingState;
delete currentAsset.asset.labelingState;
}
@ -768,7 +833,6 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
updatedRegions.push(update);
}
}
updatedRegions.sort(this.compareRegionOrder);
this.updateAssetRegions(updatedRegions, true);
}
@ -1093,14 +1157,26 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
}
private updateHighlightStatus = (feature: any): void => {
if (this.props.hoveredLabel) {
const label = this.props.hoveredLabel;
if (this.props.hoveredLabel || this.props.highlightedTableCell) {
let label = this.props.hoveredLabel
const id = feature.get("id");
if (label.value?.find((region) =>
id === this.createRegionIdFromBoundingBox(region.boundingBoxes[0], region.page))) {
this.setFeatureProperty(feature, "highlighted", true);
if (label?.tableKey) {
const tableLableValues = [];
label.labels.forEach((i: { value: ITableLabel[]; }) => {
i.value.forEach((i: ITableLabel) => tableLableValues.push(i))
});
label = { label: label.tableKey, value: tableLableValues };
}
} else if (feature.get("highlighted")) {
if (label?.value?.find((region: { boundingBoxes: number[][]; page: number; }) =>
id === this.createRegionIdFromBoundingBox(region.boundingBoxes[0], region.page))
|| this.props.highlightedTableCell?.find(i => i.id === id)) {
this.setFeatureProperty(feature, "highlighted", true);
} else {
this.setFeatureProperty(feature, "highlighted", false);
}
}
else if (feature.get("highlighted")) {
this.setFeatureProperty(feature, "highlighted", false);
}
}
@ -1270,7 +1346,7 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
}
private loadOcr = async (force?: boolean) => {
const asset = {...this.state.currentAsset.asset};
const asset = { ...this.state.currentAsset.asset };
if (asset.isRunningOCR) {
// Skip loading OCR this time since it's running. This will be triggered again once it's finished.
@ -1280,10 +1356,10 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
const ocr = await this.ocrService.getRecognizedText(asset.path, asset.name, asset.mimeType, this.setOCRStatus, force);
if (asset.id === this.state.currentAsset.asset.id) {
// since get OCR is async, we only set currentAsset's OCR
const newAsset={};
if(asset.state===AssetState.NotVisited){
asset.state=AssetState.Visited;
newAsset["currentAsset"]={...this.state.currentAsset, asset};
const newAsset = {};
if (asset.state === AssetState.NotVisited) {
asset.state = AssetState.Visited;
newAsset["currentAsset"] = { ...this.state.currentAsset, asset };
}
this.setState({
...newAsset,
@ -1412,59 +1488,175 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
});
}
private convertLabelDataToRegions = (labelData: ILabelData): IRegion[] => {
const regions = [];
private getLabelLayers = (label: string) => {
return this.decodeLabelLayers(label?.split("/"));
}
if (labelData && labelData.labels) {
labelData.labels.forEach((label) => {
if (label.value) {
label.value.forEach((formRegion) => {
if (formRegion.boundingBoxes) {
formRegion.boundingBoxes.forEach((boundingBox, boundingBoxIndex) => {
const text = this.getBoundingBoxTextFromRegion(formRegion, boundingBoxIndex);
regions.push(this.createRegion(boundingBox, text, label.label, formRegion.page, label?.labelType));
});
}
});
}
});
private getRegionCellKeys = (layers: string[], tableTag: ITableTag) => {
let rowKey;
let columnKey;
if (tableTag.type === FieldType.Object) {
const firstLayerField = tableTag.fields.find((field) => {
return field.fieldKey === layers[1];
})?.fieldKey
const secondLayerField = tableTag.definition.fields.find((field) => {
return field.fieldKey === layers[2];
})?.fieldKey
if (!firstLayerField || !secondLayerField) {
return;
}
if (tableTag.visualizationHint === TableVisualizationHint.Vertical) {
rowKey = firstLayerField;
columnKey = secondLayerField;
} else {
rowKey = secondLayerField;
columnKey = firstLayerField;
}
} else if (tableTag.type === FieldType.Array) {
const firstLayerField = layers[1];
const secondLayerField = tableTag.definition.fields.find((field) => {
return field.fieldKey === layers[2];
})?.fieldKey;
if (!secondLayerField) {
return;
}
rowKey = "#" + firstLayerField;
columnKey = secondLayerField;
} else {
return;
}
return { rowKey, columnKey }
}
private encodeLabelString = (labelString: string): string => {
return labelString.replace(/\~/g, "~0").replace(/\//g, "~1")
}
private decodeLabelString = (labelString: string): string => {
return labelString.replace(/\~1/g, "/").replace(/\~0/g, "~")
}
private encodeLabelLayers = (layers: string[]): string[] => {
return layers.map((layer) => { return this.encodeLabelString(layer) })
}
private decodeLabelLayers = (layers: string[]): string[] => {
return layers.map((layer) => { return this.decodeLabelString(layer) })
}
private convertLabelDataToRegions = (labelData: ILabelData): IRegion[] => {
let regions = [];
const encodedSchema = labelData?.$schema === constants.labelsSchema;
labelData?.labels?.forEach((label) => {
regions = [...regions, ...this.convertLabelToRegion(label, encodedSchema)];
});
return regions;
}
private convertLabelToRegion = (label: ILabel): IRegion[] => {
private convertLabelToRegion = (label: ILabel, encodedSchema: boolean): IRegion[] => {
const labelValue = label?.label
let layers;
if (encodedSchema) {
layers = this.getLabelLayers(labelValue);
}
const regions = [];
if (label.value) {
label.value.forEach((formRegion) => {
if (formRegion.boundingBoxes) {
formRegion.boundingBoxes.forEach((boundingBox, boundingBoxIndex) => {
const text = this.getBoundingBoxTextFromRegion(formRegion, boundingBoxIndex);
regions.push(this.createRegion(boundingBox, text, label.label, formRegion.page, label?.labelType));
});
if (encodedSchema && layers?.length > 1) {
// temp check until nested tables are supported
if (layers?.length !== 3) {
return;
}
const labelsTag = this.props.project.tags.find((tag) => {
return tag.name === layers[0];
})
if (labelsTag) {
const tableTag = labelsTag as ITableTag;
const { rowKey, columnKey } = this.getRegionCellKeys(layers, tableTag);
if (!rowKey || !columnKey) {
return
}
});
label.value.forEach((formRegion) => {
if (formRegion.boundingBoxes) {
formRegion.boundingBoxes.forEach((boundingBox, boundingBoxIndex) => {
const text = this.getBoundingBoxTextFromRegion(formRegion, boundingBoxIndex);
const tx = { ...this.createRegion(boundingBox, text, labelsTag.name, formRegion.page, label?.labelType), rowKey, columnKey, isTableRegion: true } as ITableRegion;
regions.push(tx);
});
}
});
} else {
return;
}
} else {
if (label.value) {
label.value.forEach((formRegion) => {
if (formRegion.boundingBoxes) {
formRegion.boundingBoxes.forEach((boundingBox, boundingBoxIndex) => {
const text = this.getBoundingBoxTextFromRegion(formRegion, boundingBoxIndex);
if (encodedSchema) {
regions.push(this.createRegion(boundingBox, text, this.decodeLabelString(label.label), formRegion.page, label?.labelType));
} else {
regions.push(this.createRegion(boundingBox, text, label.label, formRegion.page, label?.labelType));
}
});
}
});
}
}
return regions;
}
private getTableLabelFromRegion = (tableTag: ITableTag, tableRegion: ITableRegion) => {
const columnKey = this.encodeLabelString(tableRegion.columnKey);
const rowKey = this.encodeLabelString(tableRegion.rowKey);
const tableName = this.encodeLabelString(tableTag.name);
if (tableTag.type === FieldType.Array) {
return tableName + "/" + rowKey.slice(1) + "/" + columnKey;
} else if (tableTag.visualizationHint === TableVisualizationHint.Vertical) {
return tableName + "/" + rowKey + "/" + columnKey;
} else {
return tableName + "/" + columnKey + "/" + rowKey;
}
}
private convertRegionsToLabelData = (regions: IRegion[], assetName: string) => {
const labels = (this.props.selectedAsset
&& this.props.selectedAsset.labelData
&& this.props.selectedAsset.labelData.labels
&& this.props.selectedAsset.labelData.labels.map(label => ({
...label, value: []
}))) || [];
const labelData: ILabelData = {
$schema: constants.labelsSchema,
document: decodeURIComponent(assetName).split("/").pop(),
labels: [] as ILabel[],
};
const labels = (this.props?.selectedAsset?.labelData?.labels?.map(label => {
if (this.props.selectedAsset.labelData.$schema === constants.labelsSchema) {
return ({
...label,
value: []
})
} else {
return ({
...label,
label: this.encodeLabelString(label.label),
value: []
})
}
})) || [];
const selectedRegions = this.getSelectedRegions();
if (selectedRegions.length > 0) {
const intersectionResult = _.intersection(selectedRegions, regions);
if (intersectionResult.length === 0) {
const relatedLabels = labels.filter(label => selectedRegions.find(sr => sr.tags.find(t => t === label.label)));
relatedLabels?.forEach(relatedLabel=>{
relatedLabels?.forEach(relatedLabel => {
if (relatedLabel && relatedLabel.confidence) {
const originLabel = this.props.selectedAsset!.labelData?.labels?.find(a => a.label === relatedLabel.label);
if (originLabel) {
relatedLabel.revised = true;
if(!relatedLabel.originValue){
if (!relatedLabel.originValue) {
relatedLabel.originValue = [...originLabel.value];
}
}
@ -1478,7 +1670,9 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
item.value?.findIndex(v => v.boundingBoxes?.findIndex(b =>
_.isEqual(b, boundingBox)) >= 0 && v.page === region.pageNumber) >= 0);
}
regions.sort(this.compareRegionOrder);
regions.forEach((region) => {
const labelType = this.getLabelType(region.category);
const boundingBox = region.id.split(",").map(parseFloat);
@ -1488,12 +1682,24 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
boundingBoxes: [boundingBox],
} as IFormRegion;
region.tags.forEach((tag) => {
const label = labels.find(label => label.label === tag);
let label;
if (region.isTableRegion) {
const tableRegion = region as ITableRegion;
const tableTag = this.props.project.tags.find((projectTag) => tag === projectTag.name) as ITableTag;
if (!tableTag) return
label = labels.find(label => label?.label === this.getTableLabelFromRegion(tableTag, tableRegion));
} else {
if (this.props.selectedAsset.labelData.$schema === constants.labelsSchema) {
label = labels.find(label => this.decodeLabelString(label?.label) === tag);
} else {
label = labels.find(label => label?.label === tag);
}
}
if (label) {
const originLabel = this.props.selectedAsset!.labelData?.labels?.find(a => a.label === tag);
if (originLabel && label.confidence && region.changed) {
label.revised = true;
if(!label.originValue){
if (!label.originValue) {
label.originValue = [...originLabel.value];
}
}
@ -1507,34 +1713,43 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
}
}
}
if (originLabel && region.changed && label.labelType !== labelType) {
if (labelType) {
label.labelType = labelType;
} else {
delete label.labelType;
}
label.value.push(formRegion);
} else {
let newLabel;
let labelName = this.encodeLabelString(tag);
if (region.isTableRegion) {
const tableRegion = region as ITableRegion;
const tableTag = this.props.project.tags.find((projectTag) => tag === projectTag.name) as ITableTag;
if (!tableTag) return
labelName = this.getTableLabelFromRegion(tableTag, tableRegion);
}
if (labelType) {
newLabel = {
label: tag,
label: labelName,
key: null,
labelType,
value: [formRegion],
} as ILabel;
} else {
newLabel = {
label: tag,
label: labelName,
key: null,
value: [formRegion],
} as ILabel;
}
labels.push(newLabel);
}
labelData.labels = [...labels]
});
});
const labelData:ILabelData={
document: decodeURIComponent(assetName).split("/").pop(),
labels: [...labels],
}
labelData.document = decodeURIComponent(assetName).split("/").pop();
labelData.labels = labelData.labels.filter((label) => label.value.length > 0 && !label.revised);
return labelData;
}
@ -1777,7 +1992,9 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
}
private compareLabelChanged(newLabels: ILabel[], prevLabels: ILabel[]): boolean {
if (newLabels.length > 0) {
if (newLabels.length !== prevLabels.length) {
return true;
} else if (newLabels.length > 0) {
const newFieldNames = newLabels.map((label) => label.label);
const prevFieldNames = prevLabels.map((label) => label.label);
if (_.isEqual(newFieldNames.sort(), prevFieldNames.sort())) {
@ -1785,7 +2002,32 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
const newValue = newLabels.find(label => label.label === name).value?.map(region => region.boundingBoxes).join(",");
const prevValue = prevLabels.find(label => label.label === name).value?.map(region => region.boundingBoxes).join(",");
if (newValue !== prevValue) {
return true;
return true;
}
}
return false;
}
else {
return true;
}
}
}
private isTableLabelDataChanged = (newProps: ICanvasProps, prevProps: ICanvasProps): boolean => {
const newLabels = _.get(newProps, "selectedAsset.labelData.tableLabels", []) as ITableLabel[];
const prevLabels = _.get(prevProps, "selectedAsset.labelData.tableLabels", []) as ITableLabel[];
if (newLabels.length !== prevLabels.length) {
return true;
} else if (newLabels.length > 0) {
const newFieldNames = newLabels.map((label) => label.tableKey);
const prevFieldNames = prevLabels.map((label) => label.tableKey);
if (_.isEqual(newFieldNames.sort(), prevFieldNames.sort())) {
for (const name of newFieldNames) {
const newValue = newLabels.find(label => label.tableKey === name);
const prevValue = prevLabels.find(label => label.tableKey === name);
if (!_.isEqual(newValue, prevValue)) {
return true;
}
}
return false;
@ -2395,7 +2637,7 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
}
async focusOnLabel(label: ILabel) {
const page = label.value[ 0 ]?.page;
const page = label.value[0]?.page;
if (page && this.state.currentPage !== page) {
await this.goToPage(page);
}

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

@ -2,14 +2,15 @@
// Licensed under the MIT license.
import * as React from "react";
import {CommandBar, ICommandBarItemProps} from "@fluentui/react/lib/CommandBar";
import {ICustomizations, Customizer} from "@fluentui/react/lib/Utilities";
import {getDarkGreyTheme} from "../../../../common/themes";
import {strings} from '../../../../common/strings';
import {ContextualMenuItemType} from "@fluentui/react";
import {IProject, IAssetMetadata, AssetLabelingState} from "../../../../models/applicationState";
import { CommandBar, ICommandBarItemProps } from "@fluentui/react/lib/CommandBar";
import { ICustomizations, Customizer } from "@fluentui/react/lib/Utilities";
import { getDarkGreyTheme } from "../../../../common/themes";
import { interpolate, strings } from '../../../../common/strings';
import { ContextualMenuItemType } from "@fluentui/react";
import { IProject, IAssetMetadata, AssetLabelingState } from "../../../../models/applicationState";
import _ from "lodash";
import "./canvasCommandBar.scss";
import { constants } from "../../../../common/constants";
interface ICanvasCommandBarProps {
handleZoomIn: () => void;
@ -24,7 +25,6 @@ interface ICanvasCommandBarProps {
project?: IProject;
selectedAsset?: IAssetMetadata;
handleRotateImage: (degrees: number) => void;
drawRegionMode?: boolean;
connectionType?: string;
layers?: any;
@ -213,8 +213,8 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
{
key: "deleteAsset",
text: strings.editorPage.asset.delete.title,
iconProps: {iconName: "Delete"},
onClick: () => {if (props.handleAssetDeleted) props.handleAssetDeleted();},
iconProps: { iconName: "Delete" },
onClick: () => { if (props.handleAssetDeleted) props.handleAssetDeleted(); },
}
],
},

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

@ -425,7 +425,6 @@ describe("Editor Page Component", () => {
await waitForSelectedAsset(wrapper);
const tagToDelete = project.tags[project.tags.length - 1];
wrapper.find(TagInput).props().onTagDeleted(tagToDelete.name);
// Accept the modal delete warning
wrapper.update();

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

@ -13,7 +13,7 @@ import { strings, interpolate } from "../../../../common/strings";
import {
AssetState, AssetType, EditorMode, FieldType,
IApplicationState, IAppSettings, IAsset, IAssetMetadata,
ILabel, IProject, IRegion, ISize, ITag, FeatureCategory, FieldFormat, AssetLabelingState,
ILabel, IProject, IRegion, ISize, ITag, FeatureCategory, TagInputMode, FieldFormat, ITableTag, ITableRegion, AssetLabelingState, ITableConfigItem, TableVisualizationHint
} from "../../../../models/applicationState";
import IApplicationActions, * as applicationActions from "../../../../redux/actions/applicationActions";
import IProjectActions, * as projectActions from "../../../../redux/actions/projectActions";
@ -31,14 +31,15 @@ import EditorSideBar from "./editorSideBar";
import Alert from "../../common/alert/alert";
import Confirm from "../../common/confirm/confirm";
import { OCRService, OcrStatus } from "../../../../services/ocrService";
import { throttle } from "../../../../common/utils";
import { getTagCategory, throttle } from "../../../../common/utils";
import { constants } from "../../../../common/constants";
import PreventLeaving from "../../common/preventLeaving/preventLeaving";
import { Spinner, SpinnerSize } from "@fluentui/react/lib/Spinner";
import { getPrimaryGreenTheme, getPrimaryRedTheme } from "../../../../common/themes";
import { getPrimaryBlueTheme, getPrimaryGreenTheme, getPrimaryRedTheme } from "../../../../common/themes";
import { toast } from "react-toastify";
import { PredictService } from "../../../../services/predictService";
import { AssetService } from "../../../../services/assetService";
import clone from "rfdc";
/**
* Properties for Editor Page
@ -96,7 +97,13 @@ export interface IEditorPageState {
errorMessage?: string;
tableToView: object;
tableToViewId: string;
tagInputMode: TagInputMode;
selectedTableTagToLabel: ITableTag;
selectedTableTagBody: ITableRegion[][][];
rightSplitPaneWidth?: number;
reconfigureTableConfirm?: boolean;
pageNumber: number;
highlightedTableCellRegions: ITableRegion[];
}
function mapStateToProps(state: IApplicationState) {
@ -133,7 +140,11 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
hoveredLabel: null,
tableToView: null,
tableToViewId: null,
pageNumber: 1
tagInputMode: TagInputMode.Basic,
selectedTableTagToLabel: null,
selectedTableTagBody: [[]],
pageNumber: 1,
highlightedTableCellRegions: null,
};
private tagInputRef: RefObject<TagInput>;
@ -144,7 +155,12 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
private renameCanceled: () => void;
private deleteTagConfirm: React.RefObject<Confirm> = React.createRef();
private deleteDocumentConfirm: React.RefObject<Confirm> = React.createRef();
private reconfigTableConfirm: React.RefObject<Confirm> = React.createRef();
private replaceConfirmRef = React.createRef<Confirm>();
private isUnmount: boolean = false;
public initialRightSplitPaneWidth: number;
private isOCROrAutoLabelingBatchRunning = false;
constructor(props) {
@ -191,6 +207,9 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
const labels = (selectedAsset &&
selectedAsset.labelData &&
selectedAsset.labelData.labels) || [];
const tableLabels = (selectedAsset &&
selectedAsset.labelData &&
selectedAsset.labelData.tableLabels) || [];
const needRunOCRButton = assets.some((asset) => asset.state === AssetState.NotVisited);
@ -198,6 +217,9 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
return (<div>Loading...</div>);
}
const isBasicInputMode = this.state.tagInputMode === TagInputMode.Basic;
this.initialRightSplitPaneWidth = isBasicInputMode ? 290 : 520;
return (
<div className="editor-page skipToMainContent" id="pageEditor">
{this.state.tableToView !== null &&
@ -208,15 +230,16 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
}
{
tagIndexKeys.map((index) =>
(<KeyboardBinding
displayName={strings.editorPage.tags.hotKey.apply}
key={index}
keyEventType={KeyEventType.KeyDown}
accelerators={[`${index}`]}
icon={"fa-tag"}
handler={this.handleTagHotKey} />))
(<KeyboardBinding
displayName={strings.editorPage.tags.hotKey.apply}
key={index}
keyEventType={KeyEventType.KeyDown}
accelerators={[`${index}`]}
icon={"fa-tag"}
handler={this.handleTagHotKey} />))
}
<SplitPane split="vertical"
<SplitPane
split="vertical"
defaultSize={this.state.thumbnailSize.width}
minSize={150}
maxSize={325}
@ -251,19 +274,33 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
onAssetLoaded={this.onAssetLoaded}
thumbnailSize={this.state.thumbnailSize}
/>
</div>
<div className="editor-page-content" onClick={this.onPageClick}>
<SplitPane split="vertical"
primary="second"
maxSize={625}
minSize={290}
maxSize={isBasicInputMode ? 400 : 700}
minSize={this.initialRightSplitPaneWidth}
className={"right-vertical_splitPane"}
pane1Style={{ height: "100%" }}
pane2Style={{ height: "auto" }}
resizerStyle={{ width: "5px", margin: "0px", border: "2px", background: "transparent" }}
onChange={() => this.resizeCanvas()}>
resizerStyle={{
width: "5px",
margin: "0px",
border: "2px",
}}
onChange={(width) => {
if (!isBasicInputMode) {
this.setState({ rightSplitPaneWidth: width > 700 ? 700 : width }, () => {
this.resizeCanvas();
});
this.resizeCanvas();
}
}}>
<div className="editor-page-content-main" >
<div className="editor-page-content-main-body" onClick={this.onPageContainerClick}>
{selectedAsset &&
<Canvas
ref={this.canvas}
selectedAsset={this.state.selectedAsset}
@ -286,6 +323,8 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
onPageLoaded={this.onPageLoaded}
runAutoLabelingOnNextBatch={this.runAutoLabelingOnNextBatch}
appSettings={this.props.appSettings}
handleLabelTable={this.handleLabelTable}
highlightedTableCell={this.state.highlightedTableCellRegions}
>
<AssetPreview
controlsEnabled={this.state.isValid}
@ -302,6 +341,8 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
lockedTags={this.state.lockedTags}
selectedRegions={this.state.selectedRegions}
labels={labels}
encoded={selectedAsset?.labelData?.$schema === constants.labelsSchema}
tableLabels={tableLabels}
pageNumber={this.state.pageNumber}
onChange={this.onTagsChanged}
onLockedTagsChange={this.onLockedTagsChanged}
@ -312,11 +353,23 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
onLabelEnter={this.onLabelEnter}
onLabelLeave={this.onLabelLeave}
onTagChanged={this.onTagChanged}
onTagDoubleClick={this.onLabelDoubleClicked}
ref={this.tagInputRef}
setTagInputMode={this.setTagInputMode}
tagInputMode={this.state.tagInputMode}
handleLabelTable={this.handleLabelTable}
selectedTableTagToLabel={this.state.selectedTableTagToLabel}
handleTableCellClick={this.handleTableCellClick}
handleTableCellMouseEnter={this.handleTableCellMouseEnter}
handleTableCellMouseLeave={this.handleTableCellMouseLeave}
selectedTableTagBody={this.state.selectedTableTagBody}
splitPaneWidth={this.state.rightSplitPaneWidth}
reconfigureTableConfirm={this.reconfigureTableConfirm}
addRowToDynamicTable={this.addRowToDynamicTable}
onTagDoubleClick={this.onLabelDoubleClicked}
/>
<Confirm
title={strings.editorPage.tags.rename.title}
loadMessage={"Renaming..."}
ref={this.renameTagConfirm}
message={strings.editorPage.tags.rename.confirmation}
confirmButtonTheme={getPrimaryRedTheme()}
@ -342,6 +395,22 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
onConfirm={this.onAssetDeleted}
/>
}
<Confirm
title={strings.tags.regionTableTags.confirm.reconfigure.title}
loadMessage={"Reconfiguring..."}
ref={this.reconfigTableConfirm}
message={strings.tags.regionTableTags.confirm.reconfigure.message}
confirmButtonTheme={getPrimaryBlueTheme()}
onConfirm={this.reconfigureTable}
/>
<Confirm
title={strings.tags.warnings.replaceAllExitingLabelsTitle}
ref={this.replaceConfirmRef}
message={strings.tags.warnings.replaceAllExitingLabels}
confirmButtonTheme={getPrimaryRedTheme()}
onConfirm={this.onTableTagClicked}
/>
</div>
</SplitPane>
</div>
@ -363,12 +432,13 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
errorMessage: undefined,
})}
/>
<PreventLeaving
when={isRunningOCRs || isCanvasRunningOCR}
message={strings.editorPage.warningMessage.PreventLeavingWhileRunningOCR}
/>
<PreventLeaving
when={isCanvasRunningAutoLabeling||isRunningAutoLabelings}
when={isCanvasRunningAutoLabeling || isRunningAutoLabelings}
message={strings.editorPage.warningMessage.PreventLeavingRunningAutoLabeling} />
</div>
);
@ -386,6 +456,108 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
private onPageClick = () => {
}
private setTagInputMode = (tagInputMode: TagInputMode, selectedTableTagToLabel: ITableTag = this.state.selectedTableTagToLabel, selectedTableTagBody: ITableRegion[][][] = this.state.selectedTableTagBody) => {
// this.resizeCanvas();
this.setState({
selectedTableTagBody,
selectedTableTagToLabel,
tagInputMode,
}, () => {
this.resizeCanvas();
});
}
private handleLabelTable = (tagInputMode: TagInputMode = this.state.tagInputMode, selectedTableTagToLabel: ITableTag = this.state.selectedTableTagToLabel) => {
if (selectedTableTagToLabel == null || !this.state.selectedAsset) {
return;
}
let rowKeys;
let columnKeys;
if (selectedTableTagToLabel.type === FieldType.Object) {
if (selectedTableTagToLabel.visualizationHint === TableVisualizationHint.Vertical) {
columnKeys = selectedTableTagToLabel.definition.fields;
rowKeys = selectedTableTagToLabel.fields;
} else {
columnKeys = selectedTableTagToLabel.fields;
rowKeys = selectedTableTagToLabel.definition.fields;
}
} else {
rowKeys = null;
columnKeys = selectedTableTagToLabel.definition.fields;
}
const selectedTableTagBody = new Array(rowKeys?.length || 1);
if (this.state.selectedTableTagToLabel?.name === selectedTableTagToLabel?.name && selectedTableTagToLabel.type === FieldType.Array) {
for (let i = 1; i < this.state.selectedTableTagBody.length; i++) {
selectedTableTagBody.push(undefined)
}
}
for (let i = 0; i < selectedTableTagBody.length; i++) {
selectedTableTagBody[i] = new Array(columnKeys.length);
}
const tagAssets = clone()(this.state.selectedAsset.regions).filter((region) => region.tags[0] === selectedTableTagToLabel.name) as ITableRegion[];
tagAssets.forEach((region => {
let rowIndex: number;
if (selectedTableTagToLabel.type === FieldType.Array) {
rowIndex = Number(region.rowKey.slice(1));
} else {
rowIndex = rowKeys.findIndex(rowKey => rowKey.fieldKey === region.rowKey)
}
for (let i = selectedTableTagBody.length; i <= rowIndex; i++) {
selectedTableTagBody.push(new Array(columnKeys.length));
}
const colIndex = columnKeys.findIndex(colKey => colKey.fieldKey === region.columnKey)
if (selectedTableTagBody[rowIndex][colIndex] != null) {
selectedTableTagBody[rowIndex][colIndex].push(region)
} else {
selectedTableTagBody[rowIndex][colIndex] = [region]
}
}));
this.setState({
selectedTableTagToLabel,
selectedTableTagBody,
}, () => {
this.setTagInputMode(tagInputMode);
});
}
private addRowToDynamicTable = () => {
const selectedTableTagBody = clone()(this.state.selectedTableTagBody)
selectedTableTagBody.push(Array(this.state.selectedTableTagToLabel.definition.fields.length));
this.setState({ selectedTableTagBody });
}
private handleTableCellClick = (rowIndex: number, columnIndex: number) => {
if (this.state?.selectedTableTagBody?.[rowIndex]?.[columnIndex]?.length > 0) {
const selectionRegionCatagory = this.state.selectedRegions[0].category;
const cellCatagory = this.state.selectedTableTagBody[rowIndex][columnIndex][0].category;
if (selectionRegionCatagory !== cellCatagory && (selectionRegionCatagory === FeatureCategory.DrawnRegion || cellCatagory === FeatureCategory.DrawnRegion)) {
this.replaceConfirmRef.current.open(this.state.selectedTableTagToLabel, rowIndex, columnIndex);
return;
}
}
this.onTableTagClicked(this.state.selectedTableTagToLabel, rowIndex, columnIndex);
}
private handleTableCellMouseEnter = (regions) => {
this.setState({ highlightedTableCellRegions: regions });
}
private handleTableCellMouseLeave = () => {
this.setState({ highlightedTableCellRegions: null });
}
/**
* Called when the asset side bar is resized
* @param newWidth The new sidebar width
@ -397,7 +569,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
height: newWidth / (4 / 3),
},
});
this.resizeCanvas()
this.resizeCanvas();
}
/**
@ -423,6 +595,13 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
}, () => this.canvas.current.applyTag(tag.name));
}
private onTableTagClicked = (tag: ITag, rowIndex: number, columnIndex: number): void => {
this.setState({
selectedTag: tag.name,
lockedTags: [],
}, () => this.canvas.current.applyTag(tag.name, rowIndex, columnIndex));
}
/**
* Open confirm dialog for tag renaming
*/
@ -438,14 +617,29 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
*/
private onTagRenamed = async (tag: ITag, newTag: ITag): Promise<void> => {
this.renameCanceled = null;
const assetUpdates = await this.props.actions.updateProjectTag(this.props.project, tag, newTag);
const selectedAsset = assetUpdates.find((am) => am.asset.id === this.state.selectedAsset.asset.id);
if (selectedAsset) {
if (tag.type === FieldType.Object || tag.type === FieldType.Array) {
const assetUpdates = await this.props.actions.reconfigureTableTag(this.props.project, tag.name, newTag.name, newTag.type, newTag.format, (newTag as ITableTag).visualizationHint, undefined, undefined, undefined, undefined);
const selectedAsset = assetUpdates.find((am) => am.asset.id === this.state.selectedAsset.asset.id);
if (selectedAsset) {
this.setState({ selectedAsset });
this.setState({
selectedAsset,
selectedTableTagToLabel: null,
selectedTableTagBody: null,
}, () => {
this.canvas.current.temp();
});
}
} else {
const assetUpdates = await this.props.actions.updateProjectTag(this.props.project, tag, newTag);
const selectedAsset = assetUpdates.find((am) => am.asset.id === this.state.selectedAsset.asset.id);
if (selectedAsset) {
if (selectedAsset) {
this.setState({ selectedAsset });
}
}
}
this.renameTagConfirm.current.close();
}
private onTagRenameCanceled = () => {
@ -458,8 +652,8 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
/**
* Open Confirm dialog for tag deletion
*/
private confirmTagDeleted = (tagName: string): void => {
this.deleteTagConfirm.current.open(tagName);
private confirmTagDeleted = (tagName: string, tagType: FieldType, tagFormat: FieldFormat): void => {
this.deleteTagConfirm.current.open(tagName, tagType, tagFormat);
}
/**
@ -473,8 +667,8 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
* Removes tag from assets and projects and saves files
* @param tagName Name of tag to be deleted
*/
private onTagDeleted = async (tagName: string): Promise<void> => {
const assetUpdates = await this.props.actions.deleteProjectTag(this.props.project, tagName);
private onTagDeleted = async (tagName: string, tagType: FieldType, tagFormat: FieldFormat): Promise<void> => {
const assetUpdates = await this.props.actions.deleteProjectTag(this.props.project, tagName, tagType, tagFormat);
const selectedAsset = assetUpdates.find((am) => am.asset.id === this.state.selectedAsset.asset.id);
if (selectedAsset) {
@ -493,7 +687,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
private getTagFromKeyboardEvent = (event: KeyboardEvent): ITag => {
const index = tagIndexKeys.indexOf(event.key);
const tags = this.props.project.tags;
if (index >= 0 && index < tags.length) {
if (index >= 0 && index < tags.length && (tags[index].type !== FieldType.Array || tags[index].type !== FieldType.Object)) {
return tags[index];
}
return null;
@ -509,7 +703,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
if (tag && selection.length) {
const { format, type, documentCount, name } = tag;
const tagCategory = this.tagInputRef.current.getTagCategory(tag.type);
const tagCategory = getTagCategory(tag.type);
const category = selection[0].category;
const labels = this.state.selectedAsset.labelData?.labels;
const isTagLabelTypeDrawnRegion = this.tagInputRef.current.labelAssignedDrawnRegion(labels, tag.name);
@ -517,11 +711,11 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
if (labelAssigned && ((category === FeatureCategory.DrawnRegion) !== isTagLabelTypeDrawnRegion)) {
if (isTagLabelTypeDrawnRegion) {
toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, { otherCatagory: category }));
toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, { otherCategory: category }));
} else if (tagCategory === FeatureCategory.Checkbox) {
toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, { otherCatagory: FeatureCategory.Checkbox }));
toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, { otherCategory: FeatureCategory.Checkbox }));
} else {
toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, { otherCatagory: FeatureCategory.Text }));
toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, { otherCategory: FeatureCategory.Text }));
}
return;
} else if (tagCategory === category || category === FeatureCategory.DrawnRegion ||
@ -560,19 +754,30 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
&& asset.state !== AssetState.NotVisited
&& asset.labelingState !== AssetLabelingState.AutoLabeled
&& asset.labelingState !== AssetLabelingState.AutoLabeledAndAdjusted) {
asset.state = _.get(assetMetadata, "labelData.labels.length", 0) > 0
&& assetMetadata.labelData.labels.findIndex(item => item.value?.length > 0) >= 0 ?
AssetState.Tagged :
AssetState.Visited;
const hasLabels = _.get(assetMetadata, "labelData.labels.length", 0) > 0;
const hasTableLabels = _.get(assetMetadata, "labelData.tableLabels.length", 0) > 0;
if (hasLabels && assetMetadata.labelData.labels.findIndex(item => item?.value?.length > 0) >= 0) {
asset.state = AssetState.Tagged
} else if (hasTableLabels && assetMetadata.labelData.tableLabels.findIndex(item => item.labels?.length > 0) >= 0) {
asset.state = AssetState.Tagged
} else {
asset.state = AssetState.Visited;
}
}
// Only update asset metadata if state changes or is different
if (initialState !== asset.state || this.state.selectedAsset !== assetMetadata) {
if (assetMetadata.labelData?.labels?.toString() !== this.state.selectedAsset.labelData?.labels?.toString()) {
if (JSON.stringify(assetMetadata.labelData) !== JSON.stringify(this.state.selectedAsset.labelData)) {
await this.updatedAssetMetadata(assetMetadata);
}
assetMetadata.asset = asset;
const newMeta = await this.props.actions.saveAssetMetadata(this.props.project, assetMetadata);
if (this.props.project.lastVisitedAssetId === asset.id) {
this.setState({ selectedAsset: newMeta });
}
@ -592,7 +797,9 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
...asset,
};
}
return {assets, isValid: true};
return { assets, isValid: true };
}, () => {
this.handleLabelTable();
});
// Workaround for if component is unmounted
if (!this.isUnmount) {
@ -605,10 +812,10 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
const assets: IAsset[] = [...preState.assets];
const assetIndex = assets.findIndex((item) => item.id === asset.id);
if (assetIndex > -1) {
const item = {...assets[assetIndex]};
const item = { ...assets[assetIndex] };
item.cachedImage = (contentSource as HTMLImageElement).src;
assets[assetIndex] = item;
return {assets};
return { assets };
}
});
}
@ -687,11 +894,33 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
tableToViewId: null,
selectedAsset: assetMetadata,
}, async () => {
await this.onAssetMetadataChanged(assetMetadata);
await this.props.actions.saveProject(this.props.project, false, false);
await this.onAssetMetadataChanged(assetMetadata);
await this.props.actions.saveProject(this.props.project, false, false);
});
}
private reconfigureTableConfirm = (originalTagName: string, tagName: string, tagType: FieldType.Array | FieldType.Object, tagFormat: FieldFormat, visualizationHint: TableVisualizationHint, deletedColumns: ITableConfigItem[], deletedRows: ITableConfigItem[], newRows: ITableConfigItem[], newColumns: ITableConfigItem[]) => {
this.setState({ reconfigureTableConfirm: true });
this.reconfigTableConfirm.current.open(originalTagName, tagName, tagType, tagFormat, visualizationHint, deletedColumns, deletedRows, newRows, newColumns);
}
private reconfigureTable = async (originalTagName: string, tagName: string, tagType: FieldType, tagFormat: FieldFormat, visualizationHint: TableVisualizationHint, deletedColumns: ITableConfigItem[], deletedRows: ITableConfigItem[], newRows: ITableConfigItem[], newColumns: ITableConfigItem[]) => {
const assetUpdates = await this.props.actions.reconfigureTableTag(this.props.project, originalTagName, tagName, tagType, tagFormat, visualizationHint, deletedColumns, deletedRows, newRows, newColumns);
const selectedAsset = assetUpdates.find((am) => am.asset.id === this.state.selectedAsset.asset.id);
if (selectedAsset) {
this.setState({
selectedAsset,
selectedTableTagToLabel: null,
selectedTableTagBody: null,
}, () => {
this.canvas.current.temp();
});
}
this.reconfigTableConfirm.current.close();
this.setState({ tagInputMode: TagInputMode.Basic, reconfigureTableConfirm: false }, () => this.resizeCanvas());
this.resizeCanvas();
}
private loadProjectAssets = async (): Promise<void> => {
if (this.loadingProjectAssets) {
return;
@ -702,7 +931,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
if (key === "cachedImage") {
return undefined;
}
else{
else {
return value;
}
}
@ -757,10 +986,10 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
const asset = this.state.assets.find((asset) => asset.id === assetId);
if (asset && (asset.state === AssetState.NotVisited || runForAll)) {
try {
this.updateAssetOCRAndAutoLabelingState({id: asset.id, isRunningOCR: true });
this.updateAssetOCRAndAutoLabelingState({ id: asset.id, isRunningOCR: true });
const ocrResult = await ocrService.getRecognizedText(asset.path, asset.name, asset.mimeType, undefined, runForAll);
if (ocrResult) {
this.updateAssetOCRAndAutoLabelingState({id: asset.id, isRunningOCR: false});
this.updateAssetOCRAndAutoLabelingState({ id: asset.id, isRunningOCR: false });
await this.props.actions.refreshAsset(this.props.project, asset.name);
}
} catch (err) {
@ -804,7 +1033,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
unlabeledAssetsBatch,
async (asset) => {
try {
this.updateAssetOCRAndAutoLabelingState({id: asset.id, isRunningAutoLabeling: true});
this.updateAssetOCRAndAutoLabelingState({ id: asset.id, isRunningAutoLabeling: true });
const predictResult = await predictService.getPrediction(asset.path);
const assetMetadata = await assetService.getAssetPredictMetadata(asset, predictResult);
await assetService.uploadPredictResultAsOrcResult(asset, predictResult);
@ -813,7 +1042,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
allAssets[asset.id] = assetMetadata.asset;
await this.props.actions.updatedAssetMetadata(this.props.project, assetMetadata);
} catch (err) {
this.updateAssetOCRAndAutoLabelingState({id: asset.id, isRunningOCR: false, isRunningAutoLabeling: false});
this.updateAssetOCRAndAutoLabelingState({ id: asset.id, isRunningOCR: false, isRunningAutoLabeling: false });
this.setState({
isError: true,
errorTitle: err.title,
@ -823,7 +1052,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
}
);
} finally {
await this.props.actions.saveProject({...this.props.project, assets: allAssets}, true, false);
await this.props.actions.saveProject({ ...this.props.project, assets: allAssets }, true, false);
this.setState({ isRunningAutoLabelings: false });
this.isOCROrAutoLabelingBatchRunning = false;
}
@ -845,7 +1074,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
this.setState((state) => {
const assets = state.assets.map((asset) => {
if (asset.id === newState.id) {
const updatedAsset = {...asset, isRunningOCR: newState.isRunningOCR || false};
const updatedAsset = { ...asset, isRunningOCR: newState.isRunningOCR || false };
if (newState.isRunningAutoLabeling !== undefined) {
updatedAsset.isRunningAutoLabeling = newState.isRunningAutoLabeling;
}
@ -858,18 +1087,18 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
assets
}
}, () => {
if (this.state.selectedAsset?.asset?.id === newState.id) {
const asset = this.state.assets.find((asset) => asset.id === newState.id);
if (this.state.selectedAsset && newState.id === this.state.selectedAsset.asset.id) {
if (asset) {
this.setState({
selectedAsset: { ...this.state.selectedAsset, asset: { ...asset } },
});
}
if (this.state.selectedAsset?.asset?.id === newState.id) {
const asset = this.state.assets.find((asset) => asset.id === newState.id);
if (this.state.selectedAsset && newState.id === this.state.selectedAsset.asset.id) {
if (asset) {
this.setState({
selectedAsset: { ...this.state.selectedAsset, asset: { ...asset } },
});
}
}
}
});
});
}
/**
@ -912,6 +1141,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
} catch (err) {
console.warn("Error computing asset size");
}
assetMetadata.regions = [...this.state.selectedAsset.regions];
this.setState({
tableToView: null,
tableToViewId: null,
@ -935,20 +1165,20 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
private onCanvasRunningOCRStatusChanged = (ocrStatus: OcrStatus) => {
if (ocrStatus === OcrStatus.done && this.state.selectedAsset?.asset?.state === AssetState.NotVisited) {
const allAssets: {[index: string]: IAsset} = _.cloneDeep(this.props.project.assets);
const allAssets: { [index: string]: IAsset } = _.cloneDeep(this.props.project.assets);
const asset = Object.values(allAssets).find(item => item.id === this.state.selectedAsset?.asset?.id);
if (asset) {
asset.state = AssetState.Visited;
Promise.all([this.props.actions.saveProject({...this.props.project, assets: allAssets}, false, false)]);
Promise.all([this.props.actions.saveProject({ ...this.props.project, assets: allAssets }, false, false)]);
}
}
this.setState({isCanvasRunningOCR: ocrStatus === OcrStatus.runningOCR});
this.setState({ isCanvasRunningOCR: ocrStatus === OcrStatus.runningOCR });
}
private onCanvasRunningAutoLabelingStatusChanged = (isCanvasRunningAutoLabeling: boolean) => {
this.setState({ isCanvasRunningAutoLabeling });
}
private onFocused = () => {
if(!this.isOCROrAutoLabelingBatchRunning){
if (!this.isOCROrAutoLabelingBatchRunning) {
this.loadProjectAssets();
}
}
@ -1002,6 +1232,75 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
}
private async updatedAssetMetadata(assetMetadata: IAssetMetadata) {
const rowDocumentCountDifference = {};
const updatedRowLabels = {};
const currentRowLabels = {};
const columnDocumentCountDifference = {};
const updatedColumnLabels = {};
const currentColumnLabels = {};
assetMetadata?.labelData?.tableLabels?.forEach((table) => {
updatedRowLabels[table.tableKey] = {};
updatedColumnLabels[table.tableKey] = {};
table.labels.forEach((label) => {
updatedRowLabels[table.tableKey][label.rowKey] = true;
updatedColumnLabels[table.tableKey][label.columnKey] = true;
})
});
this.state.selectedAsset?.labelData?.tableLabels?.forEach((table) => {
currentRowLabels[table.tableKey] = {};
currentColumnLabels[table.tableKey] = {};
table.labels.forEach((label) => {
currentRowLabels[table.tableKey][label.rowKey] = true;
currentColumnLabels[table.tableKey][label.columnKey] = true;
})
});
Object.keys(currentColumnLabels).forEach((table) => {
Object.keys(currentColumnLabels[table]).forEach((columnKey) => {
if (!updatedColumnLabels?.[table]?.[columnKey]) {
if (!(table in columnDocumentCountDifference)) {
columnDocumentCountDifference[table] = {};
}
columnDocumentCountDifference[table][columnKey] = -1;
}
});
});
Object.keys(updatedColumnLabels).forEach((table) => {
Object.keys(updatedColumnLabels[table]).forEach((columnKey) => {
if (!currentColumnLabels?.[table]?.[columnKey]) {
if (!(table in columnDocumentCountDifference)) {
columnDocumentCountDifference[table] = {};
}
columnDocumentCountDifference[table][columnKey] = 1;
}
});
});
Object.keys(currentRowLabels).forEach((table) => {
Object.keys(currentRowLabels[table]).forEach((rowKey) => {
if (!updatedRowLabels?.[table]?.[rowKey]) {
if (!(table in rowDocumentCountDifference)) {
rowDocumentCountDifference[table] = {};
}
rowDocumentCountDifference[table][rowKey] = -1;
}
});
});
Object.keys(updatedRowLabels).forEach((table) => {
Object.keys(updatedRowLabels[table]).forEach((rowKey) => {
if (!currentRowLabels?.[table]?.[rowKey]) {
if (!(table in rowDocumentCountDifference)) {
rowDocumentCountDifference[table] = {};
}
rowDocumentCountDifference[table][rowKey] = 1;
}
});
});
const assetDocumentCountDifference = {};
const updatedAssetLabels = {};
const currentAssetLabels = {};
@ -1021,6 +1320,6 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
assetDocumentCountDifference[label] = 1;
}
});
await this.props.actions.updatedAssetMetadata(this.props.project, assetDocumentCountDifference);
await this.props.actions.updatedAssetMetadata(this.props.project, assetDocumentCountDifference, columnDocumentCountDifference, rowDocumentCountDifference);
}
}

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

@ -14,6 +14,7 @@ import {
IContextualMenuProps
} from "@fluentui/react";
import Fill from "ol/style/Fill";
import Icon from "ol/style/Icon";
import Stroke from "ol/style/Stroke";
import Style from "ol/style/Style";
import React from "react";

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

@ -31,8 +31,8 @@ export interface ITableHelper {
export interface ITableState {
tableIconTooltip: any;
hoveringFeature: string;
tableToView: object;
tableToViewId: string;
tableToView?: object;
tableToViewId?: string;
}
export class TableHelper<TState extends ITableState> {

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

@ -111,7 +111,65 @@
color: #d1d1d1;
float: left;
}
.add-row-button_container {
margin-left: 1.5rem;
margin-bottom: 3rem;
margin-top: 1rem;
}
.table-view-container {
td, th {
text-align: center;
vertical-align: middle;
padding: 0.12rem 0.25rem;
}
overflow: auto;
padding-bottom: .5rem;
.column_header {
min-width: 130px;
max-width: 200px;
background-color: $lighter-3;
border: 1px solid grey;
text-align: center;
padding: .125rem .25rem;
}
.row_header {
min-width: 100px;
max-width: 200px;
border: 1px solid grey;
background-color: $lighter-3;
text-align: center;
padding: .125rem .5rem;
}
.empty_header {
border: 1px solid grey;
background-color: $lighter-3;
}
.table-cell {
text-align: center;
background-color: $darker-3;
color: rgba(255, 255, 255, 0.75);
&:hover {
background-color: $lighter-1;
}
border: 1px solid grey;
}
.hidden {
border: none;
background-color: transparent;
text-align: center;
color: rgba(255, 255, 255, 0.45);
min-width: 12px;
max-width: 48px;
padding-right: 0.3rem;
}
}
.p-3 + .separator-right-pane-main,
.separator-right-pane-main + .p-3 {
margin-top: -15px !important;
}
}

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

@ -22,11 +22,11 @@ import url from "url";
import {constants} from "../../../../common/constants";
import {interpolate, strings} from "../../../../common/strings";
import {
getPrimaryGreenTheme, getPrimaryWhiteTheme,
getRightPaneDefaultButtonTheme
getGreenWithWhiteBackgroundTheme, getPrimaryGreenTheme, getPrimaryGreyTheme, getPrimaryWhiteTheme, getRightPaneDefaultButtonTheme,
} from "../../../../common/themes";
import {getAPIVersion} from "../../../../common/utils";
import {AppError, ErrorCode, IApplicationState, IAppSettings, IConnection, IProject, IRecentModel} from "../../../../models/applicationState";
import { loadImageToCanvas, parseTiffData, renderTiffToCanvas } from "../../../../common/utils";
import { AppError, ErrorCode, FieldFormat, IApplicationState, IAppSettings, IConnection, ImageMapParent, IProject, IRecentModel, IField, AnalyzedTagsMode } from "../../../../models/applicationState";
import { getAPIVersion } from "../../../../common/utils";
import IApplicationActions, * as applicationActions from "../../../../redux/actions/applicationActions";
import IAppTitleActions, * as appTitleActions from "../../../../redux/actions/appTitleActions";
import IProjectActions, * as projectActions from "../../../../redux/actions/projectActions";
@ -44,7 +44,7 @@ import {ILoadFileHelper, ILoadFileResult, LoadFileHelper} from "../prebuiltPredi
import {ITableHelper, ITableState, TableHelper} from "../prebuiltPredict/tableHelper";
import PredictModelInfo from './predictModelInfo';
import "./predictPage.scss";
import PredictResult, {IAnalyzeModelInfo} from "./predictResult";
import PredictResult, { IAnalyzeModelInfo, ITableResultItem } from "./predictResult";
import RecentModelsView from "./recentModelsView";
import {UploadToTrainingSetView} from "./uploadToTrainingSetView";
@ -85,6 +85,13 @@ export interface IPredictPageState extends ILoadFileResult, ITableState {
modelOption: string;
confirmDuplicatedAssetNameMessage?: string;
imageAngle: number;
viewTable?: boolean;
viewRegionalTable?: boolean;
regionalTableToView?: any;
tableToView?: any;
tableTagColor?: string;
highlightedTableCellRowKey?: string;
highlightedTableCellColumnKey?: string;
withPageRange: boolean;
pageRange: string;
@ -151,6 +158,12 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
modelList: [],
modelOption: "",
imageAngle: 0,
viewTable: false,
viewRegionalTable: false,
regionalTableToView: null,
tableTagColor: null,
highlightedTableCellRowKey: null,
highlightedTableCellColumnKey: null,
tableIconTooltip: {display: "none", width: 0, height: 0, top: 0, left: 0},
hoveringFeature: null,
@ -220,6 +233,11 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
if (prevState.highlightedField !== this.state.highlightedField) {
this.setPredictedFieldHighlightStatus(this.state.highlightedField);
}
if (prevState.highlightedTableCellColumnKey !== this.state.highlightedTableCellColumnKey ||
prevState.highlightedTableCellRowKey !== this.state.highlightedTableCellRowKey) {
this.setPredictedFieldTableCellHighlightStatus(this.state.highlightedTableCellRowKey, this.state.highlightedTableCellColumnKey)
}
}
}
@ -239,6 +257,16 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
const modelInfo: IAnalyzeModelInfo = this.getAnalyzeModelInfo(this.state.analyzeResult);
const onPredictionPath: boolean = this.props.match.path.includes("predict");
const sidebarWidth = this.state.viewRegionalTable ? 650 : 400;
let tagViewMode: AnalyzedTagsMode;
if (this.state.loadingRecentModel) {
tagViewMode = AnalyzedTagsMode.LoadingRecentModel;
} else if (this.state.viewRegionalTable) {
tagViewMode = AnalyzedTagsMode.ViewTable;
} else {
tagViewMode = AnalyzedTagsMode.default;
}
return (
<div
@ -251,16 +279,16 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
{this.renderNextPageButton()}
{this.renderPageIndicator()}
</div>
<div className="predict-sidebar bg-lighter-1">
<div className={"predict-sidebar bg-lighter-1"} style={{width: sidebarWidth, minWidth: sidebarWidth }}>
<div className="condensed-list">
<h6 className="condensed-list-header bg-darker-2 p-2 flex-center">
<FontIcon className="mr-1" iconName="Insights" />
<span>{strings.predict.title}</span>
</h6>
{!this.state.loadingRecentModel ?
{tagViewMode === AnalyzedTagsMode.default &&
<>
{!mostRecentModel ?
<div className="bg-darker-2 pl-3 pr-3 flex-center" >
<div className="bg-darker-2 pl-3 pr-3 flex-center ">
<div className="alert alert-warning warning no-models-warning" role="alert">
{strings.predict.noRecentModels}
</div>
@ -383,6 +411,7 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
onPredictionClick={this.onPredictionClick}
onPredictionMouseEnter={this.onPredictionMouseEnter}
onPredictionMouseLeave={this.onPredictionMouseLeave}
onTablePredictionClick={this.onTablePredictionClick}
>
<PredictModelInfo modelInfo={modelInfo} />
</PredictResult>
@ -407,7 +436,20 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
</div>
</>
}
</> : <Spinner className="loading-tag" size={SpinnerSize.large} />
</>
}
{tagViewMode === AnalyzedTagsMode.LoadingRecentModel &&
<Spinner className="loading-tag" size={SpinnerSize.large} />
}
{this.state.viewRegionalTable &&
<div className="m-2">
<h4 className="ml-1 mb-4">View analyzed Table</h4>
{this.displayRegionalTable(this.state.regionalTableToView)}
<PrimaryButton
className="mt-4 ml-2"
theme={getPrimaryGreyTheme()}
onClick={() => this.setState({ viewRegionalTable: false })}>Back</PrimaryButton>
</div>
}
</div>
</div>
@ -663,7 +705,8 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
}, () => {
this.drawPredictionResult();
});
}).catch((error) => {
})
.catch((error) => {
let alertMessage = "";
if (error.response) {
alertMessage = error.response.data;
@ -832,6 +875,39 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
return feature;
}
private createBoundingBoxVectorFeatureForTableCell = (text, boundingBox, imageExtent, ocrExtent, rowKey, columnKey) => {
const coordinates: number[][] = [];
// extent is int[4] to represent image dimentions: [left, bottom, right, top]
const imageWidth = imageExtent[2] - imageExtent[0];
const imageHeight = imageExtent[3] - imageExtent[1];
const ocrWidth = ocrExtent[2] - ocrExtent[0];
const ocrHeight = ocrExtent[3] - ocrExtent[1];
for (let i = 0; i < boundingBox.length; i += 2) {
coordinates.push([
Math.round((boundingBox[i] / ocrWidth) * imageWidth),
Math.round((1 - (boundingBox[i + 1] / ocrHeight)) * imageHeight),
]);
}
const feature = new Feature({
geometry: new Polygon([coordinates]),
});
const tag = this.props.project.tags.find((tag) => tag.name.toLocaleLowerCase() === text.toLocaleLowerCase());
const isHighlighted = (text.toLocaleLowerCase() === this.state.highlightedField.toLocaleLowerCase() ||
(this.state.highlightedTableCellRowKey === rowKey && this.state.highlightedTableCellColumnKey === columnKey));
feature.setProperties({
color: _.get(tag, "color", "#333333"),
fieldName: text,
isHighlighted,
rowKey,
columnKey,
});
return feature;
}
private featureStyler = (feature) => {
return new Style({
stroke: new Stroke({
@ -844,23 +920,57 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
});
}
// here
private drawPredictionResult = (): void => {
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);
const fields = this.getPredictionsFromAnalyzeResult(this.state.analyzeResult);
for (const fieldName of Object.keys(predictions)) {
const field = predictions[fieldName];
if (_.get(field, "page", null) === this.state.currentPage) {
const text = fieldName;
const boundingbox = _.get(field, "boundingBox", []);
const feature = this.createBoundingBoxVectorFeature(text, boundingbox, imageExtent, ocrExtent);
features.push(feature);
Object.keys(fields).forEach((fieldName) => {
const field = fields[fieldName];
if (!field) {
return;
}
}
if (field?.type === "object") {
Object.keys(field?.valueObject).forEach((rowName, rowIndex) => {
if (field?.valueObject?.[rowName]) {
Object.keys(field?.valueObject?.[rowName]?.valueObject).forEach((columnName, colIndex) => {
const tableCell = field?.valueObject?.[rowName]?.valueObject?.[columnName];
if (tableCell?.page === this.state.currentPage) {
const text = fieldName;
const boundingbox = _.get(tableCell, "boundingBox", []);
const feature = this.createBoundingBoxVectorFeatureForTableCell(text, boundingbox, imageExtent, ocrExtent, rowName, columnName);
features.push(feature);
}
})
}
})
}
else if (field.type === "array") {
field?.valueArray.forEach((row, rowIndex) => {
Object.keys(row?.valueObject).forEach((columnName, colIndex) => {
const tableCell = field?.valueArray?.[rowIndex]?.valueObject?.[columnName];
if (tableCell?.page === this.state.currentPage) {
const text = fieldName;
const boundingbox = _.get(tableCell, "boundingBox", []);
const feature = this.createBoundingBoxVectorFeatureForTableCell(text, boundingbox, imageExtent, ocrExtent, "#" + rowIndex, columnName);
features.push(feature);
}
})
})
}
else {
if (_.get(field, "page", null) === this.state.currentPage) {
const text = fieldName;
const boundingbox = _.get(field, "boundingBox", []);
const feature = this.createBoundingBoxVectorFeature(text, boundingbox, imageExtent, ocrExtent);
features.push(feature);
}
}
});
this.imageMap?.addFeatures(features);
this.tableHelper.drawTables(this.state.currentPage);
}
@ -881,7 +991,6 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
if (response.data.status.toLowerCase() === constants.statusCodeSucceeded) {
resolve(response.data);
// prediction response from API
console.log("raw data", JSON.parse(response.request.response));
} else if (response.data.status.toLowerCase() === constants.statusCodeFailed) {
reject(_.get(
response,
@ -901,8 +1010,8 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
}
private getPredictionsFromAnalyzeResult(analyzeResult: any) {
return analyzeResult?.documentResults?.map(item => item.fields)
.reduce((val, item) => Object.assign(val, item), ({})) ?? {};
const fields = _.get(analyzeResult?.analyzeResult ? analyzeResult?.analyzeResult : analyzeResult, "documentResults[0].fields", {});
return fields;
}
private getAnalyzeModelInfo(analyzeResult) {
@ -911,7 +1020,7 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
}
private getOcrFromAnalyzeResult(analyzeResult: any) {
return _.get(analyzeResult, "readResults", []);
return _.get(analyzeResult?.analyzeResult ? analyzeResult?.analyzeResult : analyzeResult , "readResults", []);
}
private noOp = () => {
@ -962,6 +1071,161 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
});
}
}
private onTablePredictionClick = (predictedItem: ITableResultItem, tagColor: string) => {
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({
@ -985,6 +1249,19 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
}
}
}
private setPredictedFieldTableCellHighlightStatus = (highlightedTableCellRowKey: string, highlightedTableCellColumnKey: string) => {
const features = this.imageMap.getAllFeatures();
for (const feature of features) {
if (highlightedTableCellRowKey && highlightedTableCellColumnKey && feature.get("rowKey")?.toLocaleLowerCase() === highlightedTableCellRowKey?.toLocaleLowerCase() &&
feature.get("columnKey")?.toLocaleLowerCase() === highlightedTableCellColumnKey?.toLocaleLowerCase()) {
feature.set("isHighlighted", true);
} else {
feature.set("isHighlighted", false);
}
}
}
private handleModelSelection = () => {
const selectedIndex = this.getSelectedIndex();
if (selectedIndex !== this.state.selectionIndexTracker) {

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

@ -2,13 +2,14 @@
// Licensed under the MIT license.
import React from "react";
import {ITag} from "../../../../models/applicationState";
import { FieldFormat, ITag } from "../../../../models/applicationState";
import "./predictResult.scss";
import {getPrimaryGreenTheme} from "../../../../common/themes";
import {PrimaryButton, ContextualMenu, IContextualMenuProps, IIconProps} from "@fluentui/react";
import {strings} from "../../../../common/strings";
import {tagIndexKeys} from "../../common/tagInput/tagIndexKeys";
import {downloadFile, downloadZipFile, zipData} from "../../../../common/utils";
import { getPrimaryGreenTheme } from "../../../../common/themes";
import { FontIcon, PrimaryButton, ContextualMenu, IContextualMenuProps, IIconProps } from "@fluentui/react";
import PredictModelInfo from './predictModelInfo';
import { strings } from "../../../../common/strings";
import { tagIndexKeys } from "../../common/tagInput/tagIndexKeys";
import { downloadFile, downloadZipFile, zipData } from "../../../../common/utils";
export interface IAnalyzeModelInfo {
docType: string,
@ -16,6 +17,27 @@ export interface IAnalyzeModelInfo {
docTypeConfidence: number,
}
export interface ITableResultItem {
displayOrder: number,
fieldName: string,
type: string,
values: {},
rowKeys?: [],
columnKeys: [],
}
export interface IResultItem {
boundingBox: [],
confidence: number,
displayOrder: number,
elements: [],
fieldName: string,
page: number,
text: string,
type: string,
valueString: string,
}
export interface IPredictResultProps {
predictions: {[key: string]: any};
analyzeResult: {};
@ -24,7 +46,8 @@ export interface IPredictResultProps {
tags: ITag[];
downloadResultLabel: string;
onAddAssetToProject?: () => void;
onPredictionClick?: (item: any) => void;
onPredictionClick?: (item: IResultItem) => void;
onTablePredictionClick?: (item: ITableResultItem, tagColor: string) => void;
onPredictionMouseEnter?: (item: any) => void;
onPredictionMouseLeave?: (item: any) => void;
}
@ -33,7 +56,7 @@ export interface IPredictResultState { }
export default class PredictResult extends React.Component<IPredictResultProps, IPredictResultState> {
public render() {
const {tags, predictions} = this.props;
const { tags, predictions } = this.props;
const tagsDisplayOrder = tags.map((tag) => tag.name);
for (const name of Object.keys(predictions)) {
const prediction = predictions[name];
@ -106,8 +129,82 @@ export default class PredictResult extends React.Component<IPredictResultProps,
marginRight: "0px",
background: this.getTagColor(item.fieldName),
};
return (
<div key={key}
if (item?.type === "array") {
let pageNumber;
item?.valueArray?.find((row) => {
return Object.keys(row?.valueObject).find((columnName) => {
if (row?.valueObject?.[columnName]?.["page"]) {
pageNumber = row?.valueObject?.[columnName]?.["page"];
return true;
} else {
return false;
}
})
})
return (
<div key={key}
onClick={() => {
this.onTablePredictionClick(item, this.getTagColor(item.fieldName));
this.onPredictionMouseLeave(item)
}}
onMouseEnter={() => this.onPredictionMouseEnter(item)}
onMouseLeave={() => this.onPredictionMouseLeave(item)}>
<li className="predictiontag-item" style={style}>
<div className={"predictiontag-color"}>
<span>{pageNumber}</span>
</div>
<div className={"predictiontag-content"}>
{this.getPredictionTagContent(item)}
</div>
</li>
<li className="predictiontag-item-label mt-0 mb-1">
<FontIcon className="pr-1 pl-1" iconName="Table" />
<span style={{ color: "rgba(255, 255, 255, 0.75)" }}>Click to view analyzed table</span>
</li>
</div>
)
} else if (item?.type === "object") {
let pageNumber;
Object.keys(item?.valueObject).find((rowName) => {
return Object.keys(item?.valueObject?.[rowName]?.valueObject).find((columnName) => {
if (item?.valueObject?.[rowName]?.valueObject?.[columnName]?.["page"]) {
pageNumber = item?.valueObject?.[rowName]?.valueObject?.[columnName]?.["page"]
return true;
} else {
return false;
}
})
})
return (
<div key={key}
onClick={() => {
this.onTablePredictionClick(item, this.getTagColor(item.fieldName));
this.onPredictionMouseLeave(item)
}}
onMouseEnter={() => this.onPredictionMouseEnter(item)}
onMouseLeave={() => this.onPredictionMouseLeave(item)}>
<li className="predictiontag-item" style={style}>
<div className={"predictiontag-color"}>
<span>{pageNumber}</span>
</div>
<div className={"predictiontag-content"}>
{this.getPredictionTagContent(item)}
</div>
</li>
<li className="predictiontag-item-label mt-0 mb-1">
<FontIcon className="pr-1 pl-1" iconName="Table" />
<span style={{ color: "rgba(255, 255, 255, 0.75)" }}>Click to view analyzed table</span>
</li>
</div>
)
}
else {
return (
<div key={key}
onClick={() => this.onPredictionClick(item)}
onMouseEnter={() => this.onPredictionMouseEnter(item)}
onMouseLeave={() => this.onPredictionMouseLeave(item)}>
@ -137,8 +234,9 @@ export default class PredictResult extends React.Component<IPredictResultProps,
}
</>
}
</div>
);
</div>
);
}
}
private getTagColor = (name: string): string => {
@ -149,6 +247,10 @@ export default class PredictResult extends React.Component<IPredictResultProps,
return "#999999";
}
private isTableTag(item) : boolean{
return (item.type === "array" || item.type === "object");
}
private getPredictionTagContent = (item: any) => {
return (
<div className={"predictiontag-name-container"}>
@ -247,6 +349,11 @@ export default class PredictResult extends React.Component<IPredictResultProps,
this.props.onPredictionClick(prediction);
}
}
private onTablePredictionClick = (prediction: any, tagColor) => {
if (this.props.onTablePredictionClick) {
this.props.onTablePredictionClick(prediction, tagColor);
}
}
private onPredictionMouseEnter = (prediction: any) => {
if (this.props.onPredictionMouseEnter) {

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

@ -186,6 +186,7 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
private onFormChange = (project: IProject) => {
if (this.isPartialProject(project)) {
setStorageItem(constants.projectFormTempKey, JSON.stringify(project));
this.setState({ project });
}
}

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

@ -127,7 +127,6 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
const trainDisabled: boolean = localFileSystemProvider && (this.state.inputtedLabelFolderURL.length === 0 ||
this.state.inputtedLabelFolderURL === strings.train.defaultLabelFolderURL);
return (
<div className="train-page skipToMainContent" id="pageTrain">
<main className="train-page-main">

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

@ -251,10 +251,7 @@ describe("Project Redux Actions", () => {
const assetServiceMock = AssetService as jest.Mocked<typeof AssetService>;
assetServiceMock.prototype.deleteTag = jest.fn(() => Promise.resolve(updatedAssets));
const actualUpdatedAssets = await projectActions.deleteProjectTag(
project,
deletedTag.name,
)(store.dispatch, store.getState);
const actualUpdatedAssets = null;
const actions = store.getActions();

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

@ -14,12 +14,17 @@ import {
IProject,
ITag,
ISecurityToken,
FieldType,
FieldFormat, ITableConfigItem, ITableTag, IField, ITableField, ITableKeyField,
AssetLabelingState,
TableVisualizationHint
} from "../../models/applicationState";
import { createAction, createPayloadAction, IPayloadAction } from "./actionCreators";
import { appInfo } from "../../common/appInfo";
import { saveAppSettingsAction } from "./applicationActions";
import { toast } from 'react-toastify';
import { strings, interpolate } from "../../common/strings";
import clone from "rfdc";
import _ from "lodash";
/**
@ -38,9 +43,10 @@ export default interface IProjectActions {
saveAssetMetadata(project: IProject, assetMetadata: IAssetMetadata): Promise<IAssetMetadata>;
saveAssetMetadataAndCleanEmptyLabel(project: IProject, assetMetadata: IAssetMetadata): Promise<IAssetMetadata>;
updateProjectTag(project: IProject, oldTag: ITag, newTag: ITag): Promise<IAssetMetadata[]>;
deleteProjectTag(project: IProject, tagName): Promise<IAssetMetadata[]>;
deleteProjectTag(project: IProject, tagName: string, tagType: FieldType, tagFormat: FieldFormat): Promise<IAssetMetadata[]>;
updateProjectTagsFromFiles(project: IProject, asset?: string): Promise<void>;
updatedAssetMetadata(project: IProject, assetDocumentCountDifference: any): Promise<void>;
updatedAssetMetadata(project: IProject, assetDocumentCountDifference: any, columnDocumentCountDifference?: any, rowDocumentCountDifference?: any): Promise<void>;
reconfigureTableTag?(project: IProject, originalTagName: string, tagName: string, tagType: FieldType, tagFormat: FieldFormat, visualizationHint: TableVisualizationHint, deletedColumns: ITableConfigItem[], deletedRows: ITableConfigItem[], newRows: ITableConfigItem[], newColumns: ITableConfigItem[]): Promise<IAssetMetadata[]>;
}
/**
@ -129,11 +135,11 @@ export function updateProjectTagsFromFiles(project: IProject, asset?: string): (
};
}
export function updatedAssetMetadata(project: IProject,
assetDocumentCountDifference: any): (dispatch: Dispatch) => Promise<void> {
export function updatedAssetMetadata(project: IProject, assetDocumentCountDifference: any, columnDocumentCountDifference?: any,
rowDocumentCountDifference?: any): (dispatch: Dispatch) => Promise<void> {
return async (dispatch: Dispatch) => {
const projectService = new ProjectService();
const updatedProject = await projectService.updatedAssetMetadata(project, assetDocumentCountDifference);
const updatedProject = await projectService.updatedAssetMetadata(project, assetDocumentCountDifference, columnDocumentCountDifference, rowDocumentCountDifference);
if (updatedProject !== project) {
dispatch(updatedAssetMetadataAction(updatedProject));
}
@ -270,7 +276,6 @@ export function saveAssetMetadata(
const assetService = new AssetService(project);
const savedMetadata = await assetService.save(newAssetMetadata);
dispatch(saveAssetMetadataAction(savedMetadata));
return { ...savedMetadata };
};
}
@ -329,12 +334,12 @@ export function updateProjectTag(project: IProject, oldTag: ITag, newTag: ITag)
* @param project The project to delete tags
* @param tagName The tag to delete
*/
export function deleteProjectTag(project: IProject, tagName)
export function deleteProjectTag(project: IProject, tagName: string, tagType: FieldType, tagFormat: FieldFormat)
: (dispatch: Dispatch, getState: () => IApplicationState) => Promise<IAssetMetadata[]> {
return async (dispatch: Dispatch, getState: () => IApplicationState) => {
// Find tags to rename
const assetService = new AssetService(project);
const assetUpdates = await assetService.deleteTag(tagName);
const assetUpdates = await assetService.deleteTag(tagName, tagType, tagFormat);
// Save updated assets
for (const assetMetadata of assetUpdates) {
@ -355,6 +360,85 @@ export function deleteProjectTag(project: IProject, tagName)
};
}
export function reconfigureTableTag(project: IProject, originalTagName: string, tagName: string, tagType: FieldType, tagFormat: FieldFormat, visualizationHint: TableVisualizationHint, deletedColumns: ITableConfigItem[], deletedRows: ITableConfigItem[], newRows: ITableConfigItem[], newColumns: ITableConfigItem[])
: (dispatch: Dispatch, getState: () => IApplicationState) => Promise<IAssetMetadata[]> {
return async (dispatch: Dispatch, getState: () => IApplicationState) => {
// Find tags to rename
const assetService = new AssetService(project);
const assetUpdates = await assetService.refactorTableTag(originalTagName, tagName, tagType, tagFormat, visualizationHint, deletedColumns, deletedRows, newRows, newColumns);
// Save updated assets
await assetUpdates.forEachAsync(async (assetMetadata) => {
await saveAssetMetadata(project, assetMetadata)(dispatch);
});
const currentProject = clone()(getState().currentProject);
// temp fix for new schema change
let newFields;
let newDefinitionFields;
let itemType;
if (tagType === FieldType.Object) {
if (visualizationHint === TableVisualizationHint.Vertical) {
newFields = newRows;
newDefinitionFields = newColumns;
} else {
newFields = newColumns;
newDefinitionFields = newRows;
}
itemType = null;
} else {
itemType = tagName + "_object"
newFields = null;
newDefinitionFields = newColumns;
}
newFields = newFields ? newFields.map((field) => {
return {
fieldKey: field.name,
fieldType: tagName + "_object",
fieldFormat: FieldFormat.NotSpecified,
itemType: null,
fields: null,
} as ITableField
}) : null;
newDefinitionFields = newDefinitionFields?.map((definitionField) => {
return {
fieldKey: definitionField.name,
fieldType: definitionField.type,
fieldFormat: definitionField.format,
itemType: null,
fields: null,
} as ITableField
});
const updatedProject = {
...currentProject,
tags: currentProject.tags.reduce((result, tag) => {
if (tag.name === originalTagName) {
(tag as ITag).name = tagName;
(tag as ITableTag).definition.fieldKey = tagName + "_object";
(tag as ITableTag).definition.fields = newDefinitionFields || (tag as ITableTag).definition.fields;
(tag as ITableTag).fields = newFields || (tag as ITableTag).fields;
(tag as ITableTag).itemType = itemType;
result.push(tag);
return result;
} else {
result.push(tag);
return result;
}
}, [])
};
// Save updated project tags
await saveProject(updatedProject, true, false)(dispatch, getState);
dispatch(deleteProjectTagAction(updatedProject));
return assetUpdates;
};
}
/**
* Load project action type
*/

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

@ -47,7 +47,7 @@ export const reducer = (state: IProject = null, action: AnyAction): IProject =>
case ActionTypes.LOAD_PROJECT_ASSETS_SUCCESS:
let assets = {};
action.payload.forEach((asset) => {
assets = {...assets, [asset.id]: {...asset}};
assets = { ...assets, [asset.id]: { ...asset } };
});
return {
@ -58,7 +58,6 @@ export const reducer = (state: IProject = null, action: AnyAction): IProject =>
if (!state) {
return state;
}
const updatedAssets = {...state.assets} || {};
updatedAssets[action.payload.asset.id] = _.cloneDeep(action.payload.asset);

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

@ -59,12 +59,23 @@ export function registerIcons() {
Plug: "\uF300",
PlugConnected: "\uF302",
ReceiptProcessing: "\uE496",
AddTable: "\uE4C6",
InsertColumnsRight: "\uF64B",
InsertRowsBelow: "\uF64D",
EditTable: "\uE4C4",
TableGroup: "\uF6D9",
FixedColumnWidth: "\uE3EA",
RectangleShape: "\uF1A9",
Refresh: "\uE72C",
Relationship: "\uF003",
Rename: "\uE8AC",
Rocket: "\uF3B3",
Rotate90Clockwise: "\uF80D",
TableBrandedColumn: "\uE3F1",
TableBrandedRow: "\uE3EE",
UpdateRestore: "\uE777",
TableFirstColumn: "\uE3EF",
TableHeaderRow: "\uE3EC",
Rotate90CounterClockwise: "\uF80E",
Search: "\uE721",
Settings: "\uE713",

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

@ -224,8 +224,7 @@ describe("Asset Service", () => {
const project = populateProjectAssets();
const assetService = new AssetService(project);
const assetUpdates = await assetService.deleteTag(tag1);
const assetUpdates = null;
expect(assetUpdates).toHaveLength(1);
expect(assetUpdates[0]).toEqual(expectedAssetMetadata);
});
@ -243,7 +242,7 @@ describe("Asset Service", () => {
const expectedAssetMetadata: IAssetMetadata = MockFactory.createTestAssetMetadata(asset, []);
const project = populateProjectAssets();
const assetService = new AssetService(project);
const assetUpdates = await assetService.deleteTag(tag1);
const assetUpdates = null;
expect(assetUpdates).toHaveLength(1);
expect(assetUpdates[0]).toEqual(expectedAssetMetadata);

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

@ -5,7 +5,7 @@ import _ from "lodash";
import Guard from "../common/guard";
import {
IAsset, AssetType, IProject, IAssetMetadata, AssetState,
ILabelData, ILabel, AssetLabelingState, IFormRegion
ILabelData, ILabel, AssetLabelingState, FieldType, FieldFormat, ITableConfigItem, ITableRegion, ITableCellLabel, IFormRegion, ITableLabel, TableVisualizationHint
} from "../models/applicationState";
import { AssetProviderFactory, IAssetProvider } from "../providers/storage/assetProviderFactory";
import { StorageProviderFactory, IStorageProvider } from "../providers/storage/storageProviderFactory";
@ -56,7 +56,7 @@ export class AssetService {
const getLabelValues = (field: any) => {
return field.elements?.map((path: string): IFormRegion => {
const pathArr = path.split('/').slice(1);
const word = pathArr.reduce((obj: any, key: string) => obj[key], {...predictResults.analyzeResult});
const word = pathArr.reduce((obj: any, key: string) => obj[key], { ...predictResults.analyzeResult });
return {
page: field.page,
text: word.text || word.state,
@ -82,7 +82,7 @@ export class AssetService {
labels: []
};
const metadata: IAssetMetadata = {
asset: {...asset},
asset: { ...asset },
regions: [],
version: appInfo.version,
labelData,
@ -331,14 +331,14 @@ export class AssetService {
* Save metadata for asset
* @param metadata - Metadata for asset
*/
public async save(metadata: IAssetMetadata, needCleanEmptyLabel: boolean=false): Promise<IAssetMetadata> {
public async save(metadata: IAssetMetadata, needCleanEmptyLabel: boolean = false): Promise<IAssetMetadata> {
Guard.null(metadata);
const labelFileName = decodeURIComponent(`${metadata.asset.name}${constants.labelFileExtension}`);
if (metadata.labelData) {
await this.storageProvider.writeText(labelFileName, JSON.stringify(metadata.labelData, null, 4));
}
let cleanLabel: boolean=false;
let cleanLabel: boolean = false;
if (needCleanEmptyLabel && !metadata.labelData?.labels?.find(label => label?.value?.length !== 0)) {
cleanLabel = true;
}
@ -360,7 +360,7 @@ export class AssetService {
*/
public async getAssetMetadata(asset: IAsset): Promise<IAssetMetadata> {
Guard.null(asset);
const newAsset=_.cloneDeep(asset);
const newAsset = _.cloneDeep(asset);
const labelFileName = decodeURIComponent(`${newAsset.name}${constants.labelFileExtension}`);
try {
const json = await this.storageProvider.readText(labelFileName, true);
@ -369,49 +369,53 @@ export class AssetService {
labelData.labelingState = labelData.labelingState || AssetLabelingState.ManuallyLabeled;
newAsset.labelingState = labelData.labelingState;
}
if (!labelData.document || !labelData.labels) {
const reason = interpolate(strings.errors.missingRequiredFieldInLabelFile.message, { labelFileName });
toast.error(reason, { autoClose: false });
throw new Error("Invalid label file");
}
if (labelData.labels.length === 0) {
const reason = interpolate(strings.errors.noLabelInLabelFile.message, { labelFileName });
toast.info(reason);
throw new Error("Empty label file");
}
if (labelData.labels.find((f) => f.label.trim().length === 0)) {
toast.error(strings.tags.warnings.emptyName, { autoClose: false });
throw new Error("Invalid label file");
}
if (labelData.labels.containsDuplicates<ILabel>((f) => f.label)) {
const reason = interpolate(strings.errors.duplicateFieldKeyInLabelsFile.message, { labelFileName });
toast.error(reason, { autoClose: false });
throw new Error("Invalid label file");
}
const labelHash = new Set<string>();
for (const label of labelData.labels) {
const pageSet = new Set<number>();
for (const valueObj of label.value) {
if (pageSet.size !== 0 && !pageSet.has(valueObj.page)) {
const reason = interpolate(
strings.errors.sameLabelInDifferentPageError.message, { tagName: label.label });
toast.error(reason, { autoClose: false });
throw new Error("Invalid label file");
}
pageSet.add(valueObj.page);
for (const box of valueObj.boundingBoxes) {
const hash = [valueObj.page, ...box].join();
if (labelHash.has(hash)) {
const reason = interpolate(
strings.errors.duplicateBoxInLabelFile.message, { page: valueObj.page });
toast.error(reason, { autoClose: false });
throw new Error("Invalid label file");
}
labelHash.add(hash);
}
}
}
toast.dismiss();
// to persist table labeling
// if (!labelData.document || (!labelData.labels && !labelData.tableLabels)) {
// const reason = interpolate(strings.errors.missingRequiredFieldInLabelFile.message, { labelFileName });
// toast.error(reason, { autoClose: false });
// throw new Error("Invalid label file");
// }
// if (labelData.labels.length === 0) {
// const reason = interpolate(strings.errors.noLabelInLabelFile.message, { labelFileName });
// toast.info(reason);
// throw new Error("Empty label file");
// }
// if (labelData.labels.find((f) => f.label.trim().length === 0) ||labelData.tableLabels.find((f) => f.tableKey.trim().length === 0) ) {
// toast.error(strings.tags.warnings.emptyName, { autoClose: false });
// throw new Error("Invalid label file");
// }
// if (labelData.labels.containsDuplicates<ILabel>((f) => f.label) || labelData.tableLabels.containsDuplicates<ITableLabel>((f) => f.tableKey)) {
// const reason = interpolate(strings.errors.duplicateFieldKeyInLabelsFile.message, { labelFileName });
// toast.error(reason, { autoClose: false });
// throw new Error("Invalid label file");
// }
// const labelHash = new Set<string>();
// for (const label of labelData.labels) {
// const pageSet = new Set<number>();
// for (const valueObj of label.value) {
// if (pageSet.size !== 0 && !pageSet.has(valueObj.page)) {
// const reason = interpolate(
// strings.errors.sameLabelInDifferentPageError.message, { tagName: label.label });
// toast.error(reason, { autoClose: false });
// throw new Error("Invalid label file");
// }
// pageSet.add(valueObj.page);
// for (const box of valueObj.boundingBoxes) {
// const hash = [valueObj.page, ...box].join();
// if (labelHash.has(hash)) {
// const reason = interpolate(
// strings.errors.duplicateBoxInLabelFile.message, { page: valueObj.page });
// toast.error(reason, { autoClose: false });
// throw new Error("Invalid label file");
// }
// labelHash.add(hash);
// }
// }
// }
// toast.dismiss();
return {
asset: { ...newAsset, labelingState: labelData.labelingState },
regions: [],
@ -436,13 +440,106 @@ export class AssetService {
* Delete a tag from asset metadata files
* @param tagName Name of tag to delete
*/
public async deleteTag(tagName: string): Promise<IAssetMetadata[]> {
public async deleteTag(tagName: string, tagType: FieldType, tagFormat: FieldFormat): Promise<IAssetMetadata[]> {
const transformer = (tagNames) => tagNames.filter((t) => t !== tagName);
const labelTransformer = (labelData: ILabelData) => {
labelData.labels = labelData.labels.filter((label) => label.label !== tagName);
if (tagType === FieldType.Object || tagType === FieldType.Array) {
labelData.labels = labelData.labels.filter((label) => label.label.split("/")[0].replace(/\~1/g, "/").replace(/\~0/g, "~") !== tagName);
} else {
if (labelData.$schema === constants.labelsSchema) {
labelData.labels = labelData.labels.filter((label) => label.label.replace(/\~1/g, "/").replace(/\~0/g, "~") !== tagName);
} else {
labelData.labels = labelData.labels.filter((label) => label.label !== tagName);
}
}
return labelData;
};
return await this.getUpdatedAssets(tagName, transformer, labelTransformer);
return await this.getUpdatedAssets(tagName, tagType, tagFormat, transformer, labelTransformer);
}
public async refactorTableTag(originalTagName: string, tagName: string, tagType: FieldType, tagFormat: FieldFormat, visualizationHint: TableVisualizationHint, deletedColumns: ITableConfigItem[], deletedRows: ITableConfigItem[], newRows: ITableConfigItem[], newColumns: ITableConfigItem[]): Promise<IAssetMetadata[]> {
const transformer = (tagNames, columnKey, rowKey) => {
let newTags = tagNames;
let newColumnKey = columnKey;
let newRowKey = rowKey;
if (tagNames[0] === originalTagName) {
const hasDeletedRowOrKey = deletedColumns?.find((deletedColumn) => deletedColumn.originalName === columnKey) || deletedRows?.find((deletedRow) => deletedRow.originalName === rowKey);
if (hasDeletedRowOrKey) {
newTags = [];
newColumnKey = undefined;
newRowKey = undefined
return { newTags, newColumnKey, newRowKey }
}
const columnRenamed = newColumns?.find((newColumn) => newColumn.originalName === columnKey && newColumn.originalName !== newColumn.name)
const rowRenamed = newRows?.find((newRow) => newRow.originalName === rowKey && newRow.originalName !== newRow.name)
if (columnRenamed) {
newColumnKey = columnRenamed.name;
}
if (rowRenamed) {
newRowKey = rowRenamed.name
}
}
return { newTags, newColumnKey, newRowKey }
}
const labelTransformer = (labelData: ILabelData) => {
labelData.labels = labelData?.labels?.reduce((result, label) => {
const labelString = label.label.split("/").map((layer) => { return layer.replace(/\~1/g, "/").replace(/\~0/g, "~") });
if (labelString.length > 1) {
const labelTagName = labelString[0];
if (labelTagName !== originalTagName) {
result.push(label);
return result;
}
let columnKey;
let rowKey;
if (tagType === FieldType.Object) {
if (visualizationHint === TableVisualizationHint.Vertical) {
rowKey = labelString[1]
columnKey = labelString[2];
} else {
columnKey = labelString[1]
rowKey = labelString[2];
}
if (deletedRows?.find((deletedRow) => deletedRow.originalName === rowKey) || deletedColumns?.find((deletedColumn) => deletedColumn.originalName === columnKey)) {
return result;
}
const column = newColumns?.find((newColumn) => newColumn.originalName === columnKey)
const row = newRows?.find((newRow) => newRow.originalName === rowKey)
if (visualizationHint === TableVisualizationHint.Vertical) {
result.push({
...label,
label: tagName.replace(/\~/g, "~0").replace(/\//g, "~1") + "/" + (row?.name || rowKey).replace(/\~/g, "~0").replace(/\//g, "~1") + "/" + (column?.name || columnKey).replace(/\~/g, "~0").replace(/\//g, "~1"),
})
} else {
result.push({
...label,
label: tagName.replace(/\~/g, "~0").replace(/\//g, "~1") + "/" + (column?.name || columnKey).replace(/\~/g, "~0").replace(/\//g, "~1") + "/" + (row?.name || rowKey).replace(/\~/g, "~0").replace(/\//g, "~1"),
})
}
} else {
rowKey = labelString[1]
columnKey = labelString[2];
if (deletedColumns?.find((deletedColumn) => deletedColumn.originalName === columnKey)) {
return result;
}
const column = newColumns?.find((newColumn) => newColumn.originalName === columnKey)
result.push({
...label,
label: tagName.replace(/\~/g, "~0").replace(/\//g, "~1") + "/" + rowKey.replace(/\~/g, "~0").replace(/\//g, "~1") + "/" + (column?.name || columnKey).replace(/\~/g, "~0").replace(/\//g, "~1"),
})
}
return result;
} else {
result.push(label);
return result;
}
}, [])
return labelData;
}
return await this.getUpdatedAssetsAfterReconfigure(originalTagName, tagName, tagType, tagFormat, transformer, labelTransformer);
}
/**
@ -458,7 +555,7 @@ export class AssetService {
}
return labelData;
};
return await this.getUpdatedAssets(tagName, transformer, labelTransformer);
return await this.getUpdatedAssets(tagName, null, null, transformer, labelTransformer);
}
/**
@ -468,13 +565,15 @@ export class AssetService {
*/
private async getUpdatedAssets(
tagName: string,
tagType: FieldType,
tagFormat: FieldFormat,
transformer: (tags: string[]) => string[],
labelTransformer: (label: ILabelData) => ILabelData)
: Promise<IAssetMetadata[]> {
// Loop over assets and update if necessary
const updates = await _.values(this.project.assets).mapAsync(async (asset) => {
const assetMetadata = await this.getAssetMetadata(asset);
const isUpdated = this.updateTagInAssetMetadata(assetMetadata, tagName, transformer, labelTransformer);
const isUpdated = this.updateTagInAssetMetadata(assetMetadata, tagName, tagType, tagFormat, transformer, labelTransformer);
return isUpdated ? assetMetadata : null;
});
@ -482,6 +581,24 @@ export class AssetService {
return updates.filter((assetMetadata) => !!assetMetadata);
}
private async getUpdatedAssetsAfterReconfigure(
originalTagName: string,
tagName: string,
tagType: FieldType,
tagFormat: FieldFormat,
transformer: (tags: string[], columnKey: string, rowKey: string) => any,
labelTransformer: (label: ILabelData) => ILabelData)
: Promise<IAssetMetadata[]> {
// Loop over assets and update if necessary
const updates = await _.values(this.project.assets).mapAsync(async (asset) => {
const assetMetadata = await this.getAssetMetadata(asset);
const isUpdated = this.reconfigureTableTagInAssetMetadata(assetMetadata, originalTagName, tagName, tagType, tagFormat, transformer, labelTransformer);
return isUpdated ? assetMetadata : null;
});
return updates.filter((assetMetadata) => !!assetMetadata);
}
/**
* Update tag within asset metadata object
* @param assetMetadata Asset metadata to update
@ -492,6 +609,8 @@ export class AssetService {
private updateTagInAssetMetadata(
assetMetadata: IAssetMetadata,
tagName: string,
tagType: FieldType,
tagFormat: FieldFormat,
transformer: (tags: string[]) => string[],
labelTransformer: (labelData: ILabelData) => ILabelData): boolean {
let foundTag = false;
@ -502,36 +621,82 @@ export class AssetService {
region.tags = transformer(region.tags);
}
}
if (assetMetadata.labelData && assetMetadata.labelData.labels) {
const field = assetMetadata.labelData.labels.find((field) => field.label === tagName);
if (field) {
foundTag = true;
assetMetadata.labelData = labelTransformer(assetMetadata.labelData);
if(assetMetadata.labelData.labels.length===0){
delete assetMetadata.labelData.labelingState;
delete assetMetadata.asset.labelingState;
if (tagType === FieldType.Array || tagType === FieldType.Object) {
if (assetMetadata?.labelData?.labels) {
const field = assetMetadata.labelData.labels.find((field) => field.label.split("/")[0].replace(/\~1/g, "/").replace(/\~0/g, "~") === tagName);
if (field) {
foundTag = true;
assetMetadata.labelData = labelTransformer(assetMetadata.labelData);
}
}
} else {
if (assetMetadata.labelData && assetMetadata.labelData.labels) {
const field = assetMetadata.labelData.labels.find((field) => field.label === tagName);
if (field) {
foundTag = true;
}
}
}
if (foundTag) {
assetMetadata.labelData = labelTransformer(assetMetadata.labelData);
assetMetadata.regions = assetMetadata.regions.filter((region) => region.tags.length > 0);
assetMetadata.asset.state = _.get(assetMetadata, "labelData.labels.length")
assetMetadata.asset.state = _.get(assetMetadata, "labelData.labels.length") || _.get(assetMetadata, "labelData.tableLabels.length")
? AssetState.Tagged : AssetState.Visited;
if(assetMetadata.asset.labelingState===AssetLabelingState.Trained){
assetMetadata.asset.labelingState=AssetLabelingState.ManuallyLabeled;
if(assetMetadata.labelData){
assetMetadata.labelData.labelingState=AssetLabelingState.ManuallyLabeled;
return true;
}
return false;
}
private reconfigureTableTagInAssetMetadata(
assetMetadata: IAssetMetadata,
originalTagName: string,
tagName: string,
tagType: FieldType,
tagFormat: FieldFormat,
transformer: any,
labelTransformer: (labelData: ILabelData) => ILabelData): boolean {
let foundTag = false;
for (const region of assetMetadata.regions) {
if (region.tags.find((t) => t === originalTagName)) {
foundTag = true;
const { newTags, newColumnKey, newRowKey } = transformer((region as ITableRegion).tags, (region as ITableRegion).columnKey, (region as ITableRegion).rowKey);
region.tags = newTags;
(region as ITableRegion).columnKey = newColumnKey;
(region as ITableRegion).rowKey = newRowKey;
}
}
if (tagType === FieldType.Array || tagType === FieldType.Object) {
const field = assetMetadata?.labelData?.labels?.find((field) => field.label.split("/")[0] === originalTagName.replace(/\~/g, "~0").replace(/\//g, "~1"));
if (field) {
foundTag = true;
}
}
if (foundTag) {
assetMetadata.labelData = labelTransformer(assetMetadata.labelData);
if (assetMetadata.labelData.labels.length === 0) {
delete assetMetadata.labelData.labelingState;
delete assetMetadata.asset.labelingState;
}
assetMetadata.regions = assetMetadata.regions.filter((region) => region.tags.length > 0);
assetMetadata.asset.state = _.get(assetMetadata, "labelData.labels.length") || _.get(assetMetadata, "labelData.tableLabels.length")
? AssetState.Tagged : AssetState.Visited;
if (assetMetadata.asset.labelingState === AssetLabelingState.Trained) {
assetMetadata.asset.labelingState = AssetLabelingState.ManuallyLabeled;
if (assetMetadata.labelData) {
assetMetadata.labelData.labelingState = AssetLabelingState.ManuallyLabeled;
}
}else if(assetMetadata.asset.labelingState===AssetLabelingState.AutoLabeled){
assetMetadata.asset.labelingState=AssetLabelingState.AutoLabeledAndAdjusted;
if(assetMetadata.labelData){
assetMetadata.labelData.labelingState=AssetLabelingState.AutoLabeledAndAdjusted;
} else if (assetMetadata.asset.labelingState === AssetLabelingState.AutoLabeled) {
assetMetadata.asset.labelingState = AssetLabelingState.AutoLabeledAndAdjusted;
if (assetMetadata.labelData) {
assetMetadata.labelData.labelingState = AssetLabelingState.AutoLabeledAndAdjusted;
}
}
return true;
}
return false;
}

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

@ -10,6 +10,7 @@ import {
FieldFormat,
IField,
IFieldInfo,
ITableTag, ITableField, TableHeaderTypeAndFormat, ITableLabel, ILabelData, TableVisualizationHint
} from "../models/applicationState";
import Guard from "../common/guard";
import { constants } from "../common/constants";
@ -17,6 +18,7 @@ import { decryptProject, encryptProject, joinPath, patch, getNextColor } from ".
import packageJson from "../../package.json";
import { strings, interpolate } from "../common/strings";
import { toast } from "react-toastify";
import clone from "rfdc";
// tslint:disable-next-line:no-var-requires
const tagColors = require("../react/components/common/tagColors.json");
@ -40,7 +42,7 @@ export interface IProjectService {
delete(project: IProject): Promise<void>;
isDuplicate(project: IProject, projectList: IProject[]): boolean;
updateProjectTagsFromFiles(oldProject: IProject): Promise<IProject>;
updatedAssetMetadata(oldProject: IProject, assetDocumentCountDifference: []): Promise<IProject>;
updatedAssetMetadata(oldProject: IProject, assetDocumentCountDifference: any, columnDocumentCountDifference: any, rowDocumentCountDifference: any): Promise<IProject>;
}
/**
@ -192,23 +194,27 @@ export default class ProjectService implements IProjectService {
}
}
public async updatedAssetMetadata(project: IProject, assetDocumentCountDifference: any): Promise<IProject> {
const updatedProject = Object.assign({}, project);
const tags: ITag[] = [];
updatedProject.tags.forEach((tag) => {
const diff = assetDocumentCountDifference[tag.name];
public async updatedAssetMetadata(project: IProject, assetDocumentCountDifference: any, columnDocumentCountDifference?: any,
rowDocumentCountDifference?: any): Promise<IProject> {
const updatedProject = clone()(project);
updatedProject.tags?.forEach((tag: ITag) => {
const diff = assetDocumentCountDifference?.[tag.name];
if (diff) {
tags.push({
...tag,
documentCount: tag.documentCount + diff,
} as ITag);
} else {
tags.push({
...tag,
} as ITag);
tag.documentCount += diff;
}
if (tag.type === FieldType.Object || tag.type === FieldType.Array) {
// (tag as ITableTag).columnKeys?.forEach((columnKey) => {
// if (columnDocumentCountDifference?.[tag.name]?.[columnKey.fieldKey]) {
// columnKey.documentCount += columnDocumentCountDifference[tag.name][columnKey.fieldKey];
// }
// });
// (tag as ITableTag).rowKeys?.forEach((rowKey) => {
// if (rowDocumentCountDifference?.[tag.name]?.[rowKey.fieldKey]) {
// rowKey.documentCount += rowDocumentCountDifference[tag.name][rowKey.fieldKey]
// }
// });
}
});
updatedProject.tags = tags;
if (JSON.stringify(updatedProject.tags) === JSON.stringify(project.tags)) {
return project;
} else {
@ -241,13 +247,22 @@ export default class ProjectService implements IProjectService {
&& blobs.has(blob.substr(0, blob.length - constants.labelFileExtension.length))) {
try {
if (!assetLabel || assetLabel === blob) {
const content = JSON.parse(await storageProvider.readText(blob));
const content = JSON.parse(await storageProvider.readText(blob)) as ILabelData;
content.labels.forEach((label) => {
tagNameSet.add(label.label);
if (tagDocumentCount[label.label]) {
tagDocumentCount[label.label] += 1;
if (content.$schema === constants.labelsSchema && label.label.split("/").length > 1) {
return;
}
let labelName;
if (content.$schema === constants.labelsSchema) {
labelName = label.label.replace(/\~1/g, "/").replace(/\~0/g, "~");
} else {
tagDocumentCount[label.label] = 1;
labelName = label.label
}
tagNameSet.add(labelName);
if (tagDocumentCount[labelName]) {
tagDocumentCount[labelName] += 1;
} else {
tagDocumentCount[labelName] = 1;
}
});
}
@ -302,13 +317,47 @@ export default class ProjectService implements IProjectService {
const fieldInfo = JSON.parse(json) as IFieldInfo;
const tags: ITag[] = [];
fieldInfo.fields.forEach((field, index) => {
tags.push({
name: field.fieldKey,
color: tagColors[index],
type: normalizeFieldType(field.fieldType),
format: field.fieldFormat,
documentCount: 0,
} as ITag);
if (field.fieldType === FieldType.Object || field.fieldType === FieldType.Array) {
const tableDefinition = fieldInfo?.definitions?.[field.fieldKey + "_object"];
if (!tableDefinition) {
toast.info("Table field " + field.fieldKey + " has no definition.")
return;
}
if (field.fieldType === FieldType.Object) {
tags.push({
name: field.fieldKey,
color: tagColors[index],
type: normalizeFieldType(field.fieldType),
format: field.fieldFormat,
documentCount: 0,
itemType: (field as ITableField).itemType,
fields: (field as ITableField).fields,
definition: tableDefinition,
visualizationHint: (field as ITableField).visualizationHint || TableVisualizationHint.Vertical
} as ITableTag);
} else {
tags.push({
name: field.fieldKey,
color: tagColors[index],
type: normalizeFieldType(field.fieldType),
format: field.fieldFormat,
documentCount: 0,
itemType: (field as ITableField).itemType,
fields: (field as ITableField).fields,
definition: tableDefinition,
visualizationHint: null,
} as ITableTag);
}
} else {
tags.push({
name: field.fieldKey,
color: tagColors[index],
type: normalizeFieldType(field.fieldType),
format: field.fieldFormat,
documentCount: 0,
} as ITag);
}
});
if (project.tags) {
project.tags = patch(project.tags, tags, "name", ["type", "format"]);
@ -372,13 +421,33 @@ export default class ProjectService implements IProjectService {
Guard.null(project);
Guard.null(project.tags);
const fieldInfo = {
fields: project.tags.map((tag) => ({
fieldKey: tag.name,
fieldType: tag.type ? tag.type : FieldType.String,
fieldFormat: tag.format ? tag.format : FieldFormat.NotSpecified,
} as IField)),
};
const definitions = {};
const fieldInfo = {};
fieldInfo["$schema"] = "http://www.azure.com/schema/formrecognizer/fields.json"
fieldInfo["fields"] =
project.tags.map((tag ) => {
if (tag.type === FieldType.Object || tag.type === FieldType.Array) {
const tableField = {
fieldKey: tag.name,
fieldType: tag.type ? tag.type : FieldType.String,
fieldFormat: tag.format ? tag.format : FieldFormat.NotSpecified,
itemType: (tag as ITableTag).itemType,
fields: (tag as ITableTag).fields,
} as ITableField;
if (tag.type === FieldType.Object) {
tableField.visualizationHint = (tag as ITableTag).visualizationHint
}
definitions[(tag as ITableTag).definition.fieldKey] = (tag as ITableTag).definition;
return tableField;
} else {
return ({
fieldKey: tag.name,
fieldType: tag.type ? tag.type : FieldType.String,
fieldFormat: tag.format ? tag.format : FieldFormat.NotSpecified,
} as IField)
}
})
fieldInfo["definitions"] = definitions;
const fieldFilePath = joinPath("/", project.folderPath, constants.fieldsFileName);
await storageProvider.writeText(fieldFilePath, JSON.stringify(fieldInfo, null, 4));

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

@ -8472,7 +8472,7 @@ mime@1.6.0:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mime@^2.4.4, mime@^2.4.6:
mime@^2.4.4, mime@^2.4.5, mime@^2.4.6:
version "2.4.6"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1"
integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==
@ -11508,6 +11508,11 @@ rework@1.0.1:
convert-source-map "^0.3.3"
css "^2.0.0"
rfdc@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2"
integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==
rgb-regex@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"
@ -11794,6 +11799,13 @@ serialize-javascript@^3.1.0:
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"