diff --git a/CHANGELOG.md b/CHANGELOG.md index 95edf2bb7..17148a068 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/package.json b/package.json index 70c05d4f1..c16cc959f 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/assets/sass/fabric-icons-inline.scss b/src/assets/sass/fabric-icons-inline.scss index 21b28afe0..75596903b 100644 --- a/src/assets/sass/fabric-icons-inline.scss +++ b/src/assets/sass/fabric-icons-inline.scss @@ -3,7 +3,7 @@ */ @font-face { font-family: 'FabricMDL2Icons'; - src: url('data:application/octet-stream;base64,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') format('truetype'); + src: url('data:application/octet-stream;base64,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') format('truetype'); } .ms-Icon { @@ -19,6 +19,7 @@ // Mixins @mixin ms-Icon--Add { content: "\E710"; } @mixin ms-Icon--AddField { content: "\E4C7"; } + @mixin ms-Icon--AddTable { content: "\E4C6"; } @mixin ms-Icon--AddTo { content: "\ECC8"; } @mixin ms-Icon--AlertSolid { content: "\F331"; } @mixin ms-Icon--AutoEnhanceOff { content: "\E78E"; } @@ -46,15 +47,19 @@ @mixin ms-Icon--Down { content: "\E74B"; } @mixin ms-Icon--Download { content: "\E896"; } @mixin ms-Icon--Edit { content: "\E70F"; } + @mixin ms-Icon--EditTable { content: "\E4C4"; } @mixin ms-Icon--FieldChanged { content: "\F2C3"; } @mixin ms-Icon--FieldNotChanged { content: "\F2C4"; } @mixin ms-Icon--Filter { content: "\E71C"; } + @mixin ms-Icon--FixedColumnWidth { content: "\E3EA"; } @mixin ms-Icon--GroupedList { content: "\EF74"; } @mixin ms-Icon--GroupList { content: "\F168"; } @mixin ms-Icon--Help { content: "\E897"; } @mixin ms-Icon--Hide3 { content: "\F6AC"; } @mixin ms-Icon--Home { content: "\E80F"; } @mixin ms-Icon--Info { content: "\E946"; } + @mixin ms-Icon--InsertColumnsRight { content: "\F64B"; } + @mixin ms-Icon--InsertRowsBelow { content: "\F64D"; } @mixin ms-Icon--Insights { content: "\E3AF"; } @mixin ms-Icon--KeyPhraseExtraction { content: "\E395"; } @mixin ms-Icon--Label { content: "\E932"; } @@ -83,11 +88,17 @@ @mixin ms-Icon--StatusCircleCheckmark { content: "\F13E"; } @mixin ms-Icon--System { content: "\E770"; } @mixin ms-Icon--Table { content: "\ED86"; } + @mixin ms-Icon--TableBrandedColumn { content: "\E3F1"; } + @mixin ms-Icon--TableBrandedRow { content: "\E3EE"; } + @mixin ms-Icon--TableFirstColumn { content: "\E3EF"; } + @mixin ms-Icon--TableGroup { content: "\F6D9"; } + @mixin ms-Icon--TableHeaderRow { content: "\E3EC"; } @mixin ms-Icon--Tag { content: "\E8EC"; } @mixin ms-Icon--TagGroup { content: "\E3F6"; } @mixin ms-Icon--TextDocument { content: "\F029"; } @mixin ms-Icon--TextField { content: "\EDC3"; } @mixin ms-Icon--Up { content: "\E74A"; } + @mixin ms-Icon--UpdateRestore { content: "\E777"; } @mixin ms-Icon--View { content: "\E890"; } @mixin ms-Icon--WarningSolid { content: "\F736"; } @mixin ms-Icon--ZoomIn { content: "\E8A3"; } @@ -97,6 +108,7 @@ // Classes .ms-Icon--Add:before { @include ms-Icon--Add } .ms-Icon--AddField:before { @include ms-Icon--AddField } + .ms-Icon--AddTable:before { @include ms-Icon--AddTable } .ms-Icon--AddTo:before { @include ms-Icon--AddTo } .ms-Icon--AlertSolid:before { @include ms-Icon--AlertSolid } .ms-Icon--AutoEnhanceOff:before { @include ms-Icon--AutoEnhanceOff } @@ -124,15 +136,19 @@ .ms-Icon--Down:before { @include ms-Icon--Down } .ms-Icon--Download:before { @include ms-Icon--Download } .ms-Icon--Edit:before { @include ms-Icon--Edit } + .ms-Icon--EditTable:before { @include ms-Icon--EditTable } .ms-Icon--FieldChanged:before { @include ms-Icon--FieldChanged } .ms-Icon--FieldNotChanged:before { @include ms-Icon--FieldNotChanged } .ms-Icon--Filter:before { @include ms-Icon--Filter } + .ms-Icon--FixedColumnWidth:before { @include ms-Icon--FixedColumnWidth } .ms-Icon--GroupedList:before { @include ms-Icon--GroupedList } .ms-Icon--GroupList:before { @include ms-Icon--GroupList } .ms-Icon--Help:before { @include ms-Icon--Help } .ms-Icon--Hide3:before { @include ms-Icon--Hide3 } .ms-Icon--Home:before { @include ms-Icon--Home } .ms-Icon--Info:before { @include ms-Icon--Info } + .ms-Icon--InsertColumnsRight:before { @include ms-Icon--InsertColumnsRight } + .ms-Icon--InsertRowsBelow:before { @include ms-Icon--InsertRowsBelow } .ms-Icon--Insights:before { @include ms-Icon--Insights } .ms-Icon--KeyPhraseExtraction:before { @include ms-Icon--KeyPhraseExtraction } .ms-Icon--Label:before { @include ms-Icon--Label } @@ -161,11 +177,17 @@ .ms-Icon--StatusCircleCheckmark:before { @include ms-Icon--StatusCircleCheckmark } .ms-Icon--System:before { @include ms-Icon--System } .ms-Icon--Table:before { @include ms-Icon--Table } + .ms-Icon--TableBrandedColumn:before { @include ms-Icon--TableBrandedColumn } + .ms-Icon--TableBrandedRow:before { @include ms-Icon--TableBrandedRow } + .ms-Icon--TableFirstColumn:before { @include ms-Icon--TableFirstColumn } + .ms-Icon--TableGroup:before { @include ms-Icon--TableGroup } + .ms-Icon--TableHeaderRow:before { @include ms-Icon--TableHeaderRow } .ms-Icon--Tag:before { @include ms-Icon--Tag } .ms-Icon--TagGroup:before { @include ms-Icon--TagGroup } .ms-Icon--TextDocument:before { @include ms-Icon--TextDocument } .ms-Icon--TextField:before { @include ms-Icon--TextField } .ms-Icon--Up:before { @include ms-Icon--Up } + .ms-Icon--UpdateRestore:before { @include ms-Icon--UpdateRestore } .ms-Icon--View:before { @include ms-Icon--View } .ms-Icon--WarningSolid:before { @include ms-Icon--WarningSolid } .ms-Icon--ZoomIn:before { @include ms-Icon--ZoomIn } diff --git a/src/common/constants.ts b/src/common/constants.ts index 68a0ee551..0377228e0 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -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`; diff --git a/src/common/localization/en-us.ts b/src/common/localization/en-us.ts index 0610098ca..02662fc88 100644 --- a/src/common/localization/en-us.ts +++ b/src/common/localization/en-us.ts @@ -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.", } }, }; diff --git a/src/common/localization/es-cl.ts b/src/common/localization/es-cl.ts index 2c2a290cb..728dca275 100644 --- a/src/common/localization/es-cl.ts +++ b/src/common/localization/es-cl.ts @@ -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", diff --git a/src/common/mockFactory.ts b/src/common/mockFactory.ts index 9062df19f..f80714d43 100644 --- a/src/common/mockFactory.ts +++ b/src/common/mockFactory.ts @@ -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()), }; } diff --git a/src/common/strings.ts b/src/common/strings.ts index 9a7249de4..052f76ff9 100644 --- a/src/common/strings.ts +++ b/src/common/strings.ts @@ -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, diff --git a/src/common/themes.ts b/src/common/themes.ts index 4859da725..2cfbdd530 100644 --- a/src/common/themes.ts +++ b/src/common/themes.ts @@ -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; diff --git a/src/common/utils.ts b/src/common/utils.ts index d4722568c..37f4bb364 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -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; diff --git a/src/config/fabric-icons.json b/src/config/fabric-icons.json index c29704e75..8f2c19b50 100644 --- a/src/config/fabric-icons.json +++ b/src/config/fabric-icons.json @@ -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" } ] -} \ No newline at end of file +} diff --git a/src/models/applicationState.ts b/src/models/applicationState.ts index feadddfbd..559c17f89 100644 --- a/src/models/applicationState.ts +++ b/src/models/applicationState.ts @@ -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", +} diff --git a/src/providers/storage/azureBlobStorage.ts b/src/providers/storage/azureBlobStorage.ts index 43060279a..1608ce69d 100644 --- a/src/providers/storage/azureBlobStorage.ts +++ b/src/providers/storage/azureBlobStorage.ts @@ -256,7 +256,7 @@ export class AzureBlobStorage implements IStorageProvider { return asset; } } - else{ + else { return null; } } diff --git a/src/react/components/common/confirm/confirm.scss b/src/react/components/common/confirm/confirm.scss new file mode 100644 index 000000000..94e547313 --- /dev/null +++ b/src/react/components/common/confirm/confirm.scss @@ -0,0 +1,9 @@ +@import './../../../../assets/sass/theme.scss'; + +.spinner-container { + display: flex; + width: 16rem; + height: 4rem; + align-items: center; + justify-content: center; +} diff --git a/src/react/components/common/confirm/confirm.tsx b/src/react/components/common/confirm/confirm.tsx index e58f851a7..48b2ed3d7 100644 --- a/src/react/components/common/confirm/confirm.tsx +++ b/src/react/components/common/confirm/confirm.tsx @@ -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() { diff --git a/src/react/components/common/documentFilePicker/documentFilePicker.scss b/src/react/components/common/documentFilePicker/documentFilePicker.scss index c256ac602..c38785a0e 100644 --- a/src/react/components/common/documentFilePicker/documentFilePicker.scss +++ b/src/react/components/common/documentFilePicker/documentFilePicker.scss @@ -13,6 +13,7 @@ } .sourceDropdown { width: 95px; + margin-bottom: 10px; } .local-file { float: right; diff --git a/src/react/components/common/imageMap/imageMap.tsx b/src/react/components/common/imageMap/imageMap.tsx index 2da9a4d62..980e67a74 100644 --- a/src/react/components/common/imageMap/imageMap.tsx +++ b/src/react/components/common/imageMap/imageMap.tsx @@ -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); diff --git a/src/react/components/common/tagInput/tableTagConfig.scss b/src/react/components/common/tagInput/tableTagConfig.scss new file mode 100644 index 000000000..9b2f7a7d4 --- /dev/null +++ b/src/react/components/common/tagInput/tableTagConfig.scss @@ -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; +} diff --git a/src/react/components/common/tagInput/tableTagConfig.tsx b/src/react/components/common/tagInput/tableTagConfig.tsx new file mode 100644 index 000000000..9df097b38 --- /dev/null +++ b/src/react/components/common/tagInput/tableTagConfig.tsx @@ -0,0 +1,1153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +import React, { useEffect, useMemo, useState, useRef } from 'react'; +import { useSelector } from 'react-redux'; +import { + Customizer, ICustomizations, ChoiceGroup, IChoiceGroupOption, + PrimaryButton, DetailsList, IColumn, TextField, Dropdown, SelectionMode, + DetailsListLayoutMode, FontIcon, CheckboxVisibility, IContextualMenuItem, + CommandBar, Selection, Separator, IObjectWithKey, ActionButton +} from "@fluentui/react"; +import { getPrimaryGreyTheme, getPrimaryGreenTheme, getRightPaneDefaultButtonTheme, getGreenWithWhiteBackgroundTheme, getPrimaryBlueTheme, getDefaultTheme } from '../../../../common/themes'; +import { FieldFormat, FieldType, IApplicationState, ITableRegion, ITableTag, ITag, TableElements, TagInputMode, TableVisualizationHint } from '../../../../models/applicationState'; +import { filterFormat, getTagCategory, useDebounce } from "../../../../common/utils"; +import { toast } from "react-toastify"; +import "./tableTagConfig.scss"; +import { interpolate, strings } from "../../../../common/strings"; +import _ from "lodash"; + +interface ITableTagConfigProps { + setTagInputMode: (addTableMode: TagInputMode, selectedTableTagToLabel?: ITableTag, selectedTableTagBody?: ITableRegion[][][]) => void; + addTableTag: (table: any) => void; + splitPaneWidth: number; + tableTag?: ITableTag; + reconfigureTableConfirm?: (originalTagName: string, tagName: string, tagType: FieldType.Array | FieldType.Object, tagFormat: FieldFormat, visualizationHint: TableVisualizationHint, deletedColumns: ITableConfigItem[], deletedRows: ITableConfigItem[], newRows: ITableConfigItem[], newColumns: ITableConfigItem[]) => void; + selectedTableBody: ITableRegion[][][]; +} + +interface ITableTagConfigState { + rows?: ITableConfigItem[], + columns: ITableConfigItem[], + name: { + tableName: string, + originalTableName?: string; + }, + type: FieldType.Object | FieldType.Array, + format: FieldFormat.NotSpecified, + headerTypeAndFormat: TableElements.columns | TableElements.rows; + originalName?: string; + deletedRows?: ITableConfigItem[], + deletedColumns?: ITableConfigItem[], +} +interface ITableConfigItem { + name: string, + format: string, + type: string; + originalName?: string; + originalFormat?: string, + originalType?: string; + documentCount?: number +} + +const tableFormatOptions: IChoiceGroupOption[] = [ + { + key: FieldType.Object, + text: 'Fixed sized', + iconProps: { iconName: 'Table' } + }, + { + key: FieldType.Array, + text: 'Row dynamic', + iconProps: { iconName: 'InsertRowsBelow' } + }, +]; +const headersFormatAndTypeOptions: IChoiceGroupOption[] = [ + { + key: TableElements.columns, + text: 'Column\n fields', + iconProps: { iconName: 'TableHeaderRow' } + }, + { + key: TableElements.rows, + text: 'Row\n fields', + iconProps: { iconName: 'TableFirstColumn' } + }, +]; + +const dark: ICustomizations = { + settings: { + theme: getRightPaneDefaultButtonTheme(), + }, + scopedSettings: {}, +}; + +const defaultTheme: ICustomizations = { + settings: { + theme: getDefaultTheme(), + }, + scopedSettings: {}, +}; + +const formatOptions = (type = FieldType.String) => { + const options = []; + const formats = filterFormat(type) + Object.entries(formats).forEach(([key, value]) => { + options.push({ key, text: value }) + }); + + return options; +}; + +const typeOptions = () => { + const options = []; + Object.entries(FieldType).forEach(([key, value]) => { + if (value !== FieldType.Object && value !== FieldType.Array) { + options.push({ key, text: value }); + } + }); + return options; +}; + +const defaultRowOrColumn = { name: "", type: FieldType.String, format: FieldFormat.NotSpecified, documentCount: 0 }; + +/** + * @name - Table tag configuration + * @description - Configures table tag (assigns row's/column's headers and their respective data types and formats) + */ + +export default function TableTagConfig(props: ITableTagConfigProps) { + const { setTagInputMode = null, addTableTag = null, splitPaneWidth = null } = props; + const containerWidth = splitPaneWidth > 520 ? splitPaneWidth - 10: 510; + const inputTableName = useRef(null); + const lastColumnInputRef = useRef(null); + const lastRowInputRef = useRef(null); + // Initial state + let table: ITableTagConfigState; + if (props.tableTag) { + if (props.tableTag?.type === FieldType.Object) { + if (props.tableTag.visualizationHint === TableVisualizationHint.Vertical) { + table = { + name: {tableName: props.tableTag.name, originalTableName: props.tableTag.name}, + type: FieldType.Object, + format: FieldFormat.NotSpecified, + rows: props.tableTag.fields?.map(row => ({ name: row.fieldKey, type: row.fieldType, format: row.fieldFormat, originalName: row.fieldKey, originalFormat: row.fieldFormat, originalType: row.fieldType })) || [defaultRowOrColumn], + columns: props.tableTag?.definition?.fields?.map(col => ({ name: col.fieldKey, type: col.fieldType, format: col.fieldFormat, originalName: col.fieldKey, originalFormat: col.fieldFormat, originalType: col.fieldType })) || [defaultRowOrColumn], + headerTypeAndFormat: TableElements.columns, + deletedColumns: [], + deletedRows: [], + } + + } else { + table = { + name: {tableName: props.tableTag.name, originalTableName: props.tableTag.name}, + type: FieldType.Object, + format: FieldFormat.NotSpecified, + rows: props.tableTag?.definition?.fields?.map(row => ({ name: row.fieldKey, type: row.fieldType, format: row.fieldFormat, originalName: row.fieldKey, originalFormat: row.fieldFormat, originalType: row.fieldType })) || [defaultRowOrColumn], + columns: props.tableTag?.fields?.map(col => ({ name: col.fieldKey, type: col.fieldType, format: col.fieldFormat, originalName: col.fieldKey, originalFormat: col.fieldFormat, originalType: col.fieldType })) || [defaultRowOrColumn], + headerTypeAndFormat: TableElements.rows, + deletedColumns: [], + deletedRows: [], + } + } + } else { + table = { + name: { tableName: props.tableTag.name, originalTableName: props.tableTag.name }, + type: FieldType.Array, + format: FieldFormat.NotSpecified, + rows: [defaultRowOrColumn], + columns: props.tableTag?.definition?.fields?.map(col => ({ name: col.fieldKey, type: col.fieldType, format: col.fieldFormat, originalName: col.fieldKey, originalFormat: col.fieldFormat, originalType: col.fieldType })), + headerTypeAndFormat: TableElements.columns, + deletedColumns: [], + } + } + + } else { + table = { + name: {tableName: ""}, + type: FieldType.Object, + format: FieldFormat.NotSpecified, + rows: [defaultRowOrColumn], + columns: [defaultRowOrColumn], + headerTypeAndFormat: TableElements.columns, + }; + } + + const currentProjectTags = useSelector<ITag[]>((state: IApplicationState) => state.currentProject.tags); + const [tableTagName, setTableTagName] = useState(table.name); + const [type, setType] = useState<FieldType.Object | FieldType.Array>(table.type); + const [format, setFormat] = useState<FieldFormat.NotSpecified>(table.format); + const [columns, setColumns] = useState(table.columns); + const [rows, setRows] = useState<ITableConfigItem[]>(table.rows); + const [notUniqueNames, setNotUniqueNames] = useState<{ columns: [], rows: [], tags: boolean }>({ columns: [], rows: [], tags: false }); + const [headersFormatAndType, setHeadersFormatAndType] = useState<TableElements.columns | TableElements.rows>(table.headerTypeAndFormat); + const [selectedColumn, setSelectedColumn] = useState<IObjectWithKey>(undefined); + const [selectedRow, setSelectedRow] = useState<IObjectWithKey>(undefined); + const [deletedColumns, setDeletedColumns] = useState(table.deletedColumns); + const [deletedRows, setDeletedRows] = useState<ITableConfigItem[]>(table.deletedRows); + // const [headerTypeAndFormat, setHeaderTypeAndFormat] = useState<string>(table.headerTypeAndFormat); + const [shouldAutoFocus, setShouldAutoFocus] = useState(null); + + + const isCompatibleWithType = (documentCount: number, type: string, newType: string) => { + return documentCount <= 0 ? true : getTagCategory(type) === getTagCategory(newType); + } + + function selectColumnType(idx: number, type: string, docCount: number) { + setColumns(columns.map((col, currIdx) => { + if (idx === currIdx) { + if (isCompatibleWithType(docCount, col.originalType, type)) + return { ...col, type, format: FieldFormat.NotSpecified } + else { + toast.warn(_.capitalize(interpolate(strings.tags.regionTableTags.configureTag.errors.notCompatibleTableColOrRowType, { kind: "column" }))); + return col; + } + } else { + return col + } + })); + } + + function selectRowType(idx: number, type: string, docCount: number) { + setRows(rows.map((row, currIdx) => { + if (idx === currIdx) { + if (isCompatibleWithType(docCount, row.originalType, type)) + return { ...row, type, format: FieldFormat.NotSpecified } + else { + toast.warn(_.capitalize(interpolate(strings.tags.regionTableTags.configureTag.errors.notCompatibleTableColOrRowType, { kind: "row" }))); + return row; + } + } else { + return row + } + })); + } + + function selectColumnFormat(idx: number, format: string) { + setColumns(columns.map((col, currIdx) => idx === currIdx ? { ...col, format } : col + )); + } + + function selectRowFormat(idx: number, format: string) { + setRows(rows.map((row, currIdx) => idx === currIdx ? { ...row, format } : row + )); + } + + const detailListWidth = { + nameInput: containerWidth * 0.45, + typeInput: containerWidth * 0.176, + formatInput: containerWidth * 0.176, + } + + const columnListColumns: IColumn[] = [ + { + key: "name", + name: "name", + fieldName: "name", + minWidth: detailListWidth.nameInput, + isResizable: false, + onRender: (row, index) => { + return ( + <TextField + componentRef={(index === columns.length - 1 && index !== 0) ? lastColumnInputRef : null} + className={"column-name_input"} + theme={getGreenWithWhiteBackgroundTheme()} + onChange={(e) => handleTextInput(e.target["value"], TableElements.column, index)} + value={row.name} + errorMessage={getTextInputError(notUniqueNames.columns, row.name.trim(), index)} + onRenderLabel={() => { + return row.originalName ? + <div className={"input-label-original-name original-value"}> + Original name: {row.originalName} + </div> + : null; + }} + /> + ) + }, + }, + { + key: "type", + name: "type", + fieldName: "type", + minWidth: detailListWidth.typeInput, + isResizable: false, + onRender: (row, index) => headersFormatAndType === TableElements.columns ? + <Customizer {...defaultTheme}> + <Dropdown + style={{ marginTop: 16 }} + className="type_dropdown" + placeholder={row.type} + defaultSelectedKey={FieldType.String} + options={typeOptions()} + theme={getGreenWithWhiteBackgroundTheme()} + onChange={(e, val) => selectColumnType(index, val.text, row.documentCount)} + /> + </Customizer> + : <></> + }, + { + key: "format", + name: "format", + fieldName: "format", + minWidth: detailListWidth.formatInput, + isResizable: false, + onRender: (row, index) => headersFormatAndType === TableElements.columns ? + <Customizer {...defaultTheme}> + <Dropdown + style={{ marginTop: 16 }} + className="format_dropdown" + placeholder={row.format} + selectedKey={row.format} + options={formatOptions(row.type)} + theme={getGreenWithWhiteBackgroundTheme()} + onChange={(e, val) => { + selectColumnFormat(index, val.text); + }} + /> + </Customizer> + : <></> + }, + ]; + + const rowListColumns: IColumn[] = [ + { + key: "name", + name: "name", + fieldName: "name", + minWidth: detailListWidth.nameInput, + isResizable: false, + onRender: (row, index) => { + return ( + <TextField + componentRef={(index === rows.length - 1 && index !== 0) ? lastRowInputRef : null} + className="row-name_input" + theme={getGreenWithWhiteBackgroundTheme()} + onChange={(e) => handleTextInput(e.target["value"], TableElements.row, index)} + value={row.name} + errorMessage={getTextInputError(notUniqueNames.rows, row.name, index)} + onRenderLabel={() => { + return row.originalName ? + <div className={"input-label-original-name original-value"}> + Original name: {row.originalName} + </div> + : null; + }} + /> + ) + }, + }, + { + key: "type", + name: "type", + fieldName: "type", + minWidth: detailListWidth.typeInput, + isResizable: false, + onRender: (row, index) => headersFormatAndType === TableElements.rows ? + <Customizer {...defaultTheme}> + <Dropdown + className="type_dropdown" + style={{ marginTop: 16 }} + placeholder={row.type} + defaultSelectedKey={FieldType.String} + options={typeOptions()} + theme={getGreenWithWhiteBackgroundTheme()} + onChange={(e, val) => selectRowType(index, val.text, row.documentCount)} + /> + </Customizer> + : <></> + }, + { + key: "format", + name: "format", + fieldName: "format", + minWidth: detailListWidth.formatInput, + isResizable: false, + onRender: (row, index) => headersFormatAndType === TableElements.rows ? + <Customizer {...defaultTheme}> + <Dropdown + className="format_dropdown" + style={{ marginTop: 16 }} + placeholder={row.format} + selectedKey={row.format} + options={formatOptions(row.type)} + theme={getGreenWithWhiteBackgroundTheme()} + onChange={(e, val) => { + selectRowFormat(index, val.text); + }} + /> + </Customizer> + : <></> + }, + ]; + + + function addColumn() { + setColumns([...columns, defaultRowOrColumn]); + setShouldAutoFocus(TableElements.column); + } + + function addRow() { + setRows([...rows, defaultRowOrColumn]); + setShouldAutoFocus(TableElements.row); + } + + function handleTextInput(name: string, role: string, index: number) { + if (role === TableElements.column) { + setColumns( + columns.map((column, currIndex) => (index === currIndex) + ? { ...column, name } + : column) + ); + } else { + setRows( + rows.map((row, currIndex) => (index === currIndex) ? + { ...row, name } + : row) + ); + }; + } + + function setTableName(name: string) { + setTableTagName({...tableTagName,tableName: name}); + } + + // Row/Column headers command bar (reorder, delete) + function getRowsHeaderItems(): IContextualMenuItem[] { + const currSelectionIndex = rowSelection.getSelectedIndices()[0]; + return [ + { + key: 'Name', + text: 'Name', + className: "list-headers_name", + style: { width: detailListWidth.nameInput - 122 }, + disabled: true, + }, + { + key: 'moveUp', + text: 'Move up', + iconOnly: true, + iconProps: { iconName: 'Up' }, + onClick: (e) => { + onReOrder(-1, TableElements.rows) + }, + disabled: !selectedRow || currSelectionIndex === 0, + }, + { + key: 'moveDown', + text: 'Move down', + iconOnly: true, + iconProps: { iconName: 'Down' }, + onClick: (e) => { + onReOrder(1, TableElements.rows) + }, + disabled: !selectedRow! || currSelectionIndex === rows.length - 1, + }, + { + key: 'deleteRow', + text: 'Delete row', + iconOnly: true, + iconProps: { iconName: 'Delete' }, + onClick: () => { + const selectedRowIndex = rowSelection.getSelectedIndices()[0]; + if (props.tableTag && rows[selectedRowIndex].originalName) { + const deletedRow = Object.assign({}, rows[selectedRowIndex]); + deletedRow.name = deletedRow.originalName; + deletedRow.format = deletedRow.originalFormat; + deletedRow.type = deletedRow.originalType; + setDeletedRows([...deletedRows, deletedRow]); + } + setRows(rows.filter((i, idx) => idx !== rowSelection.getSelectedIndices()[0])) + }, + disabled: !selectedRow! || rows.length === 1, + }, + { + key: 'type', + text: 'Type', + className: "list-headers_type", + style: { width: detailListWidth.typeInput }, + disabled: true, + }, + { + key: 'format', + text: 'Format', + className: "list-headers_format", + disabled: true, + }, + ]; + }; + function getColumnsHeaderItems(): IContextualMenuItem[] { + const currSelectionIndex = columnSelection.getSelectedIndices()[0]; + + return [ + { + key: 'Name', + text: 'Name', + className: "list-headers_name", + style: { width: detailListWidth.nameInput - 120 }, + disabled: true, + resizable: true, + }, + { + key: 'moveUp', + text: 'Move up', + iconOnly: true, + iconProps: { iconName: 'Up' }, + onClick: (e) => { + onReOrder(-1, TableElements.columns) + + }, + disabled: !selectedColumn || currSelectionIndex === 0, + }, + { + key: 'moveDown', + text: 'Move down', + iconOnly: true, + iconProps: { iconName: 'Down' }, + onClick: (e) => { + onReOrder(1, TableElements.columns) + }, + disabled: !selectedColumn || currSelectionIndex === columns.length - 1, + }, + { + key: 'deleteColumn', + text: 'Delete column', + iconOnly: true, + iconProps: { iconName: 'Delete', }, + onClick: () => { + const selectedColumnIndex = columnSelection.getSelectedIndices()[0]; + if (props.tableTag && columns[selectedColumnIndex].originalName) { + const deletedColumn = Object.assign({}, columns[selectedColumnIndex]); + deletedColumn.name = deletedColumn.originalName + deletedColumn.format = deletedColumn.originalFormat; + deletedColumn.type = deletedColumn.originalType; + setDeletedColumns([...deletedColumns, deletedColumn]) + } + setColumns(columns.filter((i, idx) => idx !== selectedColumnIndex)); + }, + disabled: !selectedColumn || columns.length === 1, + }, + { + key: 'type', + text: 'Type', + className: "list-headers_type", + style: { width: detailListWidth.typeInput }, + disabled: true, + }, + { + key: 'format', + text: 'Format', + className: "list-headers_format", + disabled: true, + }, + ]; + }; + + // Validation // + function getTextInputError(array: any[], rowName: string, index: number) { + if (!rowName?.length) { + return strings.tags.regionTableTags.configureTag.errors.emptyName + } else if (array.length && array.findIndex((item) => (item === index)) !== -1) { + return strings.tags.regionTableTags.configureTag.errors.notUniqueName; + } else { + return undefined; + } + }; + + function checkNameUniqueness(array: ITableConfigItem[], arrayName: string) { + const namesMap = {}; + let notUniques = []; + array.forEach((item, idx) => { + + if (item.name && item.name.length) { + const name = item.name.trim(); + namesMap[name] = namesMap[name] || []; + namesMap[name].push(idx) + } + }); + + for (const name in namesMap) { + if (namesMap[name].length > 1) { + notUniques = namesMap[name]; + } + } + setNotUniqueNames({ ...notUniqueNames, [arrayName]: notUniques }) + } + + // Check names uniqueness for rows and columns as you type , with a delay + const delay = 400; + const debouncedColumns = useDebounce(columns, delay); + const debouncedRows = useDebounce(rows, delay); + + useEffect(() => { + if (columns) { + checkNameUniqueness(debouncedColumns, TableElements.columns) + } + }, [debouncedColumns]); + + useEffect(() => { + if (rows) { + checkNameUniqueness(debouncedRows, TableElements.rows); + } + }, [debouncedRows]); + + // Check tableName uniqueness as type + const debouncedTableTagName = useDebounce(tableTagName, delay); + + useEffect(() => { + if (tableTagName) { + const existingTagName = currentProjectTags.find((item: ITag) => item.name === tableTagName.tableName.trim()); + setNotUniqueNames({ ...notUniqueNames, tags: existingTagName !== undefined ? true : false }) + } + }, [debouncedTableTagName, currentProjectTags]); + + function trimFieldNames(array: ITableConfigItem[]) { + return array.map(i => ({ ...i, name: i.name.trim() })); + } + + function save(cleanTableName: string, cleanRows: ITableConfigItem[], cleanColumns: ITableConfigItem[]) { + const [ firstLayerFieldsInput, secondLayerFieldsInput ] = getFieldsLayersInput(headersFormatAndType, cleanRows, cleanColumns); + const definition = getDefinitionLayer(cleanTableName, secondLayerFieldsInput); + const fieldsLayer = getFieldsLayer(cleanTableName, firstLayerFieldsInput); + const itemType = getItemType(cleanTableName); + const visualizationHint = getVisualizationHint(headersFormatAndType); + const tableTagToAdd = { + name: cleanTableName, + type, + columns: cleanColumns, + format : FieldFormat.NotSpecified, + itemType, + fields: fieldsLayer, + definition, + visualizationHint, + } + if (type === FieldType.Object) { + tableTagToAdd[TableElements.rows] = cleanRows; + } + addTableTag(tableTagToAdd); + setTagInputMode(TagInputMode.Basic, null, null); + toast.success(`Successfully ${props.tableTag ? "reconfigured" : "saved"} "${tableTagName.tableName}" table tag.`, { autoClose: 8000 }); + } + + function getItemType(cleanTableName) { + if (type === FieldType.Object) { + return null; + } else { + return cleanTableName + "_object"; + } + } + + function getFieldsLayersInput(headersFormatAndType, cleanRows, cleanColumns) { + if (type === FieldType.Object) { + if (headersFormatAndType === TableElements.columns) { + return [ cleanRows, cleanColumns ]; + } else { + return [ cleanColumns, cleanRows ]; + } + } else { + return [ cleanRows, cleanColumns ]; + } + } + + function getDefinitionLayer(cleanTableName, secondLayerFieldsInput) { + return { + fieldKey: cleanTableName + "_object", + fieldType: FieldType.Object, + fieldFormat: FieldFormat.NotSpecified, + itemType: null, + fields: secondLayerFieldsInput.map((field) => { + return { + fieldKey: field.name, + fieldType: field.type, + fieldFormat: field.format, + itemType: null, + fields: null, + } + }) + } + } + + function getVisualizationHint(headersFormatAndType) { + if (type === FieldType.Object) { + if (headersFormatAndType === TableElements.columns) { + return TableVisualizationHint.Vertical; + } else { + return TableVisualizationHint.Horizontal; + } + } else { + return null; + } + } + + function getFieldsLayer(cleanTableName, firstLayerFieldsInput) { + if (type === FieldType.Object) { + return firstLayerFieldsInput.map((field) => { + return { + fieldKey: field.name, + fieldType: cleanTableName + "_object", + fieldFormat: FieldFormat.NotSpecified, + itemType: null, + fields: null, + } + }); + } else { + return null; + } + } + + function hasEmptyNames(array: ITableConfigItem[]) { + return array.find((i) => !i.name.length) !== undefined ? true : false + } + + + + function getCleanTable() { + let cleanRows = rows; + let cleanColumns = columns; + if (headersFormatAndType === TableElements.columns) { + cleanRows = rows.map((row) => { + return { + ...row, + type: FieldType.String, + format: FieldFormat.NotSpecified, + } + }); + } else if (headersFormatAndType === TableElements.rows) { + cleanColumns = columns.map((col) => ({ + ...col, + type: FieldType.String, + format: FieldFormat.NotSpecified + })); + } + cleanColumns = trimFieldNames(columns); + if (type === FieldType.Object) { + cleanRows = trimFieldNames(rows); + } + const cleanTableName = tableTagName.tableName.trim(); + // const cleanOriginalTableName = tableTagName?.originalTableName?.trim(); + return { cleanTableName, cleanRows, cleanColumns }; + } + + function validateInput() { + return !( + notUniqueNames.rows.length > 0 + || notUniqueNames.columns.length > 0 + || (props.tableTag && notUniqueNames.tags && (tableTagName.tableName !== tableTagName.originalTableName)) + || (notUniqueNames.tags && !props.tableTag) + || !tableTagName.tableName.length + || hasEmptyNames(columns) + || (type === FieldType.Object && hasEmptyNames(rows)) + ); + } + + // Row selection + const rowSelection = useMemo(() => + new Selection({ + onSelectionChanged: () => { + setSelectedRow(rowSelection.getSelection()[0]) + }, selectionMode: SelectionMode.single, + }), [] + ); + + const columnSelection = useMemo(() => + new Selection({ + onSelectionChanged: () => { + setSelectedColumn(columnSelection.getSelection()[0]) + }, selectionMode: SelectionMode.single, + }), [] + ); + + // Reorder items + function onReOrder(displacement: number, role: string) { + const items = role === TableElements.rows ? [...rows] : [...columns]; + const selection = role === TableElements.rows ? rowSelection : columnSelection; + const selectedIndex = selection.getSelectedIndices()[0]; + const itemToBeMoved = items[selectedIndex]; + const newIndex = selectedIndex + displacement; + if (newIndex < 0 || newIndex > items.length - 1) { + return; + } + + items.splice(selectedIndex, 1); + items.splice(newIndex, 0, itemToBeMoved); + + if (role === TableElements.rows) { + rowSelection.setIndexSelected(newIndex, true, false); + setRows(items); + } else { + columnSelection.setIndexSelected(newIndex, true, true); + setColumns(items); + } + } + + function restoreDeletedField(fieldType: TableElements, index: number) { + let fields; + let deletedFields; + let setFields; + let setDeletedFields; + switch (fieldType) { + case TableElements.row: + fields = rows; + deletedFields = [...deletedRows]; + setFields = setRows; + setDeletedFields = setDeletedRows; + break; + case TableElements.column: + fields = columns; + deletedFields = [...deletedColumns]; + setFields = setColumns; + setDeletedFields = setDeletedColumns; + break; + } + setFields([...fields, deletedFields[index]]); + setDeletedFields([...deletedFields].slice(0, index).concat([...deletedFields].slice(index+1, deletedFields.length))); + } + + function getDeletedFieldsTable(fieldType: TableElements) { + let deletedFields; + switch (fieldType) { + case TableElements.row: + deletedFields = deletedRows; + break; + case TableElements.column: + deletedFields = deletedColumns; + break; + } + const tableBody = [[ + <tr className="compact-row" key={"row-h"}> + <th key={"row-h-0"} className=""> + <div className="mr-4"> + {fieldType.charAt(0).toUpperCase() + fieldType.slice(1) + " fields that'll be deleted"} + </div> + </th> + <th key={"row-h-1"} className=""></th> + </tr> + ]]; + for (let i = 0; i < deletedFields.length; i++) { + tableBody.push([ + <tr className="compact-row" key={`row-${i}`}> + <td key={`cell-${i}-0`} className=""> + <div className="flex-center"> + {deletedFields[i].originalName} + </div> + </td> + <td key={`cell-${i}-1`} className=""> + <ActionButton className="restore-button flex-center" + onClick={() => { + restoreDeletedField(fieldType, i) + }}> + <FontIcon className="restore-icon mr-1" iconName="UpdateRestore" /> + Restore + </ActionButton> + </td> + </tr> + ]) + } + return tableBody; + } + + // Table preview + function getTableBody() { + let tableBody = null; + const isRowDynamic = type === FieldType.Array; + if (table.rows.length !== 0 && table.columns.length !== 0) { + tableBody = []; + for (let i = 0; i < (isRowDynamic ? 2 : rows.length + 1); i++) { + const tableRow = []; + for (let j = 0; j < columns.length + 1; j++) { + if (i === 0 && j !== 0) { + const columnHeaderWasRenamed = props.tableTag && columns[j - 1].name !== columns[j - 1].originalName; + tableRow.push( + <th key={`col-h-${j}`} className="header_column"> + {columnHeaderWasRenamed && + <div className="renamed-value">{columns[j - 1].originalName}</div> + } + <div className="original-value"> + {columns[j - 1].name} + </div> + </th>); + } else if (j === 0 && i !== 0) { + if (!isRowDynamic) { + const rowHeaderWasRenamed = props.tableTag && rows[i - 1].name !== rows[i - 1].originalName; + tableRow.push( + <th key={`row-h-${j}`} className="header_row"> + {rowHeaderWasRenamed && + <div className="renamed-value"> + {rows[i - 1].originalName} + </div> + } + <div className="original-value"> + {rows[i - 1].name} + </div> + </th> + ); + } + } else if (j === 0 && i === 0) { + if (!isRowDynamic) { + tableRow.push(<th key={"ignore"} className="header_empty" ></th>); + } + } else { + tableRow.push(<td key={`cell-${i}-${j}`} className="table-cell" />); + } + } + tableBody.push(<tr key={`row-${i}`}>{tableRow}</tr>); + } + } + return tableBody + }; + + + function getTableTagNameErrorMessage(): string { + if (props.tableTag && tableTagName.tableName.trim() === props.tableTag.name) { + return ""; + } else if (!tableTagName.tableName.trim().length) { + return strings.tags.regionTableTags.configureTag.errors.assignTagName + } else if (notUniqueNames.tags) { + return strings.tags.regionTableTags.configureTag.errors.notUniqueTagName; + } + return ""; + } + + const [tableChanged, setTableChanged] = useState<boolean>(false); + useEffect(() => { + setTableChanged( + (_.isEqual(columns, table.columns) && _.isEqual(rows, table.rows)) ? false : true) + }, [columns, rows, table.columns, table.rows]); + + // Focus once on table name input when the component loads + useEffect(() => { + inputTableName.current.focus(); + }, []); + // Sets focus on last added input + useEffect(() => { + if (shouldAutoFocus === TableElements.column && lastColumnInputRef.current) { + lastColumnInputRef.current.focus(); + } + else if (shouldAutoFocus === TableElements.row && lastRowInputRef.current) { + lastRowInputRef.current.focus(); + } + setShouldAutoFocus(null); + }, [shouldAutoFocus]); + + // Render + return ( + <Customizer {...dark}> + <div className="config-view_container"> + <h4 className="mt-2">{props.tableTag ? "Reconfigure table tag" : "Configure table tag"}</h4> + <h5 className="mt-3 mb-1">Name</h5> + {tableTagName.originalTableName && + <div className={"original-table-name"}> + Original name: {tableTagName.originalTableName} + </div> + } + <TextField + componentRef={inputTableName} + className="table-name_input ml-12px" + theme={getGreenWithWhiteBackgroundTheme()} + onChange={(event) => setTableName(event.target["value"])} + value={tableTagName.tableName} + errorMessage={getTableTagNameErrorMessage()} + /> + {!props.tableTag && + <> + <h5 className="mt-4">Type</h5> + <ChoiceGroup + className="ml-12px" + onChange={(event, option) => { + if (option.key === FieldType.Object) { + setType(FieldType.Object); + } else { + setType(FieldType.Array) + setHeadersFormatAndType(TableElements.columns); + } + }} + defaultSelectedKey={FieldType.Object} + options={tableFormatOptions} + theme={getRightPaneDefaultButtonTheme()} + /> + {type === FieldType.Object && <> + <h5 className="mt-4" >Configure type and format for:</h5> + <ChoiceGroup + className="ml-12px type-format" + defaultSelectedKey={TableElements.columns} + options={headersFormatAndTypeOptions} + onChange={(e, option) => { + if (option.key === TableElements.columns) { + setHeadersFormatAndType(TableElements.columns) + + } else { + setHeadersFormatAndType(TableElements.rows) + } + }} + required={false} /> + </> + } + </> + } + <div className="columns_container mb-4 ml-12px"> + <h5 className="mt-3">Column fields</h5> + <div className="columns-list_container"> + <DetailsList + className="columns" + items={columns} + columns={columnListColumns} + isHeaderVisible={true} + theme={getRightPaneDefaultButtonTheme()} + compact={false} + setKey="none" + selection={columnSelection} + layoutMode={DetailsListLayoutMode.justified} + checkboxVisibility={CheckboxVisibility.always} + onRenderDetailsHeader={() => ( + <div className="list_header"> + <CommandBar items={getColumnsHeaderItems()} /> + <Separator styles={{ root: { height: 2, padding: 0 } }} /> + </div> + )} + + /> + </div> + <PrimaryButton + theme={getPrimaryBlueTheme()} + className="add_button ml-12px" + onClick={addColumn}> + <FontIcon iconName="Add" className="mr-2" /> + Add column + </PrimaryButton> + {deletedColumns?.length > 0 && + <div className="mt-3"> + <table className=""> + <tbody> + {getDeletedFieldsTable(TableElements.column)} + </tbody> + </table> + </div> + } + </div> + {((props.tableTag?.type === FieldType.Object) || type === FieldType.Object) && + <div className="rows_container mb-4 ml-12px"> + <h5 className="">Row fields</h5> + <div className="rows-list_container"> + <DetailsList + className="rows" + items={rows} + columns={rowListColumns} + isHeaderVisible={true} + theme={getRightPaneDefaultButtonTheme()} + compact={false} + setKey="none" + selection={rowSelection} + layoutMode={DetailsListLayoutMode.justified} + checkboxVisibility={CheckboxVisibility.always} + selectionPreservedOnEmptyClick={true} + onRenderDetailsHeader={() => ( + <div className="list_header"> + <CommandBar items={getRowsHeaderItems()} /> + <Separator styles={{ root: { height: 2, padding: 0 } }} /> + </div> + )} + /> + </div> + <PrimaryButton + theme={getPrimaryBlueTheme()} + className="add_button" + onClick={addRow}> + <FontIcon iconName="Add" className="mr-2" /> + Add row + </PrimaryButton> + {deletedRows?.length > 0 && + <div className="mt-3"> + <table className=""> + <tbody> + {getDeletedFieldsTable(TableElements.row)} + </tbody> + </table> + </div> + } + </div> + } + { + (tableChanged || props.tableTag) && + <div className="preview_container ml-12px"> + <h5 className="mt-3 mb-1">Preview</h5> + {tableTagName.tableName && + <> + {props.tableTag && tableTagName.originalTableName !== tableTagName.tableName && + <div className="tableName-original original-value"> + Table name: {tableTagName.originalTableName} + </div> + } + <span className="tableName-current" + style={{ borderBottom: props.tableTag ? `4px solid ${props.tableTag.color}` : null }}> + <span>Table name: </span> + <span className="table-name-preview">{tableTagName.tableName}</span> + </span> + </> + } + { + type === FieldType.Array && <div className="rowDynamic_message">The number of rows is specified when labeling each document.</div> + } + <div className="table_container"> + <table className="table"> + <tbody> + {getTableBody()} + </tbody> + </table> + </div> + </div> + } + <div className="control-buttons_container"> + <PrimaryButton + className="cancel" + theme={getPrimaryGreyTheme()} + onClick={() => { + if (props.tableTag) { + if (props.selectedTableBody) { + if (!props.selectedTableBody[0].length || props.selectedTableBody.length !== 0) + setTagInputMode(TagInputMode.LabelTable, + props.tableTag, + props.selectedTableBody); + } else { + setTagInputMode(TagInputMode.Basic, null, null); + } + } else { + setTagInputMode(TagInputMode.Basic, null, null); + } + }}>Cancel</PrimaryButton> + <PrimaryButton + className="save" + theme={getPrimaryGreenTheme()} + onClick={() => { + if (!validateInput()) { + toast.error(strings.tags.regionTableTags.configureTag.errors.checkFields, { autoClose: 8000 }); + return; + } else { + const { cleanTableName, cleanRows, cleanColumns} = getCleanTable(); + if (props.tableTag) { + const tableTagToReconfigure = { + name: cleanTableName, + columns: cleanColumns, + deletedColumns, + headersFormatAndType, + visualizationHint: props.tableTag.visualizationHint, + } + if (type === FieldType.Object) { + tableTagToReconfigure[TableElements.rows] = cleanRows; + tableTagToReconfigure["deletedRows"] = deletedRows; + tableTagToReconfigure["type"] = FieldType.Object; + } else { + tableTagToReconfigure[TableElements.rows] = null; + tableTagToReconfigure["deletedRows"] = null; + tableTagToReconfigure["type"] = FieldType.Array; + } + props.reconfigureTableConfirm(tableTagName?.originalTableName?.trim(), tableTagName?.tableName?.trim(), tableTagToReconfigure["type"], tableTagToReconfigure["format"], tableTagToReconfigure.visualizationHint, deletedColumns, deletedRows, tableTagToReconfigure["rows"], tableTagToReconfigure.columns); + } else { + save(cleanTableName, cleanRows, cleanColumns); + } + } + } + }>Save</PrimaryButton> + </div> + </div> + </Customizer> + ); +}; diff --git a/src/react/components/common/tagInput/tableTagLabeling.scss b/src/react/components/common/tagInput/tableTagLabeling.scss new file mode 100644 index 000000000..b5fe584ef --- /dev/null +++ b/src/react/components/common/tagInput/tableTagLabeling.scss @@ -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 { + } + } + } +} diff --git a/src/react/components/common/tagInput/tableTagLabeling.tsx b/src/react/components/common/tagInput/tableTagLabeling.tsx new file mode 100644 index 000000000..3ef521adf --- /dev/null +++ b/src/react/components/common/tagInput/tableTagLabeling.tsx @@ -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(); + } +} diff --git a/src/react/components/common/tagInput/tagInput.scss b/src/react/components/common/tagInput/tagInput.scss index cfb2e55bf..4279ed4f6 100644 --- a/src/react/components/common/tagInput/tagInput.scss +++ b/src/react/components/common/tagInput/tagInput.scss @@ -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%); + } + }; } diff --git a/src/react/components/common/tagInput/tagInput.test.tsx b/src/react/components/common/tagInput/tagInput.test.tsx index f0ee44d6c..b48288a86 100644 --- a/src/react/components/common/tagInput/tagInput.test.tsx +++ b/src/react/components/common/tagInput/tagInput.test.tsx @@ -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 }; } diff --git a/src/react/components/common/tagInput/tagInput.tsx b/src/react/components/common/tagInput/tagInput.tsx index aab010f9c..c7c69e240 100644 --- a/src/react/components/common/tagInput/tagInput.tsx +++ b/src/react/components/common/tagInput/tagInput.tsx @@ -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" /> + <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> + </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> - } - <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} + { + 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} /> - } - </Customizer> - {this.getColorPickerPortal()} + <FontIcon iconName="Tag" /> + </div> + } </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> - ); + + : + <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 { diff --git a/src/react/components/common/tagInput/tagInputItem.test.tsx b/src/react/components/common/tagInput/tagInputItem.test.tsx index 7dc776700..7c2722ad2 100644 --- a/src/react/components/common/tagInput/tagInputItem.test.tsx +++ b/src/react/components/common/tagInput/tagInputItem.test.tsx @@ -21,6 +21,8 @@ describe("Tag Input Item", () => { onRename: jest.fn(), onLabelEnter: jest.fn(), onLabelLeave: jest.fn(), + handleLabelTable: null, + addRowToDynamicTable: null, showOriginLabels:false, }; } diff --git a/src/react/components/common/tagInput/tagInputItem.tsx b/src/react/components/common/tagInput/tagInputItem.tsx index 20f05b240..1c32950b5 100644 --- a/src/react/components/common/tagInput/tagInputItem.tsx +++ b/src/react/components/common/tagInput/tagInputItem.tsx @@ -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); } diff --git a/src/react/components/common/tagInput/tagInputItemLabel.tsx b/src/react/components/common/tagInput/tagInputItemLabel.tsx index ba48406db..4140774f8 100644 --- a/src/react/components/common/tagInput/tagInputItemLabel.tsx +++ b/src/react/components/common/tagInput/tagInputItemLabel.tsx @@ -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); - } - } -} diff --git a/src/react/components/common/tagInput/tagInputToolbar.tsx b/src/react/components/common/tagInput/tagInputToolbar.tsx index c034db337..3bf30e6c4 100644 --- a/src/react/components/common/tagInput/tagInputToolbar.tsx +++ b/src/react/components/common/tagInput/tagInputToolbar.tsx @@ -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(); } diff --git a/src/react/components/pages/editorPage/canvas.scss b/src/react/components/pages/editorPage/canvas.scss index ffd4e1ba5..adc0e25eb 100644 --- a/src/react/components/pages/editorPage/canvas.scss +++ b/src/react/components/pages/editorPage/canvas.scss @@ -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; } diff --git a/src/react/components/pages/editorPage/canvas.test.tsx b/src/react/components/pages/editorPage/canvas.test.tsx index a1a85af44..02b08f9f0 100644 --- a/src/react/components/pages/editorPage/canvas.test.tsx +++ b/src/react/components/pages/editorPage/canvas.test.tsx @@ -43,6 +43,7 @@ describe("Editor Canvas", () => { lockedTags: [], hoveredLabel: null, appSettings: null, + highlightedTableCell: null, }; const assetPreviewProps: IAssetPreviewProps = { diff --git a/src/react/components/pages/editorPage/canvas.tsx b/src/react/components/pages/editorPage/canvas.tsx index afa352b85..7006a741f 100644 --- a/src/react/components/pages/editorPage/canvas.tsx +++ b/src/react/components/pages/editorPage/canvas.tsx @@ -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))) { + 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 }; + } + + 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")) { + } + 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); } diff --git a/src/react/components/pages/editorPage/canvasCommandBar.tsx b/src/react/components/pages/editorPage/canvasCommandBar.tsx index 8c83a6211..998030331 100644 --- a/src/react/components/pages/editorPage/canvasCommandBar.tsx +++ b/src/react/components/pages/editorPage/canvasCommandBar.tsx @@ -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(); }, } ], }, diff --git a/src/react/components/pages/editorPage/editorPage.test.tsx b/src/react/components/pages/editorPage/editorPage.test.tsx index e9ebbd1e2..b168f13d7 100644 --- a/src/react/components/pages/editorPage/editorPage.test.tsx +++ b/src/react/components/pages/editorPage/editorPage.test.tsx @@ -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(); diff --git a/src/react/components/pages/editorPage/editorPage.tsx b/src/react/components/pages/editorPage/editorPage.tsx index ff2c288c6..42b1547b6 100644 --- a/src/react/components/pages/editorPage/editorPage.tsx +++ b/src/react/components/pages/editorPage/editorPage.tsx @@ -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 (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, + 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 }); + 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); } } diff --git a/src/react/components/pages/prebuiltPredict/layoutPredictPage.tsx b/src/react/components/pages/prebuiltPredict/layoutPredictPage.tsx index db196adca..c1bc68f3c 100644 --- a/src/react/components/pages/prebuiltPredict/layoutPredictPage.tsx +++ b/src/react/components/pages/prebuiltPredict/layoutPredictPage.tsx @@ -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"; diff --git a/src/react/components/pages/prebuiltPredict/tableHelper.ts b/src/react/components/pages/prebuiltPredict/tableHelper.ts index cb1a11e84..6a84d9c3a 100644 --- a/src/react/components/pages/prebuiltPredict/tableHelper.ts +++ b/src/react/components/pages/prebuiltPredict/tableHelper.ts @@ -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> { diff --git a/src/react/components/pages/predict/predictPage.scss b/src/react/components/pages/predict/predictPage.scss index 742cd197a..a5bf87a75 100644 --- a/src/react/components/pages/predict/predictPage.scss +++ b/src/react/components/pages/predict/predictPage.scss @@ -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; -} +} diff --git a/src/react/components/pages/predict/predictPage.tsx b/src/react/components/pages/predict/predictPage.tsx index e6e4e54b3..c6b96d9fe 100644 --- a/src/react/components/pages/predict/predictPage.tsx +++ b/src/react/components/pages/predict/predictPage.tsx @@ -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) { diff --git a/src/react/components/pages/predict/predictResult.tsx b/src/react/components/pages/predict/predictResult.tsx index 60ff8aeb1..fd2d12d22 100644 --- a/src/react/components/pages/predict/predictResult.tsx +++ b/src/react/components/pages/predict/predictResult.tsx @@ -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) { diff --git a/src/react/components/pages/projectSettings/projectSettingsPage.tsx b/src/react/components/pages/projectSettings/projectSettingsPage.tsx index 0d751af60..d4e8337fe 100644 --- a/src/react/components/pages/projectSettings/projectSettingsPage.tsx +++ b/src/react/components/pages/projectSettings/projectSettingsPage.tsx @@ -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 }); } } diff --git a/src/react/components/pages/train/trainPage.tsx b/src/react/components/pages/train/trainPage.tsx index ed3a35964..14bfd6e84 100644 --- a/src/react/components/pages/train/trainPage.tsx +++ b/src/react/components/pages/train/trainPage.tsx @@ -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"> diff --git a/src/redux/actions/projectActions.test.ts b/src/redux/actions/projectActions.test.ts index 49c10eece..37536f9e4 100644 --- a/src/redux/actions/projectActions.test.ts +++ b/src/redux/actions/projectActions.test.ts @@ -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(); diff --git a/src/redux/actions/projectActions.ts b/src/redux/actions/projectActions.ts index 87e98b810..a2be21319 100644 --- a/src/redux/actions/projectActions.ts +++ b/src/redux/actions/projectActions.ts @@ -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 */ diff --git a/src/redux/reducers/currentProjectReducer.ts b/src/redux/reducers/currentProjectReducer.ts index 2d690fc85..ca34c2b20 100644 --- a/src/redux/reducers/currentProjectReducer.ts +++ b/src/redux/reducers/currentProjectReducer.ts @@ -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); diff --git a/src/registerIcons.ts b/src/registerIcons.ts index 48e13c703..a74b520d5 100644 --- a/src/registerIcons.ts +++ b/src/registerIcons.ts @@ -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", diff --git a/src/services/assetService.test.ts b/src/services/assetService.test.ts index 78611fc26..ec01aba5f 100644 --- a/src/services/assetService.test.ts +++ b/src/services/assetService.test.ts @@ -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); diff --git a/src/services/assetService.ts b/src/services/assetService.ts index 3b402af09..fd0aa3f8e 100644 --- a/src/services/assetService.ts +++ b/src/services/assetService.ts @@ -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,14 +565,34 @@ 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; + }); + 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; }); @@ -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 (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") || _.get(assetMetadata, "labelData.tableLabels.length") + ? AssetState.Tagged : AssetState.Visited; + return true; + } + + return false; + } - if (assetMetadata.labelData && assetMetadata.labelData.labels) { - const field = assetMetadata.labelData.labels.find((field) => field.label === tagName); + 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; - assetMetadata.labelData = labelTransformer(assetMetadata.labelData); - if(assetMetadata.labelData.labels.length===0){ - delete assetMetadata.labelData.labelingState; - delete assetMetadata.asset.labelingState; - } } } + 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") + 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; + 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; } diff --git a/src/services/projectService.ts b/src/services/projectService.ts index 63ea46303..0d2b63a98 100644 --- a/src/services/projectService.ts +++ b/src/services/projectService.ts @@ -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 { + labelName = label.label + } + tagNameSet.add(labelName); + if (tagDocumentCount[labelName]) { + tagDocumentCount[labelName] += 1; } else { - tagDocumentCount[label.label] = 1; + 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)); diff --git a/yarn.lock b/yarn.lock index 832750438..1d94cf28b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"