diff --git a/.github/workflows/build_bundle.yml b/.github/workflows/build_bundle.yml index 9bfe15fc26..c07fb56c13 100644 --- a/.github/workflows/build_bundle.yml +++ b/.github/workflows/build_bundle.yml @@ -46,7 +46,7 @@ jobs: run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Configure yarn cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} key: ${{ env.CACHE_NAME_PREFIX }}-${{ runner.os }}-node-${{ env.NODE }}-${{ hashFiles('**/yarn.lock') }} diff --git a/.github/workflows/cicd_pipeline.yml b/.github/workflows/cicd_pipeline.yml index c7ad9d933a..bd91dd9671 100644 --- a/.github/workflows/cicd_pipeline.yml +++ b/.github/workflows/cicd_pipeline.yml @@ -78,5 +78,9 @@ jobs: name: "Lint" if: github.event_name == 'push' || github.event.pull_request.draft == false uses: ./.github/workflows/eslint.yml + permissions: + checks: write + contents: read + pull-requests: write with: sha: ${{ github.event.pull_request.head.sha || github.event.after }} diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 369c08aec0..6509c934ac 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -43,7 +43,7 @@ jobs: run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Configure yarn cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} key: ${{ env.CACHE_NAME_PREFIX }}-${{ runner.os }}-node-${{ env.NODE }}-${{ hashFiles('**/yarn.lock') }} diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index d1a26dc8e2..c6269b20c8 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -12,6 +12,11 @@ env: CACHE_NAME_PREFIX: v3 NODE: '18' +permissions: + checks: write + contents: read + pull-requests: write + jobs: run: name: Run ESLint @@ -40,7 +45,7 @@ jobs: run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Configure yarn cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} key: ${{ env.CACHE_NAME_PREFIX }}-${{ runner.os }}-node-${{ env.NODE }}-${{ hashFiles('**/yarn.lock') }} @@ -57,7 +62,9 @@ jobs: npm list --depth=1 || true - name: Run ESLint - uses: tj-actions/eslint-changed-files@v21 + uses: tj-actions/eslint-changed-files@v23 + env: + REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: config_path: ".eslintrc.js" ignore_path: ".eslintignore" diff --git a/.github/workflows/fun_tests.yml b/.github/workflows/fun_tests.yml index f0b05d0642..006d54a52c 100644 --- a/.github/workflows/fun_tests.yml +++ b/.github/workflows/fun_tests.yml @@ -36,18 +36,6 @@ jobs: id: "cpu-info" run: echo "cores-count=$(cat /proc/cpuinfo | grep processor | wc -l)" >> $GITHUB_OUTPUT - - name: Setup SSH agent - env: - SSH_AUTH_SOCK: /tmp/ssh_agent.sock - SSH_DIR: /home/runner/.ssh - run: | - mkdir $SSH_DIR - ssh-keyscan github.com >> $SSH_DIR/known_hosts - echo "${{ secrets.SSH_PRIVATE_KEY }}" > $SSH_DIR/package_rsa - chmod 600 $SSH_DIR/package_rsa - ssh-agent -a $SSH_AUTH_SOCK > /dev/null - ssh-add $SSH_DIR/package_rsa - - name: Upgrade Yarn run: npm install -g yarn@1.22 @@ -56,7 +44,7 @@ jobs: run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Configure yarn cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} key: ${{ env.CACHE_NAME_PREFIX }}-${{ runner.os }}-node-${{ env.NODE }}-${{ hashFiles('**/yarn.lock') }} @@ -91,8 +79,6 @@ jobs: - name: "Setup Cypress" timeout-minutes: 1 - env: - SSH_AUTH_SOCK: /tmp/ssh_agent.sock run: | set -euo pipefail cd ./tests/functional diff --git a/.github/workflows/git-command.yml b/.github/workflows/git-command.yml index b9185c8377..b9cc7d986c 100644 --- a/.github/workflows/git-command.yml +++ b/.github/workflows/git-command.yml @@ -13,10 +13,16 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 3 steps: - - name: Dump the client payload context - env: - PAYLOAD_CONTEXT: ${{ toJson(github.event.client_payload) }} - run: echo "$PAYLOAD_CONTEXT" + - uses: hmarr/debug-action@v2.1.0 + + - name: Add Workflow link to command comment + uses: peter-evans/create-or-update-comment@v3 + with: + token: ${{ secrets.GIT_PAT }} + repository: ${{ github.event.client_payload.github.payload.repository.full_name }} + comment-id: ${{ github.event.client_payload.github.payload.comment.id }} + body: | + > [Workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) - name: Checkout on chat command uses: actions/checkout@v4 @@ -27,6 +33,13 @@ jobs: submodules: 'recursive' fetch-depth: 0 + - name: Configure git + shell: bash + run: | + set -xeuo pipefail + git config --global user.name '${{ github.event.client_payload.github.actor }}' + git config --global user.email '${{ github.event.client_payload.github.actor }}@users.noreply.github.com' + - name: Check for merge conflict id: check-conflict env: @@ -34,8 +47,6 @@ jobs: shell: bash run: | set -xeuo pipefail - git config --global user.name '${{ github.event.client_payload.github.actor }}' - git config --global user.email '${{ github.event.client_payload.github.actor }}@users.noreply.github.com' echo "merge_conflict=$(git merge-tree $(git merge-base HEAD origin/$SLASH_COMMAND_ARG_BRANCH) origin/$SLASH_COMMAND_ARG_BRANCH HEAD | grep '<<')" >> $GITHUB_OUTPUT - name: Add reaction to command comment on merge conflict @@ -46,9 +57,7 @@ jobs: repository: ${{ github.event.client_payload.github.payload.repository.full_name }} comment-id: ${{ github.event.client_payload.github.payload.comment.id }} body: | - > **Error**: Merge conflict detected, please resolve it using the git command line. - > - > [Workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + > **Error**: Merge conflict detected, please resolve it using the command line. reactions: "-1" - name: Merge branch into current branch @@ -79,8 +88,6 @@ jobs: comment-id: ${{ github.event.client_payload.github.payload.comment.id }} body: | > Already up-to-date. Nothing to commit. - > - > [Workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) reactions: "confused" - name: Add reaction to command comment on success @@ -93,8 +100,6 @@ jobs: body: | > Successfully pushed new changes: > ${{ steps.commit_and_push.outputs.last_commit_msg }} (${{ steps.commit_and_push.outputs.last_commit_sha }}) - > - > [Workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) reactions: "+1" - name: Add reaction to command comment on failure @@ -106,8 +111,6 @@ jobs: comment-id: ${{ github.event.client_payload.github.payload.comment.id }} body: | > **Error**: failed to execute "${{ github.event.client_payload.slash_command.args.unnamed.arg1 }}" command - > - > [Workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) reactions: "-1" help: diff --git a/.github/workflows/jira-command.yml b/.github/workflows/jira-command.yml new file mode 100644 index 0000000000..4ef05b3d3a --- /dev/null +++ b/.github/workflows/jira-command.yml @@ -0,0 +1,103 @@ +name: "/jira command" + +on: + repository_dispatch: + types: [ jira-command ] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.client_payload.github.payload.issue.number }}-${{ github.event.client_payload.slash_command.command }}-${{ github.event.client_payload.slash_command.args.unnamed.arg1 || github.event.client_payload.slash_command.args.all }} + +jobs: + create: + if: ${{ github.event.client_payload.slash_command.args.unnamed.arg1 == 'create' }} + runs-on: ubuntu-latest + timeout-minutes: 3 + steps: + - uses: hmarr/debug-action@v2.1.0 + + - name: Add Workflow link to command comment + uses: peter-evans/create-or-update-comment@v3 + with: + token: ${{ secrets.GIT_PAT }} + repository: ${{ github.event.client_payload.github.payload.repository.full_name }} + comment-id: ${{ github.event.client_payload.github.payload.comment.id }} + body: | + > [Workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + - name: Check user's membership + uses: actions/github-script@v7 + id: check-membership + env: + ACTOR: ${{ github.actor }} + with: + github-token: ${{ secrets.GIT_PAT }} + script: | + const { repo, owner } = context.repo; + const actor = process.env.ACTOR; + const { data: membership } = await github.rest.orgs.getMembershipForUser({ + org: owner, + username: actor, + }); + if (membership.state != "active") { + const error = `Unfortunately you don't have membership in ${owner} organization, Jira Issue was not created`; + core.setOutput("error", error); + core.setFailed(error); + } + + - name: Checkout Actions Hub + uses: actions/checkout@v4 + with: + token: ${{ secrets.GIT_PAT }} + repository: HumanSignal/actions-hub + path: ./.github/actions-hub + + - name: Jira Create Issue + id: jira-create-issue + uses: ./.github/actions-hub/actions/jira-create-issue + with: + jira_server: ${{ vars.JIRA_SERVER }} + jira_username: ${{ secrets.JIRA_USERNAME }} + jira_token: ${{ secrets.JIRA_TOKEN }} + summary: ${{ github.event.client_payload.github.payload.issue.title }} + description: ${{ github.event.client_payload.github.payload.issue.body }} + project: ${{ github.event.client_payload.slash_command.args.unnamed.arg3 || 'TRIAG' }} + type: ${{ github.event.client_payload.slash_command.args.unnamed.arg2 || 'task' }} + + - name: Add reaction to command comment on success + uses: peter-evans/create-or-update-comment@v3 + with: + token: ${{ secrets.GIT_PAT }} + repository: ${{ github.event.client_payload.github.payload.repository.full_name }} + comment-id: ${{ github.event.client_payload.github.payload.comment.id }} + body: | + > Jira issue [${{ steps.jira-create-issue.outputs.key }}](${{ steps.jira-create-issue.outputs.link }}) is created + reactions: "+1" + + - name: Add reaction to command comment on failure + uses: peter-evans/create-or-update-comment@v3 + if: failure() + with: + token: ${{ secrets.GIT_PAT }} + repository: ${{ github.event.client_payload.github.payload.repository.full_name }} + comment-id: ${{ github.event.client_payload.github.payload.comment.id }} + body: | + > **Error**: failed to execute "${{ github.event.client_payload.slash_command.args.unnamed.arg1 }}" command + > ${{ steps.check-membership.outputs.error }} + reactions: "-1" + + help: + if: github.event.client_payload.slash_command.args.unnamed.arg1 == 'help' || !contains(fromJson('["create"]'), github.event.client_payload.slash_command.args.unnamed.arg1) + runs-on: ubuntu-latest + timeout-minutes: 1 + steps: + - name: Update comment + uses: peter-evans/create-or-update-comment@v3 + with: + token: ${{ secrets.GIT_PAT }} + repository: ${{ github.event.client_payload.github.payload.repository.full_name }} + comment-id: ${{ github.event.client_payload.github.payload.comment.id }} + body: | + > Command | Description + > --- | --- + > /jira create [task|bug|story] `PROJECT` | Create a Jira issue in project `PROJECT` + reaction-type: hooray diff --git a/.github/workflows/release-set-version.yml b/.github/workflows/release-set-version.yml index 221b36b5cf..6a58412c95 100644 --- a/.github/workflows/release-set-version.yml +++ b/.github/workflows/release-set-version.yml @@ -31,7 +31,7 @@ jobs: node-version: "${{ env.NODE }}" - name: Cache node modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.npm key: npm-${{ env.CACHE_NAME_PREFIX }}-${{ runner.os }}-node-${{ env.NODE }}-jsdoc-to-markdown @@ -46,7 +46,7 @@ jobs: - name: Get GitHub user details id: get-github-user - uses: actions/github-script@v6 + uses: actions/github-script@v7 env: ACTOR_USERNAME: ${{ github.event.sender.login }} with: diff --git a/.github/workflows/slash-command-dispatch.yml b/.github/workflows/slash-command-dispatch.yml index 8c69350f50..eba62df28c 100644 --- a/.github/workflows/slash-command-dispatch.yml +++ b/.github/workflows/slash-command-dispatch.yml @@ -1,4 +1,4 @@ -name: "/Slash Command Dispatch" +name: Slash Command Dispatch on: issue_comment: types: [created] @@ -7,6 +7,10 @@ env: commands_list: | help git + jira + + issue_commands_list: | + jira jobs: slashCommandDispatch: @@ -18,41 +22,60 @@ jobs: - name: 'Validate command' id: determine_command - uses: actions/github-script@v6 + uses: actions/github-script@v7 env: COMMANDS_LIST: ${{ env.commands_list }} + ISSUE_COMMANDS_LIST: ${{ env.issue_commands_list }} with: github-token: ${{ secrets.GIT_PAT }} script: | const body = context.payload.comment.body.toLowerCase().trim() const commands_list = process.env.COMMANDS_LIST.split("\n") + const issue_commands_list = process.env.ISSUE_COMMANDS_LIST.split("\n") console.log("Detected PR comment: " + body) console.log("Commands list: " + commands_list) + console.log("Issue commands list: " + issue_commands_list) commandArray = body.split(/\s+/) const contextCommand = commandArray[0].split('/')[1].trim(); console.log("contextCommand: " + contextCommand) core.setOutput('command_state', 'known') + core.setOutput('is_issue_command', 'false') if (! commands_list.includes(contextCommand)) { core.setOutput('command_state', 'unknown') core.setOutput('command', contextCommand) } + if (issue_commands_list.includes(contextCommand)) { + core.setOutput('is_issue_command', 'true') + } + + - name: Slash Command Dispatch for Issues + id: scd_issues + if: ${{ steps.determine_command.outputs.command_state != 'unknown' && steps.determine_command.outputs.is_issue_command == 'true' }} + uses: peter-evans/slash-command-dispatch@v3 + with: + token: ${{ secrets.GIT_PAT }} + reaction-token: ${{ secrets.GIT_PAT }} + issue-type: "issue" + reactions: true + commands: ${{ env.issue_commands_list }} - - name: Slash Command Dispatch - id: scd - if: steps.determine_command.outputs.command_state != 'unknown' + - name: Slash Command Dispatch for PRs + id: scd_prs + if: ${{ steps.determine_command.outputs.command_state != 'unknown' && steps.determine_command.outputs.is_issue_command != 'true' }} uses: peter-evans/slash-command-dispatch@v3 with: token: ${{ secrets.GIT_PAT }} + reaction-token: ${{ secrets.GIT_PAT }} issue-type: "pull-request" reactions: true commands: ${{ env.commands_list }} - name: Edit comment with error message - if: steps.determine_command.outputs.command_state == 'unknown' + if: ${{ steps.determine_command.outputs.command_state == 'unknown' }} uses: peter-evans/create-or-update-comment@v3 with: comment-id: ${{ github.event.comment.id }} body: | - > '/${{ steps.determine_command.outputs.command }}' is unknown command. + > '/${{ steps.determine_command.outputs.command }}' is an unknown command. > See '/help' reactions: eyes, confused diff --git a/.github/workflows/sync-pr-ls.yml b/.github/workflows/sync-pr-ls.yml index a6ba34754d..f59115c231 100644 --- a/.github/workflows/sync-pr-ls.yml +++ b/.github/workflows/sync-pr-ls.yml @@ -1,4 +1,4 @@ -name: Sync PR LS +name: 'Follow Merge: Sync PR LSO' on: pull_request_target: @@ -16,7 +16,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.head_ref }} env: - DOWNSTREAM_REPO: label-studio + DOWNSTREAM_REPOSITORY: "label-studio" + DOWNSTREAM_EVENT_TYPE: "upstream_repo_update" jobs: sync: @@ -27,20 +28,30 @@ jobs: - uses: hmarr/debug-action@v2.1.0 - name: Sync PR - uses: actions/github-script@v6 + uses: actions/github-script@v7 id: sync-pr env: TITLE: ${{ github.event.pull_request.title }} + HEAD_REF: ${{ github.head_ref }} + BASE_REF: ${{ github.base_ref }} + PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + DOWNSTREAM_REPOSITORY: ${{ env.DOWNSTREAM_REPOSITORY }} + DOWNSTREAM_EVENT_TYPE: ${{ env.DOWNSTREAM_EVENT_TYPE }} with: github-token: ${{ secrets.GIT_PAT }} script: | const { repo, owner } = context.repo; - const [pr_owner, pr_repo] = '${{ github.event.pull_request.head.repo.full_name || github.repository }}'.split('/'); - let event_action = '${{ github.event.action }}' - let commit_sha = '${{ github.event.pull_request.head.sha }}' + const pr_head_repository = process.env.PR_HEAD_REPOSITORY; + const downstream_repository = process.env.DOWNSTREAM_REPOSITORY; + const downstream_event_type = process.env.DOWNSTREAM_EVENT_TYPE; + const [pr_owner, pr_repo] = pr_head_repository.split('/'); + const head_ref = process.env.HEAD_REF; + const base_ref = process.env.BASE_REF; + let event_action = '${{ github.event.action }}'; + let commit_sha = '${{ github.event.pull_request.head.sha }}'; if (${{ github.event.pull_request.merged }}) { - event_action = 'merged' - commit_sha = '${{ github.sha }}' + event_action = 'merged'; + commit_sha = '${{ github.sha }}'; } const getCommitResponse = await github.rest.repos.getCommit({ owner: pr_owner, @@ -49,11 +60,11 @@ jobs: }); const result = await github.rest.repos.createDispatchEvent({ owner: owner, - repo: '${{ env.DOWNSTREAM_REPO }}', - event_type: 'upstream_repo_update', + repo: downstream_repository, + event_type: downstream_event_type, client_payload: { - branch_name: '${{ github.head_ref }}', - base_branch_name: '${{ github.base_ref }}', + branch_name: head_ref, + base_branch_name: base_ref, repo_name: '${{ github.repository }}', commit_sha : commit_sha, title: process.env.TITLE, diff --git a/.github/workflows/sync-pr-lse.yml b/.github/workflows/sync-pr-lse.yml index b8468d6acd..fd781f3b05 100644 --- a/.github/workflows/sync-pr-lse.yml +++ b/.github/workflows/sync-pr-lse.yml @@ -1,4 +1,4 @@ -name: Sync PR LSE +name: 'Follow Merge: Sync PR LSE' on: pull_request_target: @@ -16,7 +16,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.head_ref }} env: - DOWNSTREAM_REPO: label-studio-enterprise + DOWNSTREAM_REPOSITORY: "label-studio-enterprise" + DOWNSTREAM_EVENT_TYPE: "upstream_repo_update" jobs: sync: @@ -27,7 +28,7 @@ jobs: - uses: hmarr/debug-action@v2.1.0 - name: Check user's membership - uses: actions/github-script@v6 + uses: actions/github-script@v7 id: check-membership with: github-token: ${{ secrets.GIT_PAT }} @@ -44,7 +45,7 @@ jobs: - name: Notify user on failure if: steps.check-membership.outputs.result == 'false' - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: github-token: ${{ secrets.GIT_PAT }} script: | @@ -56,26 +57,36 @@ jobs: body: [ 'Hi @${{ github.actor }}!', '', - `Unfortunately you don't have membership in ${owner} organization, your PR wasn't synced with ${owner}/${{ env.DOWNSTREAM_REPO }}.` + `Unfortunately you don't have membership in ${owner} organization, your PR wasn't synced with ${owner}/${{ env.DOWNSTREAM_REPOSITORY }}.` ].join('\n') }); - name: Sync PR - uses: actions/github-script@v6 + uses: actions/github-script@v7 if: steps.check-membership.outputs.result == 'true' id: sync-pr env: TITLE: ${{ github.event.pull_request.title }} + HEAD_REF: ${{ github.head_ref }} + BASE_REF: ${{ github.base_ref }} + PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + DOWNSTREAM_REPOSITORY: ${{ env.DOWNSTREAM_REPOSITORY }} + DOWNSTREAM_EVENT_TYPE: ${{ env.DOWNSTREAM_EVENT_TYPE }} with: github-token: ${{ secrets.GIT_PAT }} script: | const { repo, owner } = context.repo; - const [pr_owner, pr_repo] = '${{ github.event.pull_request.head.repo.full_name || github.repository }}'.split('/'); - let event_action = '${{ github.event.action }}' - let commit_sha = '${{ github.event.pull_request.head.sha }}' + const pr_head_repository = process.env.PR_HEAD_REPOSITORY; + const downstream_repository = process.env.DOWNSTREAM_REPOSITORY; + const downstream_event_type = process.env.DOWNSTREAM_EVENT_TYPE; + const [pr_owner, pr_repo] = pr_head_repository.split('/'); + const head_ref = process.env.HEAD_REF; + const base_ref = process.env.BASE_REF; + let event_action = '${{ github.event.action }}'; + let commit_sha = '${{ github.event.pull_request.head.sha }}'; if (${{ github.event.pull_request.merged }}) { - event_action = 'merged' - commit_sha = '${{ github.sha }}' + event_action = 'merged'; + commit_sha = '${{ github.sha }}'; } const getCommitResponse = await github.rest.repos.getCommit({ owner: pr_owner, @@ -84,11 +95,11 @@ jobs: }); const result = await github.rest.repos.createDispatchEvent({ owner: owner, - repo: '${{ env.DOWNSTREAM_REPO }}', - event_type: 'upstream_repo_update', + repo: downstream_repository, + event_type: downstream_event_type, client_payload: { - branch_name: '${{ github.head_ref }}', - base_branch_name: '${{ github.base_ref }}', + branch_name: head_ref, + base_branch_name: base_ref, repo_name: '${{ github.repository }}', commit_sha : commit_sha, title: process.env.TITLE, diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 63f50b9868..9147df2b35 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -40,7 +40,7 @@ jobs: run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Configure yarn cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} key: ${{ env.CACHE_NAME_PREFIX }}-${{ runner.os }}-node-${{ env.NODE }}-${{ hashFiles('**/yarn.lock') }} diff --git a/e2e/tests/regression-tests/image-ctrl-drawing.test.js b/e2e/tests/regression-tests/image-ctrl-drawing.test.js index d67580b6e6..01e5fc3f35 100644 --- a/e2e/tests/regression-tests/image-ctrl-drawing.test.js +++ b/e2e/tests/regression-tests/image-ctrl-drawing.test.js @@ -206,6 +206,9 @@ Scenario('How it works without ctrl', async function({ I, LabelStudio, AtSidebar for (const regionPair of regionPairs) { const [outerRegion, innerRegion] = regionPair; + // Brush is not relevant in this case anymore (it will not interact with other regions) + if (innerRegion.shape === 'Brush') continue; + LabelStudio.init(params); AtImageView.waitForImage(); AtSidebar.seeRegions(0); diff --git a/e2e/yarn.lock b/e2e/yarn.lock index 784341a7d6..bcc5ac0d38 100644 --- a/e2e/yarn.lock +++ b/e2e/yarn.lock @@ -17,6 +17,14 @@ dependencies: "@babel/highlight" "^7.16.7" +"@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + "@babel/compat-data@^7.17.10": version "7.17.10" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.10.tgz#711dc726a492dfc8be8220028b1b92482362baab" @@ -52,6 +60,16 @@ "@jridgewell/gen-mapping" "^0.3.0" jsesc "^2.5.1" +"@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-compilation-targets@^7.18.2": version "7.18.2" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.2.tgz#67a85a10cbd5fc7f1457fec2e7f45441dc6c754b" @@ -62,25 +80,30 @@ browserslist "^4.20.2" semver "^6.3.0" -"@babel/helper-environment-visitor@^7.16.7", "@babel/helper-environment-visitor@^7.18.2": +"@babel/helper-environment-visitor@^7.16.7": version "7.18.2" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.2.tgz#8a6d2dedb53f6bf248e31b4baf38739ee4a637bd" integrity sha512-14GQKWkX9oJzPiQQ7/J36FTXcD4kSp8egKjO9nINlSKiHITRA9q/R74qu8S9xlc/b/yjsJItQUeeh3xnGN0voQ== -"@babel/helper-function-name@^7.17.9": - version "7.17.9" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz#136fcd54bc1da82fcb47565cf16fd8e444b1ff12" - integrity sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg== +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== dependencies: - "@babel/template" "^7.16.7" - "@babel/types" "^7.17.0" + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" -"@babel/helper-hoist-variables@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" - integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== dependencies: - "@babel/types" "^7.16.7" + "@babel/types" "^7.22.5" "@babel/helper-module-imports@^7.16.7": version "7.16.7" @@ -117,11 +140,28 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + "@babel/helper-validator-identifier@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/helper-validator-option@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" @@ -145,11 +185,25 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/parser@^7.16.7", "@babel/parser@^7.18.0", "@babel/parser@^7.8.3": version "7.18.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.4.tgz#6774231779dd700e0af29f6ad8d479582d7ce5ef" integrity sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow== +"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== + "@babel/template@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" @@ -159,23 +213,32 @@ "@babel/parser" "^7.16.7" "@babel/types" "^7.16.7" -"@babel/traverse@^7.18.0", "@babel/traverse@^7.18.2": - version "7.18.2" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.2.tgz#b77a52604b5cc836a9e1e08dca01cba67a12d2e8" - integrity sha512-9eNwoeovJ6KH9zcCNnENY7DMFwTU9JdGCFtqNLfUAqtUHRCOsTOqWoffosP8vKmNYeSBUv3yVJXjfd8ucwOjUA== +"@babel/template@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== dependencies: - "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.18.2" - "@babel/helper-environment-visitor" "^7.18.2" - "@babel/helper-function-name" "^7.17.9" - "@babel/helper-hoist-variables" "^7.16.7" - "@babel/helper-split-export-declaration" "^7.16.7" - "@babel/parser" "^7.18.0" - "@babel/types" "^7.18.2" + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + +"@babel/traverse@^7.18.0", "@babel/traverse@^7.18.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.16.7", "@babel/types@^7.17.0", "@babel/types@^7.18.0", "@babel/types@^7.18.2": +"@babel/types@^7.16.7", "@babel/types@^7.18.0", "@babel/types@^7.18.2": version "7.18.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.4.tgz#27eae9b9fd18e9dccc3f9d6ad051336f307be354" integrity sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw== @@ -183,6 +246,15 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@codeceptjs/configure@^0.6.2": version "0.6.2" resolved "https://registry.yarnpkg.com/@codeceptjs/configure/-/configure-0.6.2.tgz#0403b38e5224622a2778ea2cf28c965a1bbaf0f4" @@ -338,21 +410,53 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/resolve-uri@^3.0.3": version "3.0.7" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz#30cd49820a962aff48c8fffc5cd760151fca61fe" integrity sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + "@jridgewell/set-array@^1.0.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.1.tgz#36a6acc93987adcf0ba50c66908bd0b70de8afea" integrity sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ== +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.13" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c" integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w== +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17": + version "0.3.20" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" + integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9": version "0.3.13" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz#dcfe3e95f224c8fe97a87a5235defec999aa92ea" diff --git a/package.json b/package.json index 340729097a..7db0eee5f5 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "@martel/audio-file-decoder": "2.3.15", "@thi.ng/rle-pack": "^2.1.6", "@types/react-beautiful-dnd": "^13.1.3", + "@types/sanitize-html": "^2.9.5", "babel-plugin-istanbul": "^6.1.1", "babel-preset-react-app": "^9.1.1", "d3": "^5.16.0", @@ -100,7 +101,8 @@ "react-beautiful-dnd": "^13.1.1", "react-konva-utils": "^0.2.0", "react-virtualized-auto-sizer": "^1.0.6", - "react-window": "^1.8.6" + "react-window": "^1.8.6", + "sanitize-html": "^2.11.0" }, "devDependencies": { "@babel/core": "7.23.2", diff --git a/src/LabelStudio.js b/src/LabelStudio.js index f0f6c6fd5e..29541ec5b7 100644 --- a/src/LabelStudio.js +++ b/src/LabelStudio.js @@ -79,6 +79,8 @@ export class LabelStudio { }; const clearRenderedApp = () => { + if (!rootElement.childNodes?.length) return; + const childNodes = [...rootElement.childNodes]; // cleanDomAfterReact needs this key to be sure that cleaning affects only current react subtree const reactKey = findReactKey(childNodes[0]); diff --git a/src/common/Dropdown/DropdownComponent.tsx b/src/common/Dropdown/DropdownComponent.tsx index 5028423a21..d32bc64012 100644 --- a/src/common/Dropdown/DropdownComponent.tsx +++ b/src/common/Dropdown/DropdownComponent.tsx @@ -2,7 +2,7 @@ import { cloneElement, CSSProperties, forwardRef, MouseEvent, useCallback, useCo import { createPortal } from 'react-dom'; import { useFullscreen } from '../../hooks/useFullscreen'; import { Block, cn } from '../../utils/bem'; -import { alignElements } from '../../utils/dom'; +import { alignElements, ElementAlignment } from '../../utils/dom'; import { aroundTransition } from '../../utils/transition'; import './Dropdown.styl'; import { DropdownContext } from './DropdownContext'; @@ -21,6 +21,7 @@ export interface DropdownRef { export interface DropdownProps { animated?: boolean; visible?: boolean; + alignment?: ElementAlignment; enabled?: boolean; inline?: boolean; className?: string; @@ -51,7 +52,7 @@ export const Dropdown = forwardRef(({ const calculatePosition = useCallback(() => { const dropdownEl = dropdown.current!; const parent = (triggerRef?.current ?? dropdownEl.parentNode) as HTMLElement; - const { left, top } = alignElements(parent!, dropdownEl, 'bottom-left'); + const { left, top } = alignElements(parent!, dropdownEl, props.alignment || 'bottom-left'); setOffset({ left, top }); }, [triggerRef, minIndex]); diff --git a/src/components/AnnotationTab/AnnotationTab.js b/src/components/AnnotationTab/AnnotationTab.js index 1abe2e0ee6..c78f114872 100644 --- a/src/components/AnnotationTab/AnnotationTab.js +++ b/src/components/AnnotationTab/AnnotationTab.js @@ -54,6 +54,7 @@ export const AnnotationTab = observer(({ store }) => { diff --git a/src/components/App/App.js b/src/components/App/App.js index 972b0d2a4e..f12b1a8ce9 100644 --- a/src/components/App/App.js +++ b/src/components/App/App.js @@ -48,6 +48,7 @@ import { FF_DEV_1170, FF_DEV_3873, FF_LSDV_4620_3_ML, isFF } from '../../utils/f import { Annotation } from './Annotation'; import { Button } from '../../common/Button/Button'; import { reactCleaner } from '../../utils/reactCleaner'; +import { sanitizeHtml } from '../../utils/html'; /** * App @@ -143,7 +144,7 @@ class App extends Component { {this.renderRelations(as.selected)} {(!isFF(FF_DEV_3873)) && getRoot(as).hasInterface('infobar') && this._renderInfobar(as)} - {as.selected.onlyTextObjects === false && ( + {as.selected.hasSuggestionsSupport && ( )} @@ -241,7 +242,7 @@ class App extends Component { <> {store.showingDescription && ( -
+
)} @@ -263,7 +264,7 @@ class App extends Component { panelsHidden={viewingAll} currentEntity={as.selectedHistory ?? as.selected} regions={as.selected.regionStore} - showComments={!store.hasInterface('annotations:comments')} + showComments={store.hasInterface('annotations:comments')} focusTab={store.commentStore.tooltipMessage ? 'comments' : null} > {mainContent} diff --git a/src/components/App/Grid.js b/src/components/App/Grid.js index 1fea7bbef9..e53cccba8d 100644 --- a/src/components/App/Grid.js +++ b/src/components/App/Grid.js @@ -21,6 +21,11 @@ This triggers next rerender with next annotation until all the annotations are r class Item extends Component { componentDidMount() { Promise.all(this.props.annotation.objects.map(o => { + // as the image has lazy load, and the image is not being added to the viewport + // until it's loaded we need to skip the validation assuming that it's always ready, + // otherwise we'll get a blank canvas + if (o.type === 'image') return Promise.resolve(); + return o.isReady ? Promise.resolve(o.isReady) : new Promise(resolve => { diff --git a/src/components/BottomBar/Controls.js b/src/components/BottomBar/Controls.js index 0104de6d04..6b6922192f 100644 --- a/src/components/BottomBar/Controls.js +++ b/src/components/BottomBar/Controls.js @@ -153,11 +153,10 @@ export const Controls = controlsInjector(observer(({ store, history, annotation const isDisabled = disabled || submitDisabled; const useExitOption = !isDisabled && isNotQuickView; - const SubmitOption = ({ isUpdate, onClickMethod }) => { return ( + + ); +}; + +/** + * Defines a column component used by the DragDropBoard component. Each column contains items + * that can be reordered by dragging. + */ const Column = (props: ColumnProps) => { const { column, items, readonly } = props; + const [collapsible] = useContext(CollapsedContext); + + const title = collapsible + ? + :

{column.title}

; return (
-

{column.title}

+ {title} {provided => (
diff --git a/src/components/Ranker/Item.tsx b/src/components/Ranker/Item.tsx index 7ae1b91d87..54d6481bbf 100644 --- a/src/components/Ranker/Item.tsx +++ b/src/components/Ranker/Item.tsx @@ -1,8 +1,10 @@ -import { useMemo } from 'react'; +import { useContext, useMemo } from 'react'; import { Draggable } from 'react-beautiful-dnd'; import { sanitizeHtml } from '../../utils/html'; import { InputItem } from './createData'; +import { CollapsedContext } from './Ranker'; + import styles from './Ranker.module.scss'; interface ItemProps { @@ -20,6 +22,14 @@ const Item = (props: ItemProps) => { // @todo document html parameter later after proper tests const html = useMemo(() => item.html ? sanitizeHtml(item.html) : '', [item.html]); + const [collapsible, collapsedMap, toggleCollapsed] = useContext(CollapsedContext); + const collapsed = collapsedMap[item.id] ?? false; + const toggle = collapsible + ? () => toggleCollapsed(item.id, !collapsed) + : undefined; + const classNames = [styles.item, 'htx-ranker-item']; + + if (collapsible) classNames.push(collapsed ? styles.collapsed : styles.expanded); return ( @@ -29,11 +39,11 @@ const Item = (props: ItemProps) => { {...provided.draggableProps} {...provided.dragHandleProps} style={{ ...provided.draggableProps.style }} - className={[styles.item, 'htx-ranker-item'].join(' ')} + className={classNames.join(' ')} ref={provided.innerRef} data-ranker-id={item.id} > - {item.title &&

{item.title}

} + {item.title &&

{item.title}

} {item.body &&

{item.body}

} {item.html &&

}

{item.id}

diff --git a/src/components/Ranker/Ranker.module.scss b/src/components/Ranker/Ranker.module.scss index a8e25eef27..12b7871dd2 100644 --- a/src/components/Ranker/Ranker.module.scss +++ b/src/components/Ranker/Ranker.module.scss @@ -16,7 +16,7 @@ border-radius: 2px; display: flex; flex-direction: column; - align-items: center; + align-items: stretch; overflow-y: scroll; [data-rbd-droppable-id] { @@ -27,6 +27,32 @@ & + & { margin-left: var(--ranker-gap); } + + .columnTitle { + display: flex; + + button { + margin-left: auto; + background: none; + border: none; + font-size: 0.75em; + width: 2em; + cursor: pointer; + } + + &.expanded button span::after { + content: '△'; + } + &.expanded button:hover span::after { + content: '▲'; + } + &.collapsed button span::after { + content: '▽'; + } + &.collapsed button:hover span::after { + content: '▼'; + } + } } .item { @@ -38,12 +64,35 @@ border-radius: 2px; background-color: #f5f5f5; color: black; - max-width: 400px; } .itemLine { margin: 0; } +.itemTitle { + margin: 0; + display: flex; + cursor: pointer; +} + +.item.collapsed > *:not(.itemTitle) { + display: none; +} + +.item.expanded > .itemTitle::after { + content: '△'; + margin-left: auto; +} +.item.expanded > .itemTitle:hover::after { + content: '▲'; +} +.item.collapsed > .itemTitle::after { + content: '▽'; + margin-left: auto; +} +.item.collapsed > .itemTitle:hover::after { + content: '▼'; +} .dropArea { min-height: 50%; diff --git a/src/components/Ranker/Ranker.tsx b/src/components/Ranker/Ranker.tsx index ba602e1339..015dba907f 100644 --- a/src/components/Ranker/Ranker.tsx +++ b/src/components/Ranker/Ranker.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { createContext, useCallback, useEffect, useState } from 'react'; import { DragDropContext, DropResult } from 'react-beautiful-dnd'; import Column from './Column'; @@ -10,11 +10,30 @@ interface BoardProps { inputData: NewBoardData; handleChange?: (ids: Record) => void; readonly?: boolean; + collapsible?: boolean; } +type CollapsedMap = Record; +type CollapsedContextType = [ + boolean, + CollapsedMap, + (idOrIds: string | string[], value: boolean) => void +]; + +const CollapsedContext = createContext([true, {}, (_id, _value) => {}]); // Component for a drag and drop board with 1+ columns -const Ranker = ({ inputData, handleChange, readonly }: BoardProps) => { +const Ranker = ({ inputData, handleChange, readonly, collapsible = true }: BoardProps) => { const [data, setData] = useState(inputData); + // items in different columns are different components, so collapsed state should be stored + // separately; also it's better to not mutate items itself, so here is the map + const [collapsed, setCollapsed] = useState({}); + // array of ids is used by columns + const toggleCollapsed = useCallback((idOrIds: string | string[], value: boolean) => { + const ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds]; + const values = ids.reduce((acc, id) => ({ ...acc, [id]: value }), {}); + + setCollapsed(c => ({ ...c, ...values })); + }, []); // Update data when inputData changes useEffect(() => { @@ -30,20 +49,20 @@ const Ranker = ({ inputData, handleChange, readonly }: BoardProps) => { return; } - //handle reorder when item was dragged to a new position - //determine which column item was moved from + // handle reorder when item was dragged to a new position + // determine which column item was moved from const startCol = data.columns.find(col => col.id === source.droppableId); const endCol = data.columns.find(col => col.id === destination.droppableId); if (startCol === endCol) { - //get original items list + // get original items list const newCol = [...data.itemIds[source.droppableId]]; - //reorder items list + // reorder items list newCol.splice(source.index, 1); newCol.splice(destination.index, 0, draggableId); - //update state + // update state const newItemIds = { ...data.itemIds, [source.droppableId]: newCol, @@ -55,12 +74,12 @@ const Ranker = ({ inputData, handleChange, readonly }: BoardProps) => { }; setData(newData); - //update results + // update results handleChange ? handleChange(newItemIds) : null; return; } - //handle case when moving from one column to a different column + // handle case when moving from one column to a different column const startItemIds = [...data.itemIds[source.droppableId]]; startItemIds.splice(source.index, 1); @@ -86,7 +105,7 @@ const Ranker = ({ inputData, handleChange, readonly }: BoardProps) => { }; return ( - <> +
<> @@ -98,8 +117,9 @@ const Ranker = ({ inputData, handleChange, readonly }: BoardProps) => {
- +
); }; +export { CollapsedContext }; export default Ranker; diff --git a/src/components/RelationsOverlay/NodesConnector.js b/src/components/RelationsOverlay/NodesConnector.js index aadc16fa4f..64b2dbf988 100644 --- a/src/components/RelationsOverlay/NodesConnector.js +++ b/src/components/RelationsOverlay/NodesConnector.js @@ -5,7 +5,7 @@ import { RelationShape } from './RelationShape'; import { createPropertyWatcher, DOMWatcher } from './watchers'; const parentImagePropsWatch = { - parent: ['zoomScale', 'zoomingPositionX', 'zoomingPositionY', 'rotation'], + parent: ['zoomScale', 'zoomingPositionX', 'zoomingPositionY', 'rotation', 'currentImage'], }; const obtainWatcher = node => { diff --git a/src/components/RelationsOverlay/RelationsOverlay.js b/src/components/RelationsOverlay/RelationsOverlay.js index 212bad482f..882e9d910f 100644 --- a/src/components/RelationsOverlay/RelationsOverlay.js +++ b/src/components/RelationsOverlay/RelationsOverlay.js @@ -171,6 +171,7 @@ const RelationItemObserver = observer(({ relation, startNode, endNode, visible, endNode={endNode} direction={relation.direction} visible={visibility} + labels={relation.selectedValues} {...rest} /> ) : null; @@ -229,7 +230,6 @@ class RelationsOverlay extends PureComponent { rootRef={this.rootNode} startNode={relation.node1} endNode={relation.node2} - labels={relation.relations?.selectedValues()} dimm={hasHighlight && !highlighted} highlight={highlighted} visible={highlighted || visible} diff --git a/src/components/Settings/Settings.js b/src/components/Settings/Settings.js index c62930d6ae..840f82d355 100644 --- a/src/components/Settings/Settings.js +++ b/src/components/Settings/Settings.js @@ -82,7 +82,7 @@ const GeneralSettings = observer(({ store }) => { {editorSettingsKeys.map((obj, index) => { return ( - + {isFF(FF_DEV_3873) ? ( <> diff --git a/src/components/Settings/Settings.styl b/src/components/Settings/Settings.styl index 3bf2ff098e..bf41efe5ac 100644 --- a/src/components/Settings/Settings.styl +++ b/src/components/Settings/Settings.styl @@ -16,6 +16,7 @@ $settings__title .settings__field display flex align-items flex-start + cursor pointer .settings__field + .settings__field margin-top 16px .settings__label diff --git a/src/components/SidePanels/DetailsPanel/DetailsPanel.tsx b/src/components/SidePanels/DetailsPanel/DetailsPanel.tsx index 21a2ed51c5..e3c8cc7f6f 100644 --- a/src/components/SidePanels/DetailsPanel/DetailsPanel.tsx +++ b/src/components/SidePanels/DetailsPanel/DetailsPanel.tsx @@ -61,7 +61,7 @@ const CommentsTab: FC = inject('store')(observer(({ store }) => { - + @@ -169,6 +169,7 @@ const GeneralPanel: FC = inject('store')(observer(({ store, currentEntity } diff --git a/src/components/SidePanels/DetailsPanel/Relations.styl b/src/components/SidePanels/DetailsPanel/Relations.styl index 29a5678825..f53a2b3228 100644 --- a/src/components/SidePanels/DetailsPanel/Relations.styl +++ b/src/components/SidePanels/DetailsPanel/Relations.styl @@ -31,6 +31,7 @@ &__icon width 24px + min-height 24px display flex flex none position relative diff --git a/src/components/SidePanels/DetailsPanel/Relations.tsx b/src/components/SidePanels/DetailsPanel/Relations.tsx index 322405803a..c409d2f237 100644 --- a/src/components/SidePanels/DetailsPanel/Relations.tsx +++ b/src/components/SidePanels/DetailsPanel/Relations.tsx @@ -56,9 +56,9 @@ const RelationItem: FC<{relation: any}> = observer(({ relation }) => { const { direction } = relation; switch (direction) { - case 'left': return ; - case 'right': return ; - case 'bi': return ; + case 'left': return ; + case 'right': return ; + case 'bi': return ; default: return null; } }, [relation.direction]); @@ -92,6 +92,7 @@ const RelationItem: FC<{relation: any}> = observer(({ relation }) => { {(hovered || relation.showMeta) && relation.hasRelations && ( )} {hovered && ( -