diff --git a/.github/workflows/build_bundle.yml b/.github/workflows/build_bundle.yml index 539aa14796..9bfe15fc26 100644 --- a/.github/workflows/build_bundle.yml +++ b/.github/workflows/build_bundle.yml @@ -30,7 +30,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ inputs.sha }} - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "${{ env.NODE }}" diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 29a46bba63..369c08aec0 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -15,7 +15,7 @@ env: jobs: run: name: "E2E Tests" - runs-on: ubuntu-latest + runs-on: ubuntu-latest-4c-16gb # ci can be skipped with `[skip ci]` prefix in message if: "!contains(github.event.head_commit.message, 'skip ci')" steps: @@ -27,7 +27,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ inputs.sha }} - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "${{ env.NODE }}" @@ -87,7 +87,7 @@ jobs: npm list --depth=1 || true - name: Run e2e test suite - timeout-minutes: 40 + timeout-minutes: 45 env: NODE_ENV: 'production' TEST_ENV: true diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 5dcb4c4212..d1a26dc8e2 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -28,7 +28,7 @@ jobs: ref: ${{ inputs.sha }} - name: "Setup NodeJS" - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: "${{ env.NODE }}" diff --git a/.github/workflows/fun_tests.yml b/.github/workflows/fun_tests.yml index dc14e3fc45..f0b05d0642 100644 --- a/.github/workflows/fun_tests.yml +++ b/.github/workflows/fun_tests.yml @@ -28,7 +28,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ inputs.sha }} - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "${{ env.NODE }}" diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 000738a6e9..4907a9ff57 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -29,14 +29,14 @@ jobs: - uses: hmarr/debug-action@v2.1.0 - name: "Validate PR's title" - uses: thehanimo/pr-title-checker@v1.4.0 + uses: thehanimo/pr-title-checker@v1.4.1 with: GITHUB_TOKEN: ${{ github.token }} pass_on_octokit_error: false configuration_path: ".github/pr-title-checker-config.json" - name: "Set PR's label based on title" - uses: release-drafter/release-drafter@v5.24.0 + uses: release-drafter/release-drafter@v5.25.0 with: disable-releaser: true config-name: autolabeler.yml diff --git a/.github/workflows/release-set-version.yml b/.github/workflows/release-set-version.yml index 1129498cc4..221b36b5cf 100644 --- a/.github/workflows/release-set-version.yml +++ b/.github/workflows/release-set-version.yml @@ -26,7 +26,7 @@ jobs: fetch-depth: 1 path: ${{ env.LSF_DIR }} - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "${{ env.NODE }}" diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index be66e35a6f..63f50b9868 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -28,7 +28,7 @@ jobs: ref: ${{ inputs.sha }} - name: "Setup NodeJS" - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: "${{ env.NODE }}" diff --git a/e2e/fragments/AtImageView.js b/e2e/fragments/AtImageView.js index 7d47ddfc37..ad2ccf152e 100644 --- a/e2e/fragments/AtImageView.js +++ b/e2e/fragments/AtImageView.js @@ -5,6 +5,7 @@ const Helpers = require('../tests/helpers'); module.exports = { _stageSelector: '.konvajs-content', + _stageFrameSelector: '[class^="frame--"]', _stageBBox: null, _toolBarSelector: '.lsf-toolbar', @@ -41,7 +42,7 @@ module.exports = { }, async grabStageBBox() { - const bbox = await I.grabElementBoundingRect(this._stageSelector); + const bbox = await I.grabElementBoundingRect(this._stageFrameSelector); return bbox; }, diff --git a/e2e/package.json b/e2e/package.json index fcf867a3d3..194cc3f66e 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -45,5 +45,9 @@ "v8-to-istanbul": "^9.0.0", "xml2js": "^0.4.23", "xmlbuilder": "^15.1.1" + }, + "resolutions": { + "debug": "^4.3.1", + "electron": "^22.3.25" } } diff --git a/e2e/setup/feature-flags.js b/e2e/setup/feature-flags.js index ba5a3c4d02..03b810dd9d 100644 --- a/e2e/setup/feature-flags.js +++ b/e2e/setup/feature-flags.js @@ -12,4 +12,5 @@ module.exports = { fflag_fix_front_lsdv_4620_memory_leaks_100723_short: true, fflag_feat_front_lsdv_4620_outliner_optimization_310723_short: true, fflag_fix_front_lsdv_5248_double_click_delay_280823_short: true, + fflag_fix_front_leap_32_zoom_perf_190923_short: true, }; diff --git a/e2e/tests/helpers.js b/e2e/tests/helpers.js index cc3f94ee22..4ab0c40e13 100644 --- a/e2e/tests/helpers.js +++ b/e2e/tests/helpers.js @@ -532,11 +532,11 @@ async function generateImageUrl({ width, height }) { } const getCanvasSize = () => { - const stage = window.Konva.stages[0]; + const imageObject = window.Htx.annotationStore.selected.objects.find(o => o.type === 'image'); return { - width: stage.width(), - height: stage.height(), + width: imageObject.canvasSize.width, + height: imageObject.canvasSize.height, }; }; const getImageSize = () => { diff --git a/e2e/tests/nested-choices.test.js b/e2e/tests/nested-choices.test.js index c894a00714..4bc27779c7 100644 --- a/e2e/tests/nested-choices.test.js +++ b/e2e/tests/nested-choices.test.js @@ -30,12 +30,6 @@ Scenario('Switching states at nested choices', async ({ I, LabelStudio })=>{ I.amOnPage('/'); - LabelStudio.setFeatureFlags({ - ff_dev_2007_rework_choices_280322_short: true, - ff_dev_2100_preselected_choices_250422_short: true, - ff_front_dev_2244_nested_choices_des_107_160522_short: true, - }); - LabelStudio.init(params); { @@ -100,12 +94,6 @@ Scenario('Nested choices states from the annotation', async ({ I, LabelStudio }) I.amOnPage('/'); - LabelStudio.setFeatureFlags({ - ff_dev_2007_rework_choices_280322_short: true, - ff_dev_2100_preselected_choices_250422_short: true, - ff_front_dev_2244_nested_choices_des_107_160522_short: true, - }); - // Load annotation with each type of selection for branches (fully checked, fully unchecked, partly checked) LabelStudio.init({ ...params, diff --git a/e2e/tests/regression-tests/dynamic-choices.test.js b/e2e/tests/regression-tests/dynamic-choices.test.js index 20bc9f46d6..53e3e9ad88 100644 --- a/e2e/tests/regression-tests/dynamic-choices.test.js +++ b/e2e/tests/regression-tests/dynamic-choices.test.js @@ -3,13 +3,6 @@ const assert = require('assert'); Feature('Dynamic choices').tag('@regress'); Scenario('Hotkeys for dynamic choices', async ({ I, LabelStudio })=>{ - LabelStudio.setFeatureFlags({ - ff_dev_2007_rework_choices_280322_short: true, - ff_dev_2007_dev_2008_dynamic_tag_children_250322_short: true, - ff_dev_2100_preselected_choices_250422_short: true, - ff_front_dev_2244_nested_choices_des_107_160522_short: true, - }); - const params = { config: ` diff --git a/e2e/tests/regression-tests/preselected-choices.test.js b/e2e/tests/regression-tests/preselected-choices.test.js index 6d9a1f5556..7a2cfd1987 100644 --- a/e2e/tests/regression-tests/preselected-choices.test.js +++ b/e2e/tests/regression-tests/preselected-choices.test.js @@ -33,9 +33,6 @@ Scenario('Make a duplicate of annotation with preselected choices', async ({ I, }; I.amOnPage('/'); - LabelStudio.setFeatureFlags({ - ff_dev_2100_preselected_choices_250422_short: true, - }); LabelStudio.init(params); // Try to create copy of current annotation AtTopbar.click('[aria-label="Copy Annotation"]'); @@ -75,9 +72,6 @@ Scenario('Make a duplicate of empty annotation with preselected choices', async }; I.amOnPage('/'); - LabelStudio.setFeatureFlags({ - ff_dev_2100_preselected_choices_250422_short: true, - }); LabelStudio.init(params); // Try to create copy of current annotation AtTopbar.click('[aria-label="Copy Annotation"]'); diff --git a/e2e/tests/taxonomy.test.js b/e2e/tests/taxonomy.test.js index e363be817f..2fd8b96297 100644 --- a/e2e/tests/taxonomy.test.js +++ b/e2e/tests/taxonomy.test.js @@ -6,7 +6,6 @@ Feature('Taxonomy'); Before(({ LabelStudio }) => { LabelStudio.setFeatureFlags({ fflag_feat_front_lsdv_5451_async_taxonomy_110823_short: false, - ff_dev_2007_dev_2008_dynamic_tag_children_250322_short: true, fflag_fix_front_dev_3617_taxonomy_memory_leaks_fix: true, ff_front_dev_1536_taxonomy_user_labels_150222_long: true, ff_front_1170_outliner_030222_short: true, diff --git a/e2e/yarn.lock b/e2e/yarn.lock index ed09df14f3..784341a7d6 100644 --- a/e2e/yarn.lock +++ b/e2e/yarn.lock @@ -249,21 +249,20 @@ dependencies: "@cspotcode/source-map-consumer" "0.8.0" -"@electron/get@^1.0.1": - version "1.13.1" - resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.13.1.tgz#42a0aa62fd1189638bd966e23effaebb16108368" - integrity sha512-U5vkXDZ9DwXtkPqlB45tfYnnYBN8PePp1z/XDCupnSpdrxT8/ThCv9WCwPLf9oqiSGZTkH6dx2jDUPuoXpjkcA== +"@electron/get@^2.0.0": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@electron/get/-/get-2.0.3.tgz#fba552683d387aebd9f3fcadbcafc8e12ee4f960" + integrity sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ== dependencies: debug "^4.1.1" env-paths "^2.2.0" fs-extra "^8.1.0" - got "^9.6.0" + got "^11.8.5" progress "^2.0.3" semver "^6.2.0" sumchecker "^3.0.1" optionalDependencies: global-agent "^3.0.0" - global-tunnel-ng "^2.7.1" "@eslint/eslintrc@^1.0.5": version "1.0.5" @@ -400,17 +399,17 @@ resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== -"@sindresorhus/is@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" - integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== +"@sindresorhus/is@^4.0.0": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" + integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== -"@szmarczak/http-timer@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" - integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== +"@szmarczak/http-timer@^4.0.5": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" + integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w== dependencies: - defer-to-connect "^1.0.1" + defer-to-connect "^2.0.0" "@tsconfig/node10@^1.0.7": version "1.0.8" @@ -432,6 +431,16 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== +"@types/cacheable-request@^6.0.1": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz#a430b3260466ca7b5ca5bfd735693b36e7a9d183" + integrity sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "^3.1.4" + "@types/node" "*" + "@types/responselike" "^1.0.0" + "@types/glob@*": version "7.2.0" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" @@ -440,6 +449,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/http-cache-semantics@*": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz#a3ff232bf7d5c55f38e4e45693eda2ebb545794d" + integrity sha512-V46MYLFp08Wf2mmaBhvgjStM3tPa+2GAdy/iqoX+noX1//zje2x4XmrIU0cAwyClATsTmahbtoQ2EwP7I5WSiA== + "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -450,6 +464,13 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== +"@types/keyv@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6" + integrity sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg== + dependencies: + "@types/node" "*" + "@types/minimatch@*": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" @@ -467,10 +488,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.12.tgz#ac7fb693ac587ee182c3780c26eb65546a1a3c10" integrity sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw== -"@types/node@^14.6.2": - version "14.18.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.0.tgz#98df2397f6936bfbff4f089e40e06fa5dd88d32a" - integrity sha512-0GeIl2kmVMXEnx8tg1SlG6Gg8vkqirrW752KqolYo1PHevhhZN3bhJ67qHj+bQaINhX0Ra3TlWwRvMCd9iEfNQ== +"@types/node@^16.11.26": + version "16.18.59" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.59.tgz#4cdbd631be6d9be266a96fb17b5d0d7ad6bbe26c" + integrity sha512-PJ1w2cNeKUEdey4LiPra0ZuxZFOGvetswE8qHRriV/sUkL5Al4tTmPV9D2+Y/TPIxTHHgxTfRjZVKWhPw/ORhQ== "@types/puppeteer@^5.4.3": version "5.4.4" @@ -479,6 +500,13 @@ dependencies: "@types/node" "*" +"@types/responselike@^1.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.2.tgz#8de1b0477fd7c12df77e50832fa51701a8414bd6" + integrity sha512-/4YQT5Kp6HxUDb4yhRkm0bJ7TbjvTddqX7PZ5hz6qV3pxSo72f/6YPRo+Mu2DU307tm9IioO69l7uAwn5XNcFA== + dependencies: + "@types/node" "*" + "@types/rimraf@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.2.tgz#a63d175b331748e5220ad48c901d7bbf1f44eef8" @@ -874,11 +902,6 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - buffer@^5.2.1, buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -892,18 +915,23 @@ bytes@3.1.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== -cacheable-request@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" - integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== +cacheable-lookup@^5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" + integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== + +cacheable-request@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.4.tgz#7a33ebf08613178b403635be7b899d3e69bbe817" + integrity sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg== dependencies: clone-response "^1.0.2" get-stream "^5.1.0" http-cache-semantics "^4.0.0" - keyv "^3.0.0" + keyv "^4.0.0" lowercase-keys "^2.0.0" - normalize-url "^4.1.0" - responselike "^1.0.2" + normalize-url "^6.0.1" + responselike "^2.0.0" caching-transform@^4.0.0: version "4.0.0" @@ -1203,17 +1231,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -concat-stream@^1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -config-chain@^1.1.11, config-chain@^1.1.13: +config-chain@^1.1.13: version "1.1.13" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== @@ -1260,11 +1278,6 @@ core-js@2.6.12: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -1315,52 +1328,24 @@ cucumber-expressions@^6.6.2: dependencies: becke-ch--regex--s0-0-v1--base--pl--lib "^1.2.0" -debug@2.6.9, debug@^2.2.0, debug@^2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: +debug@2.6.9, debug@4, debug@4.1.1, debug@4.3.1, debug@^2.2.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@~3.1.0, debug@~4.1.0: version "4.3.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== dependencies: ms "2.1.2" -debug@4.1.1, debug@~4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== - dependencies: - ms "^2.1.1" - -debug@4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== - dependencies: - ms "2.1.2" - -debug@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== -decompress-response@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" - integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== dependencies: - mimic-response "^1.0.0" + mimic-response "^3.1.0" deep-eql@^3.0.1: version "3.0.1" @@ -1391,10 +1376,10 @@ default-require-extensions@^3.0.0: dependencies: strip-bom "^4.0.0" -defer-to-connect@^1.0.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" - integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== +defer-to-connect@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" + integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== define-properties@^1.1.2, define-properties@^1.1.3, define-properties@^1.1.4: version "1.1.4" @@ -1473,11 +1458,6 @@ domutils@^2.5.2, domutils@^2.6.0, domutils@^2.7.0: domelementtype "^2.2.0" domhandler "^4.2.0" -duplexer3@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" - integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= - editorconfig@^0.15.3: version "0.15.3" resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5" @@ -1498,14 +1478,14 @@ electron-to-chromium@^1.4.147: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.148.tgz#437430e03c58ccd1d05701f66980afe54d2253ec" integrity sha512-8MJk1bcQUAYkuvCyWZxaldiwoDG0E0AMzBGA6cv3WfuvJySiPgfidEPBFCRRH3cZm6SVZwo/oRlK1ehi1QNEIQ== -electron@^12.0.4: - version "12.2.3" - resolved "https://registry.yarnpkg.com/electron/-/electron-12.2.3.tgz#d426a7861e3c722f92c32153f11f7bbedf65b000" - integrity sha512-B27c7eqx1bC5kea6An8oVhk1pShNC4VGqWarHMhD47MDtmg54KepHO5AbAvmKKZK/jWN7NTC7wyCYTDElJNtQA== +electron@^12.0.4, electron@^22.3.25: + version "22.3.27" + resolved "https://registry.yarnpkg.com/electron/-/electron-22.3.27.tgz#b77451a53f0c502e7559cceac28ac58eb289eef8" + integrity sha512-7Rht21vHqj4ZFRnKuZdFqZFsvMBCmDqmjetiMqPtF+TmTBiGne1mnstVXOA/SRGhN2Qy5gY5bznJKpiqogjM8A== dependencies: - "@electron/get" "^1.0.1" - "@types/node" "^14.6.2" - extract-zip "^1.0.3" + "@electron/get" "^2.0.0" + "@types/node" "^16.11.26" + extract-zip "^2.0.1" emoji-regex@^7.0.1: version "7.0.3" @@ -1517,7 +1497,7 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -encodeurl@^1.0.2, encodeurl@~1.0.2: +encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= @@ -1871,16 +1851,6 @@ extract-zip@2.0.1, extract-zip@^2.0.1: optionalDependencies: "@types/yauzl" "^2.9.1" -extract-zip@^1.0.3: - version "1.7.0" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" - integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== - dependencies: - concat-stream "^1.6.2" - debug "^2.6.9" - mkdirp "^0.5.4" - yauzl "^2.10.0" - fast-deep-equal@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" @@ -2131,9 +2101,9 @@ get-caller-file@^2.0.1: integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-func-name@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" - integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig== + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.1" @@ -2149,13 +2119,6 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -2237,16 +2200,6 @@ global-agent@^3.0.0: semver "^7.3.2" serialize-error "^7.0.1" -global-tunnel-ng@^2.7.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz#d03b5102dfde3a69914f5ee7d86761ca35d57d8f" - integrity sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg== - dependencies: - encodeurl "^1.0.2" - lodash "^4.17.10" - npm-conf "^1.1.3" - tunnel "^0.0.6" - globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -2278,22 +2231,22 @@ globby@^11.0.4: merge2 "^1.3.0" slash "^3.0.0" -got@^9.6.0: - version "9.6.0" - resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" - integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== - dependencies: - "@sindresorhus/is" "^0.14.0" - "@szmarczak/http-timer" "^1.1.2" - cacheable-request "^6.0.0" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^4.1.0" - lowercase-keys "^1.0.1" - mimic-response "^1.0.1" - p-cancelable "^1.0.0" - to-readable-stream "^1.0.0" - url-parse-lax "^3.0.0" +got@^11.8.5: + version "11.8.6" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" + integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g== + dependencies: + "@sindresorhus/is" "^4.0.0" + "@szmarczak/http-timer" "^4.0.5" + "@types/cacheable-request" "^6.0.1" + "@types/responselike" "^1.0.0" + cacheable-lookup "^5.0.3" + cacheable-request "^7.0.2" + decompress-response "^6.0.0" + http2-wrapper "^1.0.0-beta.5.2" + lowercase-keys "^2.0.0" + p-cancelable "^2.0.0" + responselike "^2.0.0" graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.10" @@ -2418,6 +2371,14 @@ http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" +http2-wrapper@^1.0.0-beta.5.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" + integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.0.0" + https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" @@ -2479,7 +2440,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2699,11 +2660,6 @@ isarray@^2.0.5: resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -2848,10 +2804,10 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -json-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" - integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== json-schema-traverse@^0.4.1: version "0.4.1" @@ -2880,12 +2836,12 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" -keyv@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" - integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== +keyv@^4.0.0: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== dependencies: - json-buffer "3.0.0" + json-buffer "3.0.1" levn@^0.4.1: version "0.4.1" @@ -2963,11 +2919,6 @@ loupe@^2.3.1: dependencies: get-func-name "^2.0.0" -lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== - lowercase-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" @@ -3071,11 +3022,16 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== -mimic-response@^1.0.0, mimic-response@^1.0.1: +mimic-response@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + "minimatch@2 || 3", minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -3095,7 +3051,7 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== -mkdirp@^0.5.1, mkdirp@^0.5.4: +mkdirp@^0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -3156,11 +3112,6 @@ mocha@8.1.3: yargs-parser "13.1.2" yargs-unparser "1.6.1" -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - ms@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" @@ -3171,7 +3122,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1, ms@^2.1.3: +ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -3230,18 +3181,10 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -normalize-url@^4.1.0: - version "4.5.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" - integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== - -npm-conf@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/npm-conf/-/npm-conf-1.1.3.tgz#256cc47bd0e218c259c4e9550bf413bc2192aff9" - integrity sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw== - dependencies: - config-chain "^1.1.11" - pify "^3.0.0" +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== nth-check@^2.0.0: version "2.0.1" @@ -3356,10 +3299,10 @@ os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= -p-cancelable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" - integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== +p-cancelable@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" + integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" @@ -3520,11 +3463,6 @@ picomatch@^2.2.3: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - pkg-dir@4.2.0, pkg-dir@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -3571,16 +3509,6 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prepend-http@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" - integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - process-on-spawn@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/process-on-spawn/-/process-on-spawn-1.0.0.tgz#95b05a23073d30a17acfdc92a440efd2baefdc93" @@ -3690,6 +3618,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -3722,19 +3655,6 @@ rc@~1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -readable-stream@^2.2.2: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - readable-stream@^3.1.1, readable-stream@^3.4.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" @@ -3803,6 +3723,11 @@ requireindex@~1.1.0: resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.1.0.tgz#e5404b81557ef75db6e49c5a72004893fe03e162" integrity sha1-5UBLgVV+91225JxacgBIk/4D4WI= +resolve-alpn@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" + integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -3820,12 +3745,12 @@ resolve@~1.7.1: dependencies: path-parse "^1.0.5" -responselike@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" - integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= +responselike@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.1.tgz#9a0bc8fdc252f3fb1cca68b016591059ba1422bc" + integrity sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw== dependencies: - lowercase-keys "^1.0.0" + lowercase-keys "^2.0.0" resq@^1.10.2: version "1.10.2" @@ -3895,7 +3820,7 @@ rxjs@^6.4.0: dependencies: tslib "^1.9.0" -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.2, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== @@ -4190,13 +4115,6 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - strip-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" @@ -4328,11 +4246,6 @@ to-fast-properties@^2.0.0: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= -to-readable-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" - integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -4380,11 +4293,6 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" -tunnel@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" - integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -4427,11 +4335,6 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - typescript@^4.5.3: version "4.5.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.3.tgz#afaa858e68c7103317d89eb90c5d8906268d353c" @@ -4472,14 +4375,7 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -url-parse-lax@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" - integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= - dependencies: - prepend-http "^2.0.0" - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= diff --git a/examples/classification_mixed/config.xml b/examples/classification_mixed/config.xml index 4124457a0e..bf2ef97b81 100644 --- a/examples/classification_mixed/config.xml +++ b/examples/classification_mixed/config.xml @@ -19,7 +19,7 @@ - + diff --git a/package.json b/package.json index 57b54633a2..340729097a 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "text-annotation" ], "dependencies": { + "@babel/plugin-proposal-private-methods": "^7.18.6", "@martel/audio-file-decoder": "2.3.15", "@thi.ng/rle-pack": "^2.1.6", "@types/react-beautiful-dnd": "^13.1.3", @@ -102,15 +103,15 @@ "react-window": "^1.8.6" }, "devDependencies": { - "@babel/core": "7.18.6", + "@babel/core": "7.23.2", "@babel/plugin-proposal-class-properties": "7.18.6", "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6", "@babel/plugin-proposal-optional-chaining": "7.18.6", "@babel/plugin-transform-modules-commonjs": "7.18.6", "@babel/plugin-transform-react-jsx": "7.18.6", - "@babel/plugin-transform-runtime": "7.18.6", + "@babel/plugin-transform-runtime": "7.23.2", "@babel/plugin-transform-typescript": "7.18.6", - "@babel/preset-env": "7.18.6", + "@babel/preset-env": "7.23.2", "@babel/preset-react": "7.18.6", "@babel/preset-typescript": "7.18.6", "@babel/runtime": "7.18.6", @@ -170,9 +171,9 @@ "nanoid": "^3.3.0", "node-fetch": "^2.6.1", "pleasejs": "^0.4.2", - "postcss": "^8.4.6", - "postcss-loader": "^6.2.1", - "postcss-preset-env": "^7.4.1", + "postcss": "^8.4.31", + "postcss-loader": "^7.3.3", + "postcss-preset-env": "^9.2.0", "prettier": "^1.19.1", "raw-loader": "^4.0.2", "react": "^17.0.2", @@ -205,7 +206,9 @@ }, "resolutions": { "d3-color": "3.1.0", - "loader-utils": "2.0.4" + "loader-utils": "2.0.4", + "@babel/traverse": "7.23.2", + "postcss": "8.4.31" }, "nohoist": [ "**/babel-preset-react-app/@babel/runtime" diff --git a/src/components/BottomBar/CurrentTask.js b/src/components/BottomBar/CurrentTask.js index b62b386431..328be3baa6 100644 --- a/src/components/BottomBar/CurrentTask.js +++ b/src/components/BottomBar/CurrentTask.js @@ -1,9 +1,10 @@ +import React, { useMemo } from 'react'; import { observer } from 'mobx-react'; -import { useMemo } from 'react'; import { Button } from '../../common/Button/Button'; import { Block, Elem } from '../../utils/bem'; import { guidGenerator } from '../../utils/unique'; import { isDefined } from '../../utils/utilities'; +import { FF_TASK_COUNT_FIX, isFF } from '../../common/Tooltip/Tooltip'; import './CurrentTask.styl'; @@ -18,16 +19,22 @@ export const CurrentTask = observer(({ store }) => { && !store.canGoNextTask && !store.hasInterface('review') && store.hasInterface('postpone'); - + return ( {store.task.id ?? guidGenerator()} {historyEnabled && ( - - {currentIndex} of {store.taskHistory.length} - + isFF(FF_TASK_COUNT_FIX) ? ( + + {store.queuePosition} of {store.queueTotal} + + ) : ( + + {currentIndex} of {store.taskHistory.length} + + ) )} {historyEnabled && ( diff --git a/src/components/CurrentEntity/AnnotationHistory.tsx b/src/components/CurrentEntity/AnnotationHistory.tsx index 1294a13cbc..2e95dfda9e 100644 --- a/src/components/CurrentEntity/AnnotationHistory.tsx +++ b/src/components/CurrentEntity/AnnotationHistory.tsx @@ -106,9 +106,10 @@ const AnnotationHistoryComponent: FC = ({ const annotation = annotationStore.selected; const lastItem = history?.length ? history[0] : null; const hasChanges = annotation.history.hasChanges; + // if user makes changes at the first time there are no draft yet const isDraftSelected = !annotationStore.selectedHistory && (annotation.draftSelected || (!annotation.versions.draft && hasChanges)); - + return ( {showDraft && ( @@ -121,6 +122,7 @@ const AnnotationHistoryComponent: FC = ({ const isSelected = isLastItem && !selectedHistory && showDraft ? !isDraftSelected : selectedHistory?.id === item.id; + return ( = ({ annotationStore.selectHistory(isSelected ? null : item); return; } - - if (hasChanges) { annotation.saveDraftImmediately(); // wait for draft to be saved before switching to history await when(() => !annotation.isDraftSaving); } - if (isLastItem || isSelected) { // last history state and draft are actual annotation, not from history // and if user clicks on already selected item we should switch to last state diff --git a/src/components/ErrorMessage/ErrorMessage.module.scss b/src/components/ErrorMessage/ErrorMessage.module.scss index ecd3c6f1df..6297e3e5a7 100644 --- a/src/components/ErrorMessage/ErrorMessage.module.scss +++ b/src/components/ErrorMessage/ErrorMessage.module.scss @@ -6,6 +6,7 @@ color: rgb(119, 27, 4); border: 1px solid rgb(230, 138, 110); background-color: rgb(255, 193, 174); + white-space: normal; & + & { margin: 0 0 16px; // in case we are in flex container, where margins don't collapse diff --git a/src/components/ImageTransformer/ImageTransformer.js b/src/components/ImageTransformer/ImageTransformer.js index 3cfab170e2..3c7cb5db32 100644 --- a/src/components/ImageTransformer/ImageTransformer.js +++ b/src/components/ImageTransformer/ImageTransformer.js @@ -3,7 +3,7 @@ import { MIN_SIZE } from '../../tools/Base'; import { getBoundingBoxAfterChanges } from '../../utils/image'; import LSTransformer from './LSTransformer'; import LSTransformerOld from './LSTransformerOld'; -import { FF_DEV_2671, isFF } from '../../utils/feature-flags'; +import { FF_DEV_2671, FF_ZOOM_OPTIM, isFF } from '../../utils/feature-flags'; const EPSILON = 0.001; @@ -91,14 +91,14 @@ export default class TransformerComponent extends Component { const [realX, realY] = [box.x - stage.x, box.y - stage.y]; if (realX < 0) { - x = 0; + x = isFF(FF_ZOOM_OPTIM) ? stage.x : 0; width += realX; } else if (realX + box.width > stage.width) { width = stage.width - realX; } if (realY < 0) { - y = 0; + y = isFF(FF_ZOOM_OPTIM) ? stage.y : 0; height += realY; } else if (realY + box.height > stage.height) { height = stage.height - realY; @@ -111,7 +111,11 @@ export default class TransformerComponent extends Component { const stage = this.transformer.getStage(); const { stageWidth, stageHeight } = this.props.item; - const [scaledStageWidth, scaledStageHeight] = [stageWidth * stage.scaleX(), stageHeight * stage.scaleY()]; + let [scaledStageWidth, scaledStageHeight] = [stageWidth * stage.scaleX(), stageHeight * stage.scaleY()]; + + if (isFF(FF_ZOOM_OPTIM) && this.props.item.isSideways) { + [scaledStageWidth, scaledStageHeight] = [scaledStageHeight, scaledStageWidth]; + } const [stageX, stageY] = [stage.x(), stage.y()]; return { diff --git a/src/components/ImageView/ImageView.js b/src/components/ImageView/ImageView.js index d54804e289..cc352449dc 100644 --- a/src/components/ImageView/ImageView.js +++ b/src/components/ImageView/ImageView.js @@ -1,4 +1,13 @@ -import React, { Component, createRef, forwardRef, Fragment, memo, useEffect, useRef, useState } from 'react'; +import React, { + Component, + createRef, + forwardRef, + Fragment, + memo, + useEffect, + useRef, + useState +} from 'react'; import { Group, Layer, Line, Rect, Stage } from 'react-konva'; import { observer } from 'mobx-react'; import { getEnv, getRoot, isAlive } from 'mobx-state-tree'; @@ -29,7 +38,7 @@ import { FF_DEV_4081, FF_LSDV_4583_6, FF_LSDV_4711, - FF_LSDV_4930, + FF_LSDV_4930, FF_ZOOM_OPTIM, isFF } from '../../utils/feature-flags'; import { Pagination } from '../../common/Pagination/Pagination'; @@ -389,7 +398,9 @@ const SelectionLayer = observer(({ item, selectionArea }) => { * but now they are rerendered just by mistake because of unmemoized `splitRegions` in main render. * This is temporary solution to pass in relevant props changed on window resize. */ -const Selection = observer(({ item, selectionArea, ...triggeredOnResize }) => { +const Selection = observer(({ item, ...triggeredOnResize }) => { + const { selectionArea } = item; + return ( <> { isFF(FF_DBLCLICK_DELAY) @@ -896,8 +907,6 @@ export default observer( // TODO fix me if (!store.task || !item.currentSrc) return null; - const regions = item.regs; - const containerStyle = {}; const containerClassName = styles.container; @@ -911,7 +920,7 @@ export default observer( containerStyle['height'] = item.height; } - if (!this.props.store.settings.enableSmoothing && item.zoomScale > 1) { + if (!store.settings.enableSmoothing && item.zoomScale > 1) { containerStyle['imageRendering'] = 'pixelated'; } @@ -928,23 +937,6 @@ export default observer( if (paginationEnabled) wrapperClasses.push(styles.withPagination); - const { - brushRegions, - shapeRegions, - } = splitRegions(regions); - - const { - brushRegions: suggestedBrushRegions, - shapeRegions: suggestedShapeRegions, - } = splitRegions(item.suggestions); - - const renderableRegions = Object.entries({ - brush: brushRegions, - shape: shapeRegions, - suggestedBrush: suggestedBrushRegions, - suggestedShape: suggestedShapeRegions, - }); - const [toolsReady, stageLoading] = isFF(FF_LSDV_4583_6) ? [true, false] : [ item.hasTools, item.stageWidth <= 1, ]; @@ -961,7 +953,7 @@ export default observer( {paginationEnabled ? (
) : (imageIsLoaded) ? ( - { - item.setStageRef(ref); - }} - className={[styles['image-element'], - ...imagePositionClassnames, - ].join(' ')} - width={item.canvasSize.width} - height={item.canvasSize.height} - scaleX={item.zoomScale} - scaleY={item.zoomScale} - x={item.zoomingPositionX} - y={item.zoomingPositionY} - offsetX={item.stageTranslate.x} - offsetY={item.stageTranslate.y} - rotation={item.rotation} + { if (this.crosshairRef.current) { this.crosshairRef.current.updateVisibility(true); @@ -1093,54 +1073,7 @@ export default observer( onMouseUp={this.handleMouseUp} onWheel={item.zoom ? this.handleZoom : () => { }} - > - {/* Hack to keep stage in place when there's no regions */} - {regions.length === 0 && ( - - - - )} - {item.grid && item.sizeUpdated && } - - { - isFF(FF_LSDV_4930) - ? - : null - } - - {renderableRegions.map(([groupName, list]) => { - const isBrush = groupName.match(/brush/i) !== null; - const isSuggestion = groupName.match('suggested') !== null; - - return list.length > 0 ? ( - - ) : ; - })} - - - - {item.crosshair && ( - - )} - + /> ) : null} @@ -1165,3 +1098,152 @@ export default observer( } }, ); + +const EntireStage = observer(({ + item, + imagePositionClassnames, + state, + onClick, + onMouseEnter, + onMouseLeave, + onDragMove, + onMouseDown, + onMouseMove, + onMouseUp, + onWheel, +}) => { + const { store } = item; + let size, position; + + if (isFF(FF_ZOOM_OPTIM)) { + size = { + width: item.containerWidth, + height: item.containerHeight, + }; + position = { + x: item.zoomingPositionX + item.alignmentOffset.x, + y: item.zoomingPositionY + item.alignmentOffset.y, + }; + } else { + size = { ...item.canvasSize }; + position = { + x: item.zoomingPositionX, + y: item.zoomingPositionY, + }; + } + + return ( + { + item.setStageRef(ref); + }} + className={[styles['image-element'], + ...imagePositionClassnames, + ].join(' ')} + width={size.width} + height={size.height} + scaleX={item.zoomScale} + scaleY={item.zoomScale} + x={position.x} + y={position.y} + offsetX={item.stageTranslate.x} + offsetY={item.stageTranslate.y} + rotation={item.rotation} + onClick={onClick} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + onDragMove={onDragMove} + onMouseDown={onMouseDown} + onMouseMove={onMouseMove} + onMouseUp={onMouseUp} + onWheel={onWheel} + > + + + ); +}); + +const StageContent = observer(({ + item, + store, + state, +}) => { + if (!isAlive(item)) return null; + if (!store.task || !item.currentSrc) return null; + + const regions = item.regs; + const paginationEnabled = !!item.isMultiItem; + const wrapperClasses = [ + styles.wrapperComponent, + item.images.length > 1 ? styles.withGallery : styles.wrapper, + ]; + + if (paginationEnabled) wrapperClasses.push(styles.withPagination); + + const { + brushRegions, + shapeRegions, + } = splitRegions(regions); + + const { + brushRegions: suggestedBrushRegions, + shapeRegions: suggestedShapeRegions, + } = splitRegions(item.suggestions); + + const renderableRegions = Object.entries({ + brush: brushRegions, + shape: shapeRegions, + suggestedBrush: suggestedBrushRegions, + suggestedShape: suggestedShapeRegions, + }); + + return ( + <> + {/* Hack to keep stage in place when there's no regions */} + {regions.length === 0 && ( + + + + )} + {item.grid && item.sizeUpdated && } + + { + isFF(FF_LSDV_4930) + ? + : null + } + + {renderableRegions.map(([groupName, list]) => { + const isBrush = groupName.match(/brush/i) !== null; + const isSuggestion = groupName.match('suggested') !== null; + + return list.length > 0 ? ( + + ) : ; + })} + + + + {item.crosshair && ( + + )} + + ); +}); diff --git a/src/components/NewTaxonomy/NewTaxonomy.styl b/src/components/NewTaxonomy/NewTaxonomy.styl new file mode 100644 index 0000000000..f034aa4b34 --- /dev/null +++ b/src/components/NewTaxonomy/NewTaxonomy.styl @@ -0,0 +1,3 @@ +:global(.htx-taxonomy-item-color) + padding 4px 4px + border-radius 2px diff --git a/src/components/NewTaxonomy/NewTaxonomy.tsx b/src/components/NewTaxonomy/NewTaxonomy.tsx index 9c7ecc73c5..0f8d1cb4c4 100644 --- a/src/components/NewTaxonomy/NewTaxonomy.tsx +++ b/src/components/NewTaxonomy/NewTaxonomy.tsx @@ -1,8 +1,11 @@ import { TreeSelect } from 'antd'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react'; import { Tooltip } from '../../common/Tooltip/Tooltip'; +import './NewTaxonomy.styl'; +import { TaxonomySearch, TaxonomySearchRef } from './TaxonomySearch'; + type TaxonomyPath = string[]; type onAddLabelCallback = (path: string[]) => any; type onDeleteLabelCallback = (path: string[]) => any; @@ -15,14 +18,16 @@ type TaxonomyItem = { children?: TaxonomyItem[], origin?: 'config' | 'user' | 'session', hint?: string, + color?: string, }; -type AntTaxonomyItem = { +export type AntTaxonomyItem = { title: string | JSX.Element, value: string, key: string, isLeaf?: boolean, children?: AntTaxonomyItem[], + disableCheckbox?: boolean, }; type TaxonomyOptions = { @@ -36,30 +41,67 @@ type TaxonomyOptions = { placeholder?: string, }; +type SelectedItem = { + label: string, + value: string, +}[]; + type TaxonomyProps = { items: TaxonomyItem[], - selected: TaxonomyPath[], + selected: SelectedItem[], onChange: (node: any, selected: TaxonomyPath[]) => any, onLoadData?: (item: TaxonomyPath) => any, onAddLabel?: onAddLabelCallback, onDeleteLabel?: onDeleteLabelCallback, options: TaxonomyOptions, isEditable?: boolean, + defaultSearch?: boolean, +}; + +type TaxonomyExtendedOptions = TaxonomyOptions & { + maxUsagesReached?: boolean, }; -const convert = (items: TaxonomyItem[], options: TaxonomyOptions): AntTaxonomyItem[] => { - return items.map(item => ({ - title: item.hint ? ( +const convert = ( + items: TaxonomyItem[], + options: TaxonomyExtendedOptions, + selectedPaths: string[], +): AntTaxonomyItem[] => { + // generate string or component to be the `title` of the item + const enrich = (item: TaxonomyItem) => { + const color = (item: TaxonomyItem) => ( + // no BEM here to make it more lightweight + // global classname to allow to change it in Style tag + + {item.label} + + ); + + if (!item.hint) return item.color ? color(item) : item.label; + + return ( - {item.label} + {item.color ? color(item) : {item.label}} - ) : item.label, - value: item.path.join(options.pathSeparator), - key: item.path.join(options.pathSeparator), - isLeaf: item.isLeaf !== false && !item.children, - disableCheckbox: options.leafsOnly && (item.isLeaf === false || !!item.children), - children: item.children ? convert(item.children, options) : undefined, - })); + ); + }; + + const convertItem = (item: TaxonomyItem): AntTaxonomyItem => { + const value = item.path.join(options.pathSeparator); + const disabledNode = options.leafsOnly && (item.isLeaf === false || !!item.children); + const maxUsagesReached = options.maxUsagesReached && !selectedPaths.includes(value); + + return { + title: enrich(item), + value, + key: value, + isLeaf: item.isLeaf !== false && !item.children, + disableCheckbox: disabledNode || maxUsagesReached, + children: item.children?.map(convertItem), + }; + }; + + return items.map(convertItem); }; const NewTaxonomy = ({ @@ -67,6 +109,7 @@ const NewTaxonomy = ({ selected, onChange, onLoadData, + defaultSearch = true, // @todo implement user labels // onAddLabel, // onDeleteLabel, @@ -74,26 +117,78 @@ const NewTaxonomy = ({ // @todo implement readonly mode // isEditable = true, }: TaxonomyProps) => { + const refInput = useRef(null); const [treeData, setTreeData] = useState([]); + const [filteredTreeData, setFilteredTreeData] = useState([]); + const [expandedKeys, setExpandedKeys] = useState([]); const separator = options.pathSeparator; const style = { minWidth: options.minWidth ?? 200, maxWidth: options.maxWidth }; const dropdownWidth = options.dropdownWidth === undefined ? true : +options.dropdownWidth; + const maxUsagesReached = !!options.maxUsages && selected.length >= options.maxUsages; + const value = selected.map(path => path.map(p => p.value).join(separator)); + const displayed = selected.map(path => ({ + value: path.map(p => p.value).join(separator), + label: options.showFullPath ? path.map(p => p.label).join(separator) : path.at(-1).label, + })); useEffect(() => { - setTreeData(convert(items, options)); - }, [items]); + setTreeData(convert(items, { ...options, maxUsagesReached }, value)); + }, [items, maxUsagesReached]); const loadData = useCallback(async (node: any) => { return onLoadData?.(node.value.split(separator)); }, []); + const handleSearch = useCallback((list: AntTaxonomyItem[], expandedKeys: React.Key[] | null) => { + setFilteredTreeData(list); + if (expandedKeys?.length) setExpandedKeys(expandedKeys); + else setExpandedKeys(undefined); + + }, []); + + const renderDropdown = useCallback((origin: ReactNode) => { + return ( + <> + {!defaultSearch && ( + + )} + {origin} + + ); + }, [treeData]); + + const handleDropdownChange = useCallback((open: boolean) => { + if (open) { + // handleDropdownChange is being called before the dropdown is rendered, + // 200ms is the time that we have to wait to dropdown be rendered and animated + setTimeout(() => { + refInput.current?.focus(); + }, 200); + } else { + refInput.current?.resetValue(); + } + }, [refInput]); + return ( path.join(separator))} + treeData={defaultSearch ? treeData : filteredTreeData} + value={displayed} + labelInValue={true} onChange={items => onChange(null, items.map(item => item.value.split(separator)))} loadData={loadData} treeCheckable + showSearch={defaultSearch} + showArrow={!defaultSearch} + dropdownRender={renderDropdown} + onDropdownVisibleChange={handleDropdownChange} + treeExpandedKeys={!defaultSearch ? expandedKeys : undefined} + onTreeExpand={(expandedKeys: React.Key[]) => { + setExpandedKeys(expandedKeys); + }} treeCheckStrictly showCheckedStrategy={TreeSelect.SHOW_ALL} treeExpandAction="click" diff --git a/src/components/NewTaxonomy/TaxonomySearch.styl b/src/components/NewTaxonomy/TaxonomySearch.styl new file mode 100644 index 0000000000..3f4b6a2d5f --- /dev/null +++ b/src/components/NewTaxonomy/TaxonomySearch.styl @@ -0,0 +1,8 @@ +.taxonomy-search-input + width calc(100% - 8px) + height 40px + border-radius 4px + border 1px solid rgba(137, 128, 152, 0.16) + background url("data:image/svg+xml;base64, PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1Ljc1NSAxNC4yNTVIMTQuOTY1TDE0LjY4NSAxMy45ODVDMTUuNjY1IDEyLjg0NSAxNi4yNTUgMTEuMzY1IDE2LjI1NSA5Ljc1NUMxNi4yNTUgNi4xNjUgMTMuMzQ1IDMuMjU1IDkuNzU1IDMuMjU1QzYuMTY1IDMuMjU1IDMuMjU1IDYuMTY1IDMuMjU1IDkuNzU1QzMuMjU1IDEzLjM0NSA2LjE2NSAxNi4yNTUgOS43NTUgMTYuMjU1QzExLjM2NSAxNi4yNTUgMTIuODQ1IDE1LjY2NSAxMy45ODUgMTQuNjg1TDE0LjI1NSAxNC45NjVWMTUuNzU1TDE5LjI1NSAyMC43NDVMMjAuNzQ1IDE5LjI1NUwxNS43NTUgMTQuMjU1Wk05Ljc1NSAxNC4yNTVDNy4yNjUwMSAxNC4yNTUgNS4yNTUgMTIuMjQ1IDUuMjU1IDkuNzU1QzUuMjU1IDcuMjY1MDEgNy4yNjUwMSA1LjI1NSA5Ljc1NSA1LjI1NUMxMi4yNDUgNS4yNTUgMTQuMjU1IDcuMjY1MDEgMTQuMjU1IDkuNzU1QzE0LjI1NSAxMi4yNDUgMTIuMjQ1IDE0LjI1NSA5Ljc1NSAxNC4yNTVaIiBmaWxsPSIjMDk2REQ5Ii8+Cjwvc3ZnPgo=") center left 4px no-repeat #FAFAFA; + padding 4px 4px 4px 32px + margin 0 4px 14px 4px diff --git a/src/components/NewTaxonomy/TaxonomySearch.tsx b/src/components/NewTaxonomy/TaxonomySearch.tsx new file mode 100644 index 0000000000..f6e89e61d8 --- /dev/null +++ b/src/components/NewTaxonomy/TaxonomySearch.tsx @@ -0,0 +1,132 @@ +import React, { ChangeEvent, KeyboardEvent, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; + +import './TaxonomySearch.styl'; +import { Block } from '../../utils/bem'; +import { AntTaxonomyItem } from './NewTaxonomy'; +import { debounce } from 'lodash'; + +type TaxonomySearchProps = { + treeData: AntTaxonomyItem[], + onChange: (list: AntTaxonomyItem[], expandedKeys: React.Key[] | null) => void, +} + +export type TaxonomySearchRef = { + resetValue: () => void, + focus: () => void, +} + +const TaxonomySearch = React.forwardRef(({ + treeData, + onChange, +}, ref) => { + useImperativeHandle(ref, (): TaxonomySearchRef => { + return { + resetValue() { + setInputValue(''); + onChange(treeData, []); + }, + focus() { + return inputRef.current?.focus(); + }, + }; + }); + + const inputRef = useRef(); + const [inputValue, setInputValue] = useState(''); + + useEffect(() => { + const _filteredData = filterTreeData(treeData, inputValue); + + onChange(_filteredData.filteredDataTree, null); + }, [treeData]); + + // When the treeNode has additional formatting because of `hint` or `color` props, + // the `treeNode.title` is not a string but a react component, + // so we have to look for the title in children (1 or 2 levels deep) + const getTitle = useCallback((treeNodeTitle: any): string => { + if (typeof treeNodeTitle === 'string') return treeNodeTitle; + + if (typeof treeNodeTitle.props.children === 'object') + return getTitle(treeNodeTitle.props.children); + + return treeNodeTitle.props.children; + }, []); + + // To filter the treeData items that match with the searchValue + const filterTreeNode = useCallback((searchValue: string, treeNode: AntTaxonomyItem) => { + const lowerSearchValue = String(searchValue).toLowerCase(); + const lowerResultValue = getTitle(treeNode.title); + + if (!lowerSearchValue) { + return false; + } + + return String(lowerResultValue).toLowerCase().includes(lowerSearchValue); + }, []); + + // It's running recursively through treeData and its children filtering the content that match with the search value + const filterTreeData = useCallback((treeData: AntTaxonomyItem[], searchValue: string) => { + const _expandedKeys: React.Key[] = []; + + if (!searchValue) { + return { + filteredDataTree: treeData, + expandedKeys: _expandedKeys, + }; + } + + const dig = (list: AntTaxonomyItem[], keepAll = false) => { + return list.reduce((total, dataNode) => { + const children = dataNode['children']; + + const match = keepAll || filterTreeNode(searchValue, dataNode); + const childList = children?.length ? dig(children, match) : undefined; + + if (match || childList?.length) { + if (!keepAll && dataNode['children']?.length) + _expandedKeys.push(dataNode.key); + + total.push({ + ...dataNode, + isLeaf: !childList?.length, + children: childList, + }); + } + + return total; + }, []); + }; + + return { + filteredDataTree: dig(treeData), + expandedKeys: _expandedKeys, + }; + }, []); + + const handleSearch = useCallback(debounce(async (e: ChangeEvent) => { + const _filteredData = filterTreeData(treeData, e.target.value); + + onChange(_filteredData.filteredDataTree, _filteredData.expandedKeys); + }, 300), [treeData]); + + return ( + ) => { + setInputValue(e.target.value); + handleSearch(e); + }} + onKeyDown={(e: KeyboardEvent) => { + // to prevent selected items from being deleted + if (e.key === 'Backspace' || e.key === 'Delete') e.stopPropagation(); + }} + placeholder={'Search'} + data-testid={'taxonomy-search'} + name={'taxonomy-search-input'} + /> + ); +}); + +export { TaxonomySearch }; diff --git a/src/components/Node/Node.tsx b/src/components/Node/Node.tsx index d48cef6709..0785c979af 100644 --- a/src/components/Node/Node.tsx +++ b/src/components/Node/Node.tsx @@ -188,6 +188,9 @@ const NodeMinimal: FC = observer(({ node }) => { }); const useNodeName = (node: any) => { + // @todo sometimes node is control tag, not a region + // @todo and for new taxonomy it can be plain object + if (!node.$treenode) return null; return getType(node).name as keyof typeof NodeViews; }; diff --git a/src/components/Taxonomy/Taxonomy.tsx b/src/components/Taxonomy/Taxonomy.tsx index 4607417d4b..90f7f26503 100644 --- a/src/components/Taxonomy/Taxonomy.tsx +++ b/src/components/Taxonomy/Taxonomy.tsx @@ -9,15 +9,15 @@ import React, { } from 'react'; import { Dropdown, Menu } from 'antd'; +import { LsChevron } from '../../assets/icons'; +import { Tooltip } from '../../common/Tooltip/Tooltip'; import { useToggle } from '../../hooks/useToggle'; +import { CNTagName } from '../../utils/bem'; +import { FF_DEV_4075, FF_PROD_309, isFF } from '../../utils/feature-flags'; import { isArraysEqual } from '../../utils/utilities'; -import { LsChevron } from '../../assets/icons'; import TreeStructure from '../TreeStructure/TreeStructure'; import styles from './Taxonomy.module.scss'; -import { FF_DEV_4075, FF_PROD_309, isFF } from '../../utils/feature-flags'; -import { Tooltip } from '../../common/Tooltip/Tooltip'; -import { CNTagName } from '../../utils/bem'; type TaxonomyPath = string[]; type onAddLabelCallback = (path: string[]) => any; @@ -33,6 +33,7 @@ type TaxonomyItem = { }; type TaxonomyOptions = { + canRemoveItems?: boolean, leafsOnly?: boolean, showFullPath?: boolean, pathSeparator?: string, @@ -504,6 +505,10 @@ const Taxonomy = ({ const setSelected = (path: TaxonomyPath, value: boolean) => { const newSelected = value ? [...selected, path] : selected.filter(current => !isArraysEqual(current, path)); + // don't remove last item when taxonomy is used as labeling tool + // canRemoveItems is undefined when FF is off; false only when region is active + if (options.canRemoveItems === false && !newSelected.length) return; + setInternalSelected(newSelected); onChange && onChange(null, newSelected); }; diff --git a/src/components/TopBar/CurrentTask.js b/src/components/TopBar/CurrentTask.js index 8ceb9f8076..b3016b9413 100644 --- a/src/components/TopBar/CurrentTask.js +++ b/src/components/TopBar/CurrentTask.js @@ -1,15 +1,19 @@ +import React, { useMemo } from 'react'; import { observer } from 'mobx-react'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import { Button } from '../../common/Button/Button'; import { Block, Elem } from '../../utils/bem'; -import { FF_DEV_3873, FF_DEV_4174, isFF } from '../../utils/feature-flags'; +import { FF_DEV_3873, FF_DEV_4174, FF_TASK_COUNT_FIX, isFF } from '../../utils/feature-flags'; import { guidGenerator } from '../../utils/unique'; import { isDefined } from '../../utils/utilities'; import './CurrentTask.styl'; import { reaction } from 'mobx'; - export const CurrentTask = observer(({ store }) => { + const currentIndex = useMemo(() => { + return store.taskHistory.findIndex((x) => x.taskId === store.task.id) + 1; + }, [store.taskHistory]); + const [initialCommentLength, setInitialCommentLength] = useState(0); const [visibleComments, setVisibleComments] = useState(0); @@ -28,10 +32,6 @@ export const CurrentTask = observer(({ store }) => { }; }, []); - const currentIndex = useMemo(() => { - return store.taskHistory.findIndex((x) => x.taskId === store.task.id) + 1; - }, [store.taskHistory]); - useEffect(() => { if (store.commentStore.addedCommentThisSession) { setInitialCommentLength(visibleComments); @@ -61,9 +61,15 @@ export const CurrentTask = observer(({ store }) => { {store.task.id ?? guidGenerator()} {historyEnabled && showCounter && ( - - {currentIndex} of {store.taskHistory.length} - + isFF(FF_TASK_COUNT_FIX) ? ( + + {store.queuePosition} of {store.queueTotal} + + ) : ( + + {currentIndex} of {store.taskHistory.length} + + ) )} {historyEnabled && ( diff --git a/src/env/development.js b/src/env/development.js index de0b774ab6..93060c9e30 100644 --- a/src/env/development.js +++ b/src/env/development.js @@ -146,13 +146,13 @@ function configureApplication(params) { const options = { settings: params.settings || {}, - alert: m => console.log(m), // Noop for demo: window.alert(m) messages: { ...Messages, ...params.messages }, onSubmitAnnotation: params.onSubmitAnnotation ? params.onSubmitAnnotation : External.onSubmitAnnotation, onUpdateAnnotation: params.onUpdateAnnotation ? params.onUpdateAnnotation : External.onUpdateAnnotation, onDeleteAnnotation: params.onDeleteAnnotation ? params.onDeleteAnnotation : External.onDeleteAnnotation, onSkipTask: params.onSkipTask ? params.onSkipTask : External.onSkipTask, onUnskipTask: params.onUnskipTask ? params.onUnskipTask : External.onUnskipTask, + onPresignUrlForProject: params.onPresignUrlForProject, onSubmitDraft: params.onSubmitDraft, onTaskLoad: params.onTaskLoad ? params.onTaskLoad : External.onTaskLoad, onLabelStudioLoad: params.onLabelStudioLoad ? params.onLabelStudioLoad : External.onLabelStudioLoad, diff --git a/src/env/production.js b/src/env/production.js index 79386281eb..03aa48f6e4 100644 --- a/src/env/production.js +++ b/src/env/production.js @@ -56,7 +56,6 @@ function configureApplication(params) { // communication with the user settings: params.settings || {}, - alert: m => console.log(m), // Noop for demo: window.alert(m) messages: { ...Messages, ...params.messages }, // callbacks and event handlers @@ -66,6 +65,7 @@ function configureApplication(params) { onSkipTask: params.onSkipTask ? params.onSkipTask : External.onSkipTask, onUnskipTask: params.onUnskipTask ? params.onUnskipTask : External.onUnskipTask, onSubmitDraft: params.onSubmitDraft, + onPresignUrlForProject: params.onPresignUrlForProject, onTaskLoad: params.onTaskLoad || External.onTaskLoad, onLabelStudioLoad: params.onLabelStudioLoad || External.onLabelStudioLoad, onEntityCreate: params.onEntityCreate || External.onEntityCreate, diff --git a/src/mixins/HighlightMixin.js b/src/mixins/HighlightMixin.js index 7d604be1bf..2223861209 100644 --- a/src/mixins/HighlightMixin.js +++ b/src/mixins/HighlightMixin.js @@ -153,14 +153,8 @@ export const HighlightMixin = types updateSpans() { if (self._hasSpans || (isFF(FF_LSDV_4620_3) && self._spans?.length)) { const lastSpan = self._spans[self._spans.length - 1]; - const label = self.getLabels(); - // label is array, string or null, so check for length - if (!label?.length) { - lastSpan.removeAttribute('data-label'); - } else { - lastSpan.setAttribute('data-label', label); - } + Utils.Selection.applySpanStyles(lastSpan, { label: self.getLabels() }); } }, @@ -274,7 +268,7 @@ export const HighlightMixin = types }, getLabels() { - return self.labeling?.mainValue ?? []; + return (self.labeling?.selectedLabels ?? []).map(label => label.value).join(','); }, getLabelColor() { diff --git a/src/mixins/KonvaRegion.js b/src/mixins/KonvaRegion.js index c0f1e8d2ca..a1eee055db 100644 --- a/src/mixins/KonvaRegion.js +++ b/src/mixins/KonvaRegion.js @@ -1,5 +1,5 @@ import { types } from 'mobx-state-tree'; -import { FF_DBLCLICK_DELAY, FF_DEV_3793, isFF } from '../utils/feature-flags'; +import { FF_DBLCLICK_DELAY, FF_DEV_3793, FF_ZOOM_OPTIM, isFF } from '../utils/feature-flags'; export const KonvaRegionMixin = types.model({}) .views((self) => { return { @@ -11,6 +11,7 @@ export const KonvaRegionMixin = types.model({}) const bbox = self.bboxCoords; if (!isFF(FF_DEV_3793)) return bbox; + if (!self.parent) return null; return { left: self.parent.internalToCanvasX(bbox.left), @@ -19,6 +20,15 @@ export const KonvaRegionMixin = types.model({}) bottom: self.parent.internalToCanvasY(bbox.bottom), }; }, + get inViewPort() { + if (!isFF(FF_ZOOM_OPTIM)) return true; + return !!self && !!self.bboxCoordsCanvas && !!self.object && ( + self.bboxCoordsCanvas.right >= self.object.viewPortBBoxCoords.left + && self.bboxCoordsCanvas.bottom >= self.object.viewPortBBoxCoords.top + && self.bboxCoordsCanvas.left <= self.object.viewPortBBoxCoords.right + && self.bboxCoordsCanvas.top <= self.object.viewPortBBoxCoords.bottom + ); + }, get control() { // that's a little bit tricky, but it seems that having a tools field is necessary for the region-creating control tag and it's might be a clue return self.results.find(result => result.from_name.tools)?.from_name; @@ -107,7 +117,7 @@ export const KonvaRegionMixin = types.model({}) if (isDoubleClick) { self.onDoubleClickRegion(); - return; + return; } } diff --git a/src/mixins/ProcessAttrs.js b/src/mixins/ProcessAttrs.js index b41a6d14e7..9c828f107b 100644 --- a/src/mixins/ProcessAttrs.js +++ b/src/mixins/ProcessAttrs.js @@ -31,7 +31,7 @@ const ProcessAttrsMixin = types }, updateValue(store) { - self._value = parseValue(self.value, store.task.dataObj); + self._value = parseValue(self.value, store?.task?.dataObj ?? {}); }, /** diff --git a/src/mixins/SelectedChoiceMixin.js b/src/mixins/SelectedChoiceMixin.js index 99f5fb84b7..5ef40a7b95 100644 --- a/src/mixins/SelectedChoiceMixin.js +++ b/src/mixins/SelectedChoiceMixin.js @@ -21,6 +21,19 @@ const SelectedChoiceMixin = types return isDefined(choice1) && isDefined(choice2) && choice1 === choice2; }, + // @todo it's better to only take final values into account + // @todo (meaning alias only, not alias + value when alias is present) + // @todo so this should be the final and simpliest method + hasChoiceSelectionSimple(choiceValue) { + if (choiceValue?.length) { + // grab the string value; for taxonomy, it's the last value in the array + const selectedValues = self.selectedValues().map(s => Array.isArray(s) ? s.at(-1) : s); + + return choiceValue.some(value => selectedValues.includes(value)); + } + + return self.isSelected; + }, hasChoiceSelection(choiceValue, selectedValues = []) { if (choiceValue?.length) { // @todo Revisit this and make it more consistent, and refactor this diff --git a/src/regions/BrushRegion.js b/src/regions/BrushRegion.js index 515b6774c3..b1f7cd7330 100644 --- a/src/regions/BrushRegion.js +++ b/src/regions/BrushRegion.js @@ -1,7 +1,7 @@ import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { Group, Image, Layer, Shape } from 'react-konva'; import { observer } from 'mobx-react'; -import { getParent, getRoot, getType, hasParent, isAlive, types } from 'mobx-state-tree'; +import { getParent, getRoot, hasParent, isAlive, types } from 'mobx-state-tree'; import Registry from '../core/Registry'; import NormalizationMixin from '../mixins/Normalization'; @@ -664,11 +664,6 @@ const HtxBrushView = ({ item, setShapeRef }) => { return; } - const tool = item.parent.getToolsManager().findSelectedTool(); - const isMoveTool = tool && getType(tool).name === 'MoveTool'; - - if (tool && !isMoveTool) return; - if (store.annotationStore.selected.relationMode) { stage.container().style.cursor = 'default'; } diff --git a/src/regions/EllipseRegion.js b/src/regions/EllipseRegion.js index b689ee0dbc..e13d8f86c1 100644 --- a/src/regions/EllipseRegion.js +++ b/src/regions/EllipseRegion.js @@ -309,6 +309,9 @@ const HtxEllipseView = ({ item, setShapeRef }) => { const stage = item.parent?.stageRef; const { suggestion } = useContext(ImageViewContext) ?? {}; + if (!item.parent) return null; + if (!item.inViewPort) return null; + return ( { stroke={regionStyles.strokeColor} strokeWidth={regionStyles.strokeWidth} strokeScaleEnabled={false} + perfectDrawEnabled={false} + shadowForStrokeEnabled={false} shadowBlur={0} scaleX={item.scaleX} scaleY={item.scaleY} diff --git a/src/regions/KeyPointRegion.js b/src/regions/KeyPointRegion.js index 9258d38df9..ff10303eb4 100644 --- a/src/regions/KeyPointRegion.js +++ b/src/regions/KeyPointRegion.js @@ -212,6 +212,9 @@ const HtxKeyPointView = ({ item, setShapeRef }) => { const stage = item.parent?.stageRef; + if (!item.parent) return null; + if (!item.inViewPort) return null; + return ( { strokeWidth={stroke[item.size]} dragOnTop={false} strokeScaleEnabled={false} + perfectDrawEnabled={false} + shadowForStrokeEnabled={false} scaleX={1 / (item.stage.zoomScale || 1)} scaleY={1 / (item.stage.zoomScale || 1)} onDblClick={() => { @@ -335,6 +337,9 @@ const PolygonPointView = observer(({ item, name }) => { fill={fill} stroke="black" strokeWidth={stroke[item.size]} + strokeScaleEnabled={false} + perfectDrawEnabled={false} + shadowForStrokeEnabled={false} dragOnTop={false} {...dragOpts} {...startPointAttr} diff --git a/src/regions/PolygonRegion.js b/src/regions/PolygonRegion.js index a9249ca347..950e58d8f2 100644 --- a/src/regions/PolygonRegion.js +++ b/src/regions/PolygonRegion.js @@ -96,6 +96,9 @@ const Model = types return bbox; }, + get flattenedPoints() { + return getFlattenedPoints(this.points); + }, })) .actions(self => { return { @@ -337,15 +340,25 @@ const PolygonRegionModel = types.compose( function getAnchorPoint({ flattenedPoints, cursorX, cursorY }) { const [point1X, point1Y, point2X, point2Y] = flattenedPoints; const y = - ((point2X - point1X) * (point2X * point1Y - point1X * point2Y) + + ( + (point2X - point1X) * (point2X * point1Y - point1X * point2Y) + (point2X - point1X) * (point2Y - point1Y) * cursorX + - (point2Y - point1Y) * (point2Y - point1Y) * cursorY) / - ((point2Y - point1Y) * (point2Y - point1Y) + (point2X - point1X) * (point2X - point1X)); + (point2Y - point1Y) * (point2Y - point1Y) * cursorY + ) / + ( + (point2Y - point1Y) * (point2Y - point1Y) + + (point2X - point1X) * (point2X - point1X) + ); const x = cursorX - - ((point2Y - point1Y) * - (point2X * point1Y - point1X * point2Y + cursorX * (point2Y - point1Y) - cursorY * (point2X - point1X))) / - ((point2Y - point1Y) * (point2Y - point1Y) + (point2X - point1X) * (point2X - point1X)); + ( + (point2Y - point1Y) * + (point2X * point1Y - point1X * point2Y + cursorX * (point2Y - point1Y) - cursorY * (point2X - point1X)) + ) / + ( + (point2Y - point1Y) * (point2Y - point1Y) + + (point2X - point1X) * (point2X - point1X) + ); return [x, y]; } @@ -399,9 +412,8 @@ function removeHoverAnchor({ layer }) { } const Poly = memo(observer(({ item, colors, dragProps, draggable }) => { - const { points } = item; + const { flattenedPoints } = item; const name = 'poly'; - const flattenedPoints = getFlattenedPoints(points); return ( @@ -412,6 +424,8 @@ const Poly = memo(observer(({ item, colors, dragProps, draggable }) => { stroke={colors.strokeColor} strokeWidth={colors.strokeWidth} strokeScaleEnabled={false} + perfectDrawEnabled={false} + shadowForStrokeEnabled={false} points={flattenedPoints} fill={colors.fillColor} closed={true} @@ -452,72 +466,92 @@ const Poly = memo(observer(({ item, colors, dragProps, draggable }) => { ); })); -const HtxPolygonView = ({ item, setShapeRef }) => { - const { store } = item; - const { suggestion } = useContext(ImageViewContext) ?? {}; +/** + * Line between 2 points + */ +function Edge({ name, item, idx, p1, p2, closed, regionStyles }) { + const insertIdx = idx + 1; // idx1 + 1 or idx2 + const flattenedPoints = useMemo(() => { + return getFlattenedPoints([p1, p2]); + }, [p1, p2]); + + const lineProps = closed ? { + stroke: 'transparent', + strokeWidth: regionStyles.strokeWidth, + strokeScaleEnabled: false, + } : { + stroke: regionStyles.strokeColor, + strokeWidth: regionStyles.strokeWidth, + strokeScaleEnabled: false, + }; - const regionStyles = useRegionStyles(item, { - useStrokeAsFill: true, - }); + return ( + item.handleLineClick({ e, flattenedPoints, insertIdx })} + onMouseMove={e => { + if (!item.closed || !item.selected || item.isReadOnly()) return; - /** - * Render line between 2 points - */ - function renderLine({ points, idx1, idx2, closed }) { - const name = `border_${idx1}_${idx2}`; - - if (!item.closed && idx2 === 0) return null; - - const insertIdx = idx1 + 1; // idx1 + 1 or idx2 - const flattenedPoints = getFlattenedPoints([points[idx1], points[idx2]]); - - const lineProps = closed ? { - stroke: 'transparent', - strokeWidth: regionStyles.strokeWidth, - strokeScaleEnabled: false, - } : { - stroke: regionStyles.strokeColor, - strokeWidth: regionStyles.strokeWidth, - strokeScaleEnabled: false, - }; + item.handleMouseMove({ e, flattenedPoints }); + }} + onMouseLeave={e => item.handleMouseLeave({ e })} + > + + + ); +} - return ( - item.handleLineClick({ e, flattenedPoints, insertIdx })} - onMouseMove={e => { - if (!item.closed || !item.selected || item.isReadOnly()) return; - - item.handleMouseMove({ e, flattenedPoints }); - }} - onMouseLeave={e => item.handleMouseLeave({ e })} - > - - - ); +const Edges = memo(observer(({ item, regionStyles }) => { + const { points,closed } = item; + const name = 'borders'; + + if (item.closed && (item.parent.useTransformer || !item.selected)) { + return null; } + return ( + + {points.map((p, idx) => { + const idx1 = idx; + const idx2 = idx === points.length - 1 ? 0 : idx + 1; - function renderLines(points, closed) { - const name = 'borders'; + if (!closed && idx2 === 0) { + return null; + } - return ( - - {points.map((p, idx) => { - const idx1 = idx; - const idx2 = idx === points.length - 1 ? 0 : idx + 1; + return ( + + ); + })} + + ); +})); - return renderLine({ points, idx1, idx2, closed }); - })} - - ); - } +const HtxPolygonView = ({ item, setShapeRef }) => { + const { store } = item; + const { suggestion } = useContext(ImageViewContext) ?? {}; + + const regionStyles = useRegionStyles(item, { + useStrokeAsFill: true, + }); function renderCircle({ points, idx }) { const name = `anchor_${points.length}_${idx}`; @@ -531,7 +565,9 @@ const HtxPolygonView = ({ item, setShapeRef }) => { function renderCircles(points) { const name = 'anchors'; - if (item.parent.useTransformer && item.closed) return null; + if (item.closed && (item.parent.useTransformer || !item.selected)) { + return null; + } return ( {points.map((p, idx) => renderCircle({ points, idx }))} @@ -588,6 +624,7 @@ const HtxPolygonView = ({ item, setShapeRef }) => { }, [item.closed]); if (!item.parent) return null; + if (!item.inViewPort) return null; const stage = item.parent?.stageRef; @@ -636,7 +673,7 @@ const HtxPolygonView = ({ item, setShapeRef }) => { {item.mouseOverStartPoint} {item.points && item.closed ? 1}/> : null} - {(item.points && !item.isReadOnly()) ? renderLines(item.points, item.closed) : null} + {(item.points && !item.isReadOnly()) ? : null} {(item.points && !item.isReadOnly()) ? renderCircles(item.points) : null} ); diff --git a/src/regions/RectRegion.js b/src/regions/RectRegion.js index 97a082179a..bff975b4c7 100644 --- a/src/regions/RectRegion.js +++ b/src/regions/RectRegion.js @@ -418,6 +418,9 @@ const HtxRectangleView = ({ item, setShapeRef }) => { const eventHandlers = {}; + if (!item.parent) return null; + if (!item.inViewPort) return null; + if (!suggestion && !item.isReadOnly()) { eventHandlers.onTransform = ({ target }) => { // resetting the skew makes transformations weird but predictable @@ -483,6 +486,8 @@ const HtxRectangleView = ({ item, setShapeRef }) => { stroke={regionStyles.strokeColor} strokeWidth={regionStyles.strokeWidth} strokeScaleEnabled={false} + perfectDrawEnabled={false} + shadowForStrokeEnabled={false} shadowBlur={0} dash={suggestion ? [10, 10] : null} scaleX={item.scaleX} diff --git a/src/regions/Result.js b/src/regions/Result.js index 4be89f4a53..d1756fa6fd 100644 --- a/src/regions/Result.js +++ b/src/regions/Result.js @@ -140,19 +140,14 @@ const Result = types return self.mainValue?.join(joinstr) || ''; }, + // @todo check all usages of selectedLabels: + // — check usages of non-array values (like `if selectedValues ...`) + // - check empty labels, they should be returned as an array get selectedLabels() { - if (self.type === 'taxonomy') { - const sep = self.from_name.pathseparator; - const join = self.from_name.showfullpath; - - return (self.mainValue || []) - .map(v => join ? v.join(sep) : v.at(-1)) - .map(v => ({ value: v, id: v })); - } if (self.mainValue?.length === 0 && self.from_name.allowempty) { return self.from_name.findLabel(null); } - return self.mainValue?.map(value => self.from_name.findLabel(value)).filter(Boolean); + return self.mainValue?.map(value => self.from_name.findLabel(value)).filter(Boolean) ?? []; }, /** @@ -212,7 +207,7 @@ const Result = types get style() { if (!self.tag) return null; - const fillcolor = self.tag.background || self.tag.parent.fillcolor; + const fillcolor = self.tag.background || self.tag.parent?.fillcolor; if (!fillcolor) return null; const strokecolor = self.tag.background || self.tag.parent.strokecolor; diff --git a/src/stores/Annotation/Annotation.js b/src/stores/Annotation/Annotation.js index 9cefe1c823..fe92f70b02 100644 --- a/src/stores/Annotation/Annotation.js +++ b/src/stores/Annotation/Annotation.js @@ -15,7 +15,6 @@ import { FF_DEV_1284, FF_DEV_1598, FF_DEV_2100, - FF_DEV_2100_A, FF_DEV_2432, FF_DEV_3391, FF_LLM_EPIC, FF_LSDV_3009, @@ -597,9 +596,9 @@ export const Annotation = types setDefaultValues() { self.names.forEach(tag => { - if (isFF(FF_DEV_2100_A) && tag?.type === 'choices' && tag.preselectedValues?.length) { + if (['choices', 'taxonomy'].includes(tag?.type) && tag.preselectedValues?.length) { // - self.createResult({}, { choices: tag.preselectedValues }, tag, tag.toname); + self.createResult({}, { [tag?.type]: tag.preselectedValues }, tag, tag.toname); } }); }, @@ -697,8 +696,8 @@ export const Annotation = types async saveDraftImmediatelyWithResults() { // There is no draft to save as it was already saved as an annotation - if (self.submissionStarted) return {}; - + if (self.submissionStarted || self.isDraftSaving) return {}; + self.setDraftSaving(true); const res = await self.saveDraft(null); return res; @@ -751,13 +750,6 @@ export const Annotation = types // may come handy when you have a tag that acts or depends // on other elements in the tree. if (node.annotationAttached) node.annotationAttached(); - - - // @todo special place to init such predefined values; `afterAttach` of the tag? - // preselected choices - if (!isFF(FF_DEV_2100_A) && !self.pk && node?.type === 'choices' && node.preselectedValues?.length) { - self.createResult({}, { choices: node.preselectedValues }, node, node.toname); - } }); self.history.onUpdate(self.updateObjects); diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js index fdba6f4f72..7b53ea8ed0 100644 --- a/src/stores/AppStore.js +++ b/src/stores/AppStore.js @@ -17,12 +17,12 @@ import { Hotkey } from '../core/Hotkey'; import ToolsManager from '../tools/Manager'; import Utils from '../utils'; import { guidGenerator } from '../utils/unique'; -import { delay, isDefined } from '../utils/utilities'; +import { clamp, delay, isDefined } from '../utils/utilities'; import AnnotationStore from './Annotation/store'; import Project from './ProjectStore'; import Settings from './SettingsStore'; import Task from './TaskStore'; -import { UserExtended } from './UserStore'; +import { UserExtended } from './UserStore'; import { UserLabels } from './UserLabels'; import { FF_DEV_1536, FF_DEV_2715, FF_LLM_EPIC, FF_LSDV_4620_3_ML, FF_LSDV_4998, isFF } from '../utils/feature-flags'; import { CommentStore } from './Comment/CommentStore'; @@ -154,6 +154,10 @@ export default types users: types.optional(types.array(UserExtended), []), userLabels: isFF(FF_DEV_1536) ? types.optional(UserLabels, { controls: {} }) : types.undefined, + + queueTotal: types.optional(types.number, 0), + + queuePosition: types.optional(types.number, 0), }) .preProcessSnapshot((sn) => { // This should only be handled if the sn.user value is an object, and converted to a reference id for other @@ -185,13 +189,9 @@ export default types suggestionsRequest: null, })) .views(self => ({ - /** - * Get alert - */ - get alert() { - return getEnv(self).alert; + get events() { + return getEnv(self).events; }, - get hasSegmentation() { // not an object and not a classification const isSegmentation = t => !t.getAvailableStates && !t.perRegionVisible; @@ -536,6 +536,9 @@ export default types }) .then(() => self.setFlags({ isSubmitting: false })); } + function incrementQueuePosition(number = 1) { + self.queuePosition = clamp(self.queuePosition + number, 1, self.queueTotal); + } function submitAnnotation() { if (self.isSubmitting) return; @@ -550,6 +553,7 @@ export default types entity.sendUserGenerate(); handleSubmittingFlag(async () => { await getEnv(self).events.invoke(event, self, entity); + self.incrementQueuePosition(); }); entity.dropDraft(); } @@ -565,6 +569,7 @@ export default types handleSubmittingFlag(async () => { await getEnv(self).events.invoke('updateAnnotation', self, entity, extraData); + self.incrementQueuePosition(); }); entity.dropDraft(); !entity.sentUserGenerate && entity.sendUserGenerate(); @@ -574,6 +579,7 @@ export default types if (self.isSubmitting) return; handleSubmittingFlag(() => { getEnv(self).events.invoke('skipTask', self, extraData); + self.incrementQueuePosition(); }, 'Error during skip, try again'); } @@ -597,6 +603,7 @@ export default types entity.dropDraft(); await getEnv(self).events.invoke('acceptAnnotation', self, { isDirty, entity }); + self.incrementQueuePosition(); }, 'Error during accept, try again'); } @@ -613,9 +620,23 @@ export default types entity.dropDraft(); await getEnv(self).events.invoke('rejectAnnotation', self, { isDirty, entity, comment }); + self.incrementQueuePosition(-1); + }, 'Error during reject, try again'); } + /** + * Exchange storage url for presigned url for task + */ + async function presignUrlForProject(url) { + // Event invocation returns array of results for all handlers. + const urls = await self.events.invoke('presignUrlForProject', self, url); + + const presignUrl = urls?.[0]; + + return presignUrl; + } + /** * Reset annotation store */ @@ -761,14 +782,20 @@ export default types // or annotation created from prediction await annotation.saveDraft({ was_postponed: true }); await getEnv(self).events.invoke('nextTask'); + self.incrementQueuePosition(); + } function nextTask() { + if (self.canGoNextTask) { const { taskId, annotationId } = self.taskHistory[self.taskHistory.findIndex((x) => x.taskId === self.task.id) + 1]; getEnv(self).events.invoke('nextTask', taskId, annotationId); + self.incrementQueuePosition(); + } + } function prevTask(e, shouldGoBack = false) { @@ -778,6 +805,8 @@ export default types const { taskId, annotationId } = self.taskHistory[length]; getEnv(self).events.invoke('prevTask', taskId, annotationId); + self.incrementQueuePosition(-1); + } } @@ -817,6 +846,7 @@ export default types updateAnnotation, acceptAnnotation, rejectAnnotation, + presignUrlForProject, setUsers, mergeUsers, @@ -833,6 +863,7 @@ export default types nextTask, prevTask, postponeTask, + incrementQueuePosition, beforeDestroy() { ToolsManager.removeAllTools(); appControls = null; diff --git a/src/tags/control/Choice.js b/src/tags/control/Choice.js index 64ad18f7f0..e4868136f1 100644 --- a/src/tags/control/Choice.js +++ b/src/tags/control/Choice.js @@ -1,6 +1,5 @@ -import React, { Component, useCallback, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import Button from 'antd/lib/button/index'; -import Form from 'antd/lib/form/index'; import Radio from 'antd/lib/radio/index'; import Checkbox from 'antd/lib/checkbox/index'; import { inject, observer } from 'mobx-react'; @@ -13,7 +12,7 @@ import Tree from '../../core/Tree'; import Types from '../../core/Types'; import { AnnotationMixin } from '../../mixins/AnnotationMixin'; import { TagParentMixin } from '../../mixins/TagParentMixin'; -import { FF_DEV_2007, FF_DEV_2244, FF_DEV_3391, FF_PROD_309, isFF } from '../../utils/feature-flags'; +import { FF_DEV_3391, FF_PROD_309, isFF } from '../../utils/feature-flags'; import { Block, Elem } from '../../utils/bem'; import './Choice/Choice.styl'; import { LsChevron } from '../../assets/icons'; @@ -22,7 +21,6 @@ import { HintTooltip } from '../../components/Taxonomy/Taxonomy'; /** * The `Choice` tag represents a single choice for annotations. Use with the `Choices` tag or `Taxonomy` tag to provide specific choice options. * - * [^FF_DEV_2007]: `ff_dev_2007_rework_choices_280322_short` should be enabled to use `html` attribute * [^FF_PROD_309]: The `hint` attribute works only when `fflag_feat_front_prod_309_choice_hint_080523_short` is enabled * * @example @@ -44,8 +42,9 @@ import { HintTooltip } from '../../components/Taxonomy/Taxonomy'; * @param {string} [alias] - Alias for the choice. If used, the alias replaces the choice value in the annotation results. Alias does not display in the interface. * @param {style} [style] - CSS style of the checkbox element * @param {string} [hotkey] - Hotkey for the selection - * @param {string} [html] - can be used to show enriched content[^FF_DEV_2007], it has higher priority than `value`, however `value` will be used in the exported result (should be properly escaped) + * @param {string} [html] - Can be used to show enriched content, it has higher priority than `value`, however `value` will be used in the exported result (should be properly escaped) * @param {string} [hint] - Hint for choice on hover[^FF_PROD_309] + * @param {string} [color] - Color for Taxonomy item */ const TagAttrs = types.model({ ...(isFF(FF_DEV_3391) ? { id: types.identifier } : {}), @@ -54,7 +53,8 @@ const TagAttrs = types.model({ value: types.maybeNull(types.string), hotkey: types.maybeNull(types.string), style: types.maybeNull(types.string), - ...(isFF(FF_DEV_2007) ? { html: types.maybeNull(types.string) } : {}), + html: types.maybeNull(types.string), + color: types.maybeNull(types.string), ...(isFF(FF_PROD_309) ? { hint: types.maybeNull(types.string) } : {}), }); @@ -90,11 +90,11 @@ const Model = types }, get sel() { - return !isFF(FF_DEV_2244) || self.isLeaf ? self._sel : self.children.every(child => child.sel === true); + return self.isLeaf ? self._sel : self.children.every(child => child.sel === true); }, get indeterminate() { - return isFF(FF_DEV_2244) && (self.isLeaf ? false : !self.sel && self.children.some(child => child.sel === true)); + return self.isLeaf ? false : !self.sel && self.children.some(child => child.sel === true); }, get parentChoice() { @@ -104,13 +104,13 @@ const Model = types return !self.nestedResults && !!self.parentChoice; }, get nestedResults() { - return isFF(FF_DEV_2007) && self.parent?.allownested !== false; + return self.parent?.allownested !== false; }, get _resultValue() { return self.alias ?? self._value; }, get resultValue() { - if (isFF(FF_DEV_2007) && self.nestedResults) { + if (self.nestedResults) { const value = []; let choice = self; @@ -167,66 +167,7 @@ const Model = types return {}; }); -const ChoiceModel = types.compose('ChoiceModel', TagParentMixin, TagAttrs, Model, ProcessAttrsMixin, AnnotationMixin); - -function triggerElementGetter(el) { - return el?.input?.parentNode?.parentNode; -} - -class HtxChoiceView extends Component { - render() { - const { item, store } = this.props; - - let style = {}; - - if (item.style) style = Tree.cssConverter(item.style); - - if (!item.visible) { - style['display'] = 'none'; - } - - const showHotkey = - (store.settings.enableTooltips || store.settings.enableLabelTooltips) && - store.settings.enableHotkeys && - item.hotkey; - - const props = { - checked: item.sel, - disabled: item.parent?.isReadOnly(), - onChange: ev => { - if (item.isReadOnly()) return; - item.toggleSelected(); - ev.nativeEvent.target.blur(); - }, - }; - - if (item.isCheckbox) { - const cStyle = Object.assign({ display: 'flex', alignItems: 'center', marginBottom: 0 }, style); - - return ( - - - - {item._value} - {showHotkey && [{item.hotkey}]} - - - - ); - } else { - return ( -
- - - {item._value} - {showHotkey && [{item.hotkey}]} - - -
- ); - } - } -} +const ChoiceModel = types.compose('ChoiceModel', TagParentMixin, TagAttrs, ProcessAttrsMixin, Model, AnnotationMixin); // `name` can't be passed into bem components const nameWrapper = (Component, name) => { @@ -285,14 +226,7 @@ const HtxNewChoiceView = ({ item, store }) => { ); }; -const HtxOldChoice = inject('store')(observer(HtxChoiceView)); -const HtxNewChoice = inject('store')(observer(HtxNewChoiceView)); - -const HtxChoice = (props) => { - const HtxChoiceComponent = !isFF(FF_DEV_2007) ? HtxOldChoice : HtxNewChoice; - - return ; -}; +const HtxChoice = inject('store')(observer(HtxNewChoiceView)); Registry.addTag('choice', ChoiceModel, HtxChoice); diff --git a/src/tags/control/Choices.js b/src/tags/control/Choices.js index f8784b6a00..182d142c3c 100644 --- a/src/tags/control/Choices.js +++ b/src/tags/control/Choices.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Form, Select } from 'antd'; +import { Select } from 'antd'; import { observer } from 'mobx-react'; import { types } from 'mobx-state-tree'; @@ -19,7 +19,7 @@ import './Choices/Choises.styl'; import './Choice'; import DynamicChildrenMixin from '../../mixins/DynamicChildrenMixin'; -import { FF_DEV_2007, FF_DEV_2007_DEV_2008, FF_LSDV_4583, isFF } from '../../utils/feature-flags'; +import { FF_LSDV_4583, isFF } from '../../utils/feature-flags'; import { ReadOnlyControlMixin } from '../../mixins/ReadOnlyMixin'; import SelectedChoiceMixin from '../../mixins/SelectedChoiceMixin'; import { HintTooltip } from '../../components/Taxonomy/Taxonomy'; @@ -37,8 +37,6 @@ const { Option } = Select; * The `Choices` tag can be used with any data types. * * [^FF_LSDV_4583]: `fflag_feat_front_lsdv_4583_multi_image_segmentation_short` should be enabled for `perItem` functionality. - * [^FF_DEV_2007_DEV_2008]: `ff_dev_2007_dev_2008_dynamic_tag_children_250322_short` should be enabled to use dynamic options. - * [^FF_DEV_2007]: `ff_dev_2007_rework_choices_280322_short` should be enabled to use `html` attribute * * @example * @@ -54,8 +52,8 @@ const { Option } = Select; * * @example This config with dynamic labels * * *
); diff --git a/src/tags/control/DateTime.js b/src/tags/control/DateTime.js index 679639b36d..8cfdda6371 100644 --- a/src/tags/control/DateTime.js +++ b/src/tags/control/DateTime.js @@ -188,6 +188,9 @@ const Model = types years.push(y); } + // every month should have this day, so current day is bad: + // on Oct 30th when you change month to February it resets to March + date.setDate(1); for (let m = 0; m < 12; m++) { date.setMonth(m); months[m] = monthName(date); diff --git a/src/tags/control/Labels/Labels.js b/src/tags/control/Labels/Labels.js index 5508e26e6c..4bfe098263 100644 --- a/src/tags/control/Labels/Labels.js +++ b/src/tags/control/Labels/Labels.js @@ -13,7 +13,6 @@ import DynamicChildrenMixin from '../../../mixins/DynamicChildrenMixin'; import LabelMixin from '../../../mixins/LabelMixin'; import SelectedModelMixin from '../../../mixins/SelectedModel'; import { Block } from '../../../utils/bem'; -import { FF_DEV_2007_DEV_2008, isFF } from '../../../utils/feature-flags'; import ControlBase from '../Base'; import '../Label'; import './Labels.styl'; @@ -85,7 +84,7 @@ const TagAttrs = types.model({ fillopacity: types.maybeNull(customTypes.range()), allowempty: types.optional(types.boolean, false), - ...(isFF(FF_DEV_2007_DEV_2008) ? { value: types.optional(types.string, '') } : {}), + value: types.optional(types.string, ''), }); /** @@ -141,7 +140,7 @@ const LabelsModel = types.compose( ModelAttrs, TagAttrs, AnnotationMixin, - ...(isFF(FF_DEV_2007_DEV_2008) ? [DynamicChildrenMixin] : []), + DynamicChildrenMixin, Model, SelectedModelMixin.props({ _child: 'LabelModel' }), ); diff --git a/src/tags/control/Taxonomy/Taxonomy.js b/src/tags/control/Taxonomy/Taxonomy.js index 52431090af..1745b2a330 100644 --- a/src/tags/control/Taxonomy/Taxonomy.js +++ b/src/tags/control/Taxonomy/Taxonomy.js @@ -1,6 +1,6 @@ import React from 'react'; import { observer } from 'mobx-react'; -import { flow, types } from 'mobx-state-tree'; +import { flow, getRoot, types } from 'mobx-state-tree'; import { Spin } from 'antd'; import Infomodal from '../../../components/Infomodal/Infomodal'; @@ -8,6 +8,7 @@ import { NewTaxonomy } from '../../../components/NewTaxonomy/NewTaxonomy'; import { Taxonomy } from '../../../components/Taxonomy/Taxonomy'; import { guidGenerator } from '../../../core/Helpers'; import Registry from '../../../core/Registry'; +import Tree from '../../../core/Tree'; import Types from '../../../core/Types'; import { AnnotationMixin } from '../../../mixins/AnnotationMixin'; import DynamicChildrenMixin from '../../../mixins/DynamicChildrenMixin'; @@ -19,11 +20,32 @@ import SelectedChoiceMixin from '../../../mixins/SelectedChoiceMixin'; import { SharedStoreMixin } from '../../../mixins/SharedChoiceStore/mixin'; import VisibilityMixin from '../../../mixins/Visibility'; import { parseValue } from '../../../utils/data'; -import { FF_DEV_2007_DEV_2008, FF_DEV_3617, FF_LSDV_4583, FF_TAXONOMY_ASYNC, FF_TAXONOMY_LABELING, isFF } from '../../../utils/feature-flags'; +import { + FF_DEV_3617, + FF_LEAP_218, + FF_LSDV_4583, + FF_TAXONOMY_ASYNC, + FF_TAXONOMY_LABELING, + FF_TAXONOMY_SELECTED, + isFF +} from '../../../utils/feature-flags'; import ControlBase from '../Base'; import ClassificationBase from '../ClassificationBase'; import styles from './Taxonomy.styl'; +import messages from '../../../utils/messages'; +import { errorBuilder } from '../../../core/DataValidator/ConfigValidator'; + +/** + * @typedef TaxonomyItem + * @property {string} label + * @property {string[]} path + * @property {number} depth + * @property {string} [hint] + * @property {string} [color] + * @property {TaxonomyItem[]} [children] + * @property {string} [alias] + */ /** * The `Taxonomy` tag is used to create one or more hierarchical classifications, storing both choice selections and their ancestors in the results. Use for nested classification tasks with the `Choice` tag. @@ -72,6 +94,7 @@ const TagAttrs = types.model({ labeling: types.optional(types.boolean, false), leafsonly: types.optional(types.boolean, false), showfullpath: types.optional(types.boolean, false), + legacy: types.optional(types.boolean, false), pathseparator: types.optional(types.string, ' / '), apiurl: types.maybeNull(types.string), placeholder: '', @@ -79,7 +102,7 @@ const TagAttrs = types.model({ maxwidth: types.maybeNull(types.string), dropdownwidth: types.maybeNull(types.string), maxusages: types.maybeNull(types.string), - ...(isFF(FF_DEV_2007_DEV_2008) ? { value: types.optional(types.string, '') } : {}), + value: types.optional(types.string, ''), }); function traverse(root) { @@ -103,6 +126,7 @@ function traverse(root) { const depth = parents.length; const obj = { label, path, depth, hint }; + if (node.color) obj.color = node.color; if (node.children) { obj.children = visitUnique(node.children, path); } @@ -111,7 +135,7 @@ function traverse(root) { }; if (!root) return []; - if (isFF(FF_DEV_2007_DEV_2008) && !Array.isArray(root)) return visitUnique([root]); + if (!Array.isArray(root)) return visitUnique([root]); return visitUnique(root); } @@ -141,6 +165,10 @@ const TaxonomyLabelingResult = types return self.annotation.results.find(r => r.from_name === self && r.area === area); }, + get canRemoveItems() { + if (!self.isLabeling) return true; + return !self.result; + }, })) .actions(self => { const Super = { @@ -154,6 +182,35 @@ const TaxonomyLabelingResult = types self.result.area.setValue(self); } }, + + /** + * @param {string[]} path saved value from Taxonomy + * @returns quazi-label object to act as Label in most places + */ + findLabel(path) { + let title = ''; + let items = self.items; + let item; + + for (const value of path) { + item = items?.find(item => item.path.at(-1) === value); + + if (!item) return null; + + items = item.children; + title = self.showfullpath && title ? title + self.pathseparator + item.label : item.label; + } + + const label = { value: title, id: path.join(self.pathseparator) }; + + if (item.color) { + // to conform the current format of our Result#style (and it requires parent) + label.background = item.color; + label.parent = {}; + } + + return label; + }, }; }); @@ -203,6 +260,14 @@ const Model = types return 'taxonomy'; }, + get tiedChildren() { + return Tree.filterChildrenOfType(self, 'ChoiceModel'); + }, + + get preselectedValues() { + return self.tiedChildren.filter(c => c.selected === true && !c.isSkipped).map(c => c.resultValue); + }, + get isLoadedByApi() { return isFF(FF_TAXONOMY_ASYNC) && !!self.apiurl; }, @@ -232,6 +297,25 @@ const Model = types return fromConfig; }, + get selectedItems() { + const full = self.selected.map(path => { + /** @type {TaxonomyItem[]} items */ + let items = self.items; + const levels = []; + + for (const value of path) { + const item = items.find(item => item.path.at(-1) === value); + + levels.push({ label: item?.label ?? value, value }); + items = item?.children ?? []; + } + + return levels; + }); + + return full; + }, + get defaultChildType() { return 'choice'; }, @@ -276,7 +360,14 @@ const Model = types const children = ChildrenSnapshots.get(self.name) ?? []; if (isFF(FF_DEV_3617) && self.store && children.length !== self.children.length) { - setTimeout(() => self.updateChildren()); + if (isFF(FF_TAXONOMY_SELECTED)) { + // we have to update it during config parsing to let other code work + // with correctly added children. + // looks like there are no obstacles to do it in the same tick + self.updateChildren(); + } else { + setTimeout(() => self.updateChildren()); + } } else { self.loading = false; } @@ -288,6 +379,7 @@ const Model = types */ loadItems: flow(function * (path) { if (!self._api) return; + let requestOptions = {}; // will be used only to load children for nested items // to check that item exists and requires loading @@ -313,8 +405,23 @@ const Model = types path?.forEach(p => url.searchParams.append('path', p)); + if (url.username && url.password) { + requestOptions = { + headers: new Headers({ + 'Authorization': `Basic ${btoa(`${url.username}:${url.password}`)}`, + }), + }; + + url.username = ''; + url.password = ''; + } + try { - const res = yield fetch(url); + const res = yield fetch(url, requestOptions); + const { ok, status, statusText } = res; + + if (!ok) throw new Error(`${status} ${statusText}`); + const dataRaw = yield res.json(); // @todo temporary to support deprecated API response format (just array, no items) const data = dataRaw.items ?? dataRaw; @@ -336,8 +443,11 @@ const Model = types self._items = items; } } catch (err) { + const message = messages.ERR_LOADING_HTTP({ attr: 'apiUrl', error: String(err), url: self.apiurl }); + + self.annotationStore.addErrors([errorBuilder.generalError(message)]); + console.error(err); - Infomodal.error(`Failed to load taxonomy "${self.name}" from "${self.apiurl}" by path "${path}".`); } self.loading = false; @@ -351,10 +461,22 @@ const Model = types const children = ChildrenSnapshots.get(self.name) ?? []; if (children.length) { + const root = getRoot(self); + // SharedChoiceStore doesn't call `updateValue()` because it's annotation agnostic, + // so call it here right after Taxonomy is attached + const updateChildrenValue = children => { + children?.map(child => { + child.updateValue?.(root); + updateChildrenValue(child.children); + }); + }; + self._children = children; self.children = [...children]; self.store.unlock(); ChildrenSnapshots.delete(self.name); + + updateChildrenValue(self.children); } self.loading = false; @@ -375,6 +497,10 @@ const Model = types }, onChange(_node, checked) { + // don't remove last label from region if region is selected (so canRemoveItems is false) + // should be checked only for Taxonomy as labbeling tool + if (self.canRemoveItems === false && !checked.length) return; + self.selected = checked.map(s => s.path ?? s); self.maxUsagesReached = self.selected.length >= self.maxusages; self.updateResult(); @@ -423,6 +549,8 @@ const Model = types if (!self.isLoadedByApi) return Super.updateValue?.(store); self._api = parseValue(self.apiurl, store.task.dataObj); + // trying to presign this url if needed and if handler is passed into LSF + self._api = (yield store.presignUrlForProject(self._api)) ?? self._api; yield self.loadItems(); }), @@ -447,7 +575,7 @@ const TaxonomyModel = types.compose('TaxonomyModel', ControlBase, ClassificationBase, TagAttrs, - ...(isFF(FF_DEV_2007_DEV_2008) ? [DynamicChildrenMixin] : []), + DynamicChildrenMixin, AnnotationMixin, RequiredMixin, Model, @@ -461,6 +589,12 @@ const TaxonomyModel = types.compose('TaxonomyModel', ); const HtxTaxonomy = observer(({ item }) => { + // literal "taxonomy" class name is for external styling + const className = [ + styles.taxonomy, + 'taxonomy', + isFF(FF_TAXONOMY_ASYNC) ? styles.taxonomy__new : '', + ].filter(Boolean).join(' '); const visibleStyle = item.perRegionVisible() && item.isVisible ? {} : { display: 'none' }; const options = { showFullPath: item.showfullpath, @@ -471,38 +605,48 @@ const HtxTaxonomy = observer(({ item }) => { minWidth: item.minwidth, dropdownWidth: item.dropdownwidth, placeholder: item.placeholder, + canRemoveItems: item.canRemoveItems, }; + // without full api there will be just one initial loading; + // with full api we should not block UI with spinner on nested requests — + // they are indicated by loading icon on the item itself + const firstLoad = item.isLoadedByApi ? !item.items.length : true; + + if (item.loading && isFF(FF_DEV_3617) && firstLoad) { + return ( +
+
+ +
+
+ ); + } + return ( - // @todo use BEM class names + literal "taxonomy" for external styling -
- {isFF(FF_TAXONOMY_ASYNC) ? ( +
+ {(isFF(FF_TAXONOMY_ASYNC) && !item.legacy) ? ( ) : ( - item.loading && isFF(FF_DEV_3617) ? ( -
- -
- ) : ( - - ) + )}
); diff --git a/src/tags/control/Taxonomy/Taxonomy.styl b/src/tags/control/Taxonomy/Taxonomy.styl index cd8bd344e3..e9dd239222 100644 --- a/src/tags/control/Taxonomy/Taxonomy.styl +++ b/src/tags/control/Taxonomy/Taxonomy.styl @@ -19,3 +19,7 @@ & > div > span display block + + &__new &__loading + margin-top 0 + height 31px diff --git a/src/tags/object/Image/Image.js b/src/tags/object/Image/Image.js index 23461df20a..233cfd4631 100644 --- a/src/tags/object/Image/Image.js +++ b/src/tags/object/Image/Image.js @@ -22,6 +22,7 @@ import { FF_LSDV_4583, FF_LSDV_4583_6, FF_LSDV_4711, + FF_ZOOM_OPTIM, isFF } from '../../../utils/feature-flags'; import { guidGenerator } from '../../../utils/unique'; @@ -69,6 +70,7 @@ const IMAGE_PRELOAD_COUNT = 3; * @meta_description Customize Label Studio with the Image tag to annotate images for computer vision machine learning and data science projects. * @param {string} name - Name of the element * @param {string} value - Data field containing a path or URL to the image + * @param {string} [valueList] - References a variable that holds a list of image URLs * @param {boolean} [smoothing] - Enable smoothing, by default it uses user settings * @param {string=} [width=100%] - Image width * @param {string=} [maxWidth=750px] - Maximum image width @@ -83,11 +85,10 @@ const IMAGE_PRELOAD_COUNT = 3; * @param {boolean} [contrastControl=false] - Show contrast control in toolbar * @param {boolean} [rotateControl=false] - Show rotate control in toolbar * @param {boolean} [crosshair=false] - Show crosshair cursor - * @param {string} [horizontalAlignment=left] - Where to align image horizontally. Can be one of "left", "center" or "right" - * @param {string} [verticalAlignment=top] - Where to align image vertically. Can be one of "top", "center" or "bottom" - * @param {string} [defaultZoom=fit] - Specify the initial zoom of the image within the viewport while preserving it’s ratio. Can be one of "auto", "original" or "fit" - * @param {string} [valuelist] - References a variable that holds a list of image URLs - * @param {string} [crossOrigin=none] - Configures CORS cross domain behavior for this image, either "none", "anonymous", or "use-credentials", similar to [DOM `img` crossOrigin property](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/crossOrigin). + * @param {left|center|right} [horizontalAlignment=left] - Where to align image horizontally. Can be one of "left", "center", or "right" + * @param {top|center|bottom} [verticalAlignment=top] - Where to align image vertically. Can be one of "top", "center", or "bottom" + * @param {auto|original|fit} [defaultZoom=fit] - Specify the initial zoom of the image within the viewport while preserving its ratio. Can be one of "auto", "original", or "fit" + * @param {none|anonymous|use-credentials} [crossOrigin=none] - Configures CORS cross domain behavior for this image, either "none", "anonymous", or "use-credentials", similar to [DOM `img` crossOrigin property](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/crossOrigin). */ const TagAttrs = types.model({ value: types.maybeNull(types.string), @@ -456,6 +457,34 @@ const Model = types.model({ }; }, + get alignmentOffset() { + const offset = { x: 0, y: 0 }; + + if (isFF(FF_ZOOM_OPTIM)) { + switch (self.horizontalalignment) { + case 'center': { + offset.x = (self.containerWidth - self.canvasSize.width) / 2; + break; + } + case 'right': { + offset.x = (self.containerWidth - self.canvasSize.width); + break; + } + } + switch (self.verticalalignment) { + case 'center': { + offset.y = (self.containerHeight - self.canvasSize.height) / 2; + break; + } + case 'bottom': { + offset.y = (self.containerHeight - self.canvasSize.height); + break; + } + } + } + return offset; + }, + get zoomBy() { return parseFloat(self.zoomby); }, @@ -511,6 +540,38 @@ const Model = types.model({ ? Math.max(self.containerWidth / self.naturalHeight, self.containerHeight / self.naturalWidth) : Math.max(self.containerWidth / self.naturalWidth, self.containerHeight / self.naturalHeight); }, + + get viewPortBBoxCoords() { + let width = self.canvasSize.width / self.zoomScale; + let height = self.canvasSize.height / self.zoomScale; + const leftOffset = -self.zoomingPositionX / self.zoomScale; + const topOffset = -self.zoomingPositionY / self.zoomScale; + const rightOffset = self.stageComponentSize.width - (leftOffset + width); + const bottomOffset = self.stageComponentSize.height - (topOffset + height); + const offsets = [leftOffset, topOffset, rightOffset, bottomOffset]; + + if (self.isSideways) { + [width, height] = [height, width]; + } + if (self.rotation) { + const rotateCount = (self.rotation / 90) % 4; + + for (let k = 0; k < rotateCount; k++) { + offsets.push(offsets.shift()); + } + } + const left = offsets[0]; + const top = offsets[1]; + + return { + left, + top, + right: left + width, + bottom: top + height, + width, + height, + }; + }, })) // actions for the tools @@ -1007,7 +1068,7 @@ const Model = types.model({ //sometimes when user zoomed in, annotation was creating a new history. This fix that in case the user has nothing in the history yet if (_historyLength <= 1) { // Don't force unselection of regions during the updateObjects callback from history reinit - setTimeout(() => self.annotation.reinitHistory(false), 0); + setTimeout(() => self.annotation?.reinitHistory(false), 0); } }, @@ -1030,7 +1091,7 @@ const Model = types.model({ self.sizeToAuto(); } // Don't force unselection of regions during the updateObjects callback from history reinit - setTimeout(() => self.annotation.reinitHistory(false), 0); + setTimeout(() => self.annotation?.reinitHistory(false), 0); }, checkLabels() { diff --git a/src/tags/object/Paragraphs/HtxParagraphs.js b/src/tags/object/Paragraphs/HtxParagraphs.js index 9d884dfc51..2ac1ae2c7b 100644 --- a/src/tags/object/Paragraphs/HtxParagraphs.js +++ b/src/tags/object/Paragraphs/HtxParagraphs.js @@ -522,7 +522,7 @@ class HtxParagraphsView extends Component { }, _timeoutDelay); }} /> )} - {this.props.item.contextscroll && ( + {item.contextscroll && (
{ - if (!element || !isFF(FF_LSDV_E_278)) return; + if (!element || (!isFF(FF_LSDV_E_278) || !item.contextscroll)) return; const _animationKeyFrame = element.animate( [{ top: `${start}%` }, { top: '100%' }], @@ -55,7 +55,7 @@ export const Phrases = observer(({ item, playingId, activeRef, setIsInViewport } // this function is used to animate the reading line when user seek audio const setSeekAnimation = useCallback( (isSeeking) => { - if (!isFF(FF_LSDV_E_278)) return; + if (!isFF(FF_LSDV_E_278) || !item.contextscroll) return; const duration = item._value[playingId]?.duration || item._value[playingId]?.end - item._value[playingId]?.start; const endTime = !item._value[playingId]?.end ? item._value[playingId]?.start + item._value[playingId]?.duration : item._value[playingId]?.end; @@ -94,7 +94,7 @@ export const Phrases = observer(({ item, playingId, activeRef, setIsInViewport } }, [playingId]); useEffect(() => { - if (!isFF(FF_LSDV_E_278)) return; + if (!isFF(FF_LSDV_E_278) || !item.contextscroll) return; item.syncHandlers?.set('seek', seek => { item.handleSyncPlay(seek); @@ -122,8 +122,8 @@ export const Phrases = observer(({ item, playingId, activeRef, setIsInViewport } // when user click on play/pause button, the useEffect will be triggered and pause or play the reading line animation useEffect(() => { - if (!isFF(FF_LSDV_E_278)) return; - + if (!isFF(FF_LSDV_E_278) || !item.contextscroll) return; + if (item.playing) animationKeyFrame?.play(); else animationKeyFrame?.pause(); }, [item.playing]); diff --git a/src/tags/object/Paragraphs/model.js b/src/tags/object/Paragraphs/model.js index fd6511c6fc..63a40078d1 100644 --- a/src/tags/object/Paragraphs/model.js +++ b/src/tags/object/Paragraphs/model.js @@ -60,8 +60,8 @@ import styles from './Paragraphs.module.scss'; * @param {string} name - Name of the element * @param {string} value - Data field containing the paragraph content * @param {json|url} [valueType=json] - Whether the data is stored directly in uploaded JSON data or needs to be loaded from a URL - * @param {string} audioUrl - Audio to sync phrases with - * @param {string} [sync] - object name to sync with + * @param {string} [audioUrl] - Audio to sync phrases with + * @param {string} [sync] - Object name to sync with * @param {boolean} [showPlayer=false] - Whether to show audio player above the paragraphs. Ignored if sync object is audio * @param {no|yes} [saveTextResult=yes] - Whether to store labeled text along with the results. By default, doesn't store text for `valueType=url` * @param {none|dialogue} [layout=none] - Whether to use a dialogue-style layout or not diff --git a/src/utils/feature-flags.ts b/src/utils/feature-flags.ts index 91d35edc07..0a758e138a 100644 --- a/src/utils/feature-flags.ts +++ b/src/utils/feature-flags.ts @@ -42,27 +42,15 @@ export const FF_DEV_1621 = 'ff_front_dev_1621_interactive_mode_150222_short'; // New Audio 2.0 UI export const FF_DEV_1713 = 'ff_front_DEV_1713_audio_ui_150222_short'; -// Rework of Choices tag -export const FF_DEV_2007 = 'ff_dev_2007_rework_choices_280322_short'; - -// Add ability to generate children tags from task data -export const FF_DEV_2007_DEV_2008 = 'ff_dev_2007_dev_2008_dynamic_tag_children_250322_short'; - // Clean unnecessary classification areas after deserialization export const FF_DEV_2100 = 'ff_dev_2100_clean_unnecessary_areas_140422_short'; -// Fix preselected choices -export const FF_DEV_2100_A = 'ff_dev_2100_preselected_choices_250422_short'; - // Allow to use html inside