From 9f7186079d6b4e5ef445aee9db174ab909835a8a Mon Sep 17 00:00:00 2001 From: Jim Grady Date: Wed, 1 Nov 2023 14:10:24 -0400 Subject: [PATCH 1/7] Dependabot Updates for November 2023 (#2773) * Bump dotnet/sdk in /Backend * Bump dotnet/aspnet in /Backend * Bump Microsoft.AspNetCore.Authentication.JwtBearer in /Backend * Bump eslint-plugin-import from 2.28.1 to 2.29.0 * Bump @types/crypto-js from 4.1.2 to 4.1.3 * Bump @mui/material from 5.14.12 to 5.14.16 * Bump @types/nspell from 2.1.3 to 2.1.5 * Bump @mui/styles from 5.14.3 to 5.14.16 * Bump MongoDB.Driver from 2.21.0 to 2.22.0 in /Backend * Bump actions/checkout from 4.1.0 to 4.1.1 * Bump actions/setup-node from 3.8.1 to 4.0.0 * Bump github/codeql-action from 2.22.1 to 2.22.5 * Bump ossf/scorecard-action from 2.3.0 to 2.3.1 * Update python dependencies * Update license reports --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/backend.yml | 14 +- .github/workflows/codeql.yml | 8 +- .github/workflows/database.yml | 2 +- .github/workflows/deploy_qa.yml | 6 +- .github/workflows/deploy_release.yml | 4 +- .github/workflows/frontend.yml | 12 +- .github/workflows/maintenance.yml | 2 +- .github/workflows/pages.yml | 2 +- .github/workflows/python.yml | 2 +- .github/workflows/scorecards.yml | 6 +- Backend/BackendFramework.csproj | 4 +- Backend/Dockerfile | 4 +- deploy/requirements.txt | 10 +- dev-requirements.txt | 26 +- .../assets/licenses/backend_licenses.txt | 36 +-- .../assets/licenses/frontend_licenses.txt | 85 +++-- maintenance/requirements.txt | 10 +- package-lock.json | 291 +++++++++--------- package.json | 10 +- 19 files changed, 270 insertions(+), 264 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index b6c336607b..b2c2815083 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -35,7 +35,7 @@ jobs: github.com:443 md-hdd-t032zjxllntc.z26.blob.storage.azure.net:443 objects.githubusercontent.com:443 - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup dotnet uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 with: @@ -83,7 +83,7 @@ jobs: storage.googleapis.com:443 uploader.codecov.io:443 - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Download coverage artifact uses: actions/download-artifact@v3 with: @@ -121,7 +121,7 @@ jobs: objects.githubusercontent.com:443 - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Manually install .NET to work around: # https://github.com/github/codeql-action/issues/757 - name: Setup .NET @@ -129,11 +129,11 @@ jobs: with: dotnet-version: "6.0.x" - name: Initialize CodeQL - uses: github/codeql-action/init@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 + uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 with: languages: csharp - name: Autobuild - uses: github/codeql-action/autobuild@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 + uses: github/codeql-action/autobuild@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 - name: Upload artifacts if build failed uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 if: ${{ failure() }} @@ -141,7 +141,7 @@ jobs: name: tracer-logs path: ${{ runner.temp }}/*.log - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 + uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 docker_build: runs-on: ubuntu-22.04 @@ -167,7 +167,7 @@ jobs: security.ubuntu.com:80 # For subfolders, currently a full checkout is required. # See: https://github.com/marketplace/actions/build-and-push-docker-images#path-context - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 - name: Build backend diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ad58558ad1..7d94d9957c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -59,11 +59,11 @@ jobs: objects.githubusercontent.com:443 pypi.org:443 - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 + uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -76,7 +76,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 + uses: github/codeql-action/autobuild@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 # Command-line programs to run using the OS shell. # See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -89,6 +89,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 + uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 04024a7a0b..19a0ad8484 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -26,7 +26,7 @@ jobs: registry-1.docker.io:443 # For subfolders, currently a full checkout is required. # See: https://github.com/marketplace/actions/build-and-push-docker-images#path-context - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 - name: Build database image diff --git a/.github/workflows/deploy_qa.yml b/.github/workflows/deploy_qa.yml index ecbc1f37eb..ca9c8582a7 100644 --- a/.github/workflows/deploy_qa.yml +++ b/.github/workflows/deploy_qa.yml @@ -51,7 +51,7 @@ jobs: storage.googleapis.com:443 sts.${{ secrets.AWS_DEFAULT_REGION }}.amazonaws.com:443 uploader.codecov.io:443 - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 - name: Build The Combine @@ -82,7 +82,7 @@ jobs: api.ecr.${{ secrets.AWS_DEFAULT_REGION }}.amazonaws.com:443 github.com:443 sts.${{ secrets.AWS_DEFAULT_REGION }}.amazonaws.com:443 - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4.0.1 with: @@ -97,7 +97,7 @@ jobs: if: ${{ github.ref_name == 'master' }} runs-on: [self-hosted, thecombine] steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Deploy The Combine Update uses: ./.github/actions/combine-deploy-update with: diff --git a/.github/workflows/deploy_release.yml b/.github/workflows/deploy_release.yml index a5cb5e3f5d..8386db22e2 100644 --- a/.github/workflows/deploy_release.yml +++ b/.github/workflows/deploy_release.yml @@ -47,7 +47,7 @@ jobs: security.ubuntu.com:80 storage.googleapis.com:443 sts.us-east-1.amazonaws.com:443 - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Build The Combine id: build_combine uses: ./.github/actions/combine-build @@ -66,7 +66,7 @@ jobs: needs: build runs-on: [self-hosted, thecombine] steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 - name: Deploy The Combine Update to QA diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 0df946a33a..731844e02a 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -29,9 +29,9 @@ jobs: github.com:443 registry.npmjs.org:443 - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version: ${{ matrix.node-version }} - run: npm ci @@ -58,9 +58,9 @@ jobs: github.com:443 registry.npmjs.org:443 - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version: ${{ matrix.node-version }} - run: npm ci @@ -93,7 +93,7 @@ jobs: storage.googleapis.com:443 uploader.codecov.io:443 - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Download coverage artifact uses: actions/download-artifact@v3 with: @@ -126,7 +126,7 @@ jobs: pypi.org:443 registry-1.docker.io:443 registry.npmjs.org:443 - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 - name: Build frontend diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index f30cc3d6e1..1c9db34f9e 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -30,7 +30,7 @@ jobs: security.ubuntu.com:80 # For subfolders, currently a full checkout is required. # See: https://github.com/marketplace/actions/build-and-push-docker-images#path-context - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 - name: Build maintenance image diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 65857fa32c..9b3b83d5e1 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -25,7 +25,7 @@ jobs: files.pythonhosted.org:443 github.com:443 pypi.org:443 - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 with: python-version: 3.11 diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 7350ea6949..8edd4cc565 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -28,7 +28,7 @@ jobs: files.pythonhosted.org:443 github.com:443 pypi.org:443 - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 with: diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 8f736539ea..cf7acda42a 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -54,12 +54,12 @@ jobs: tuf-repo-cdn.sigstore.dev:443 www.bestpractices.dev:443 - name: "Checkout code" - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398 # v2.3.0 + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 with: results_file: results.sarif results_format: sarif @@ -89,6 +89,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 + uses: github/codeql-action/upload-sarif@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 with: sarif_file: results.sarif diff --git a/Backend/BackendFramework.csproj b/Backend/BackendFramework.csproj index 5239e219d5..a0dad68b4e 100644 --- a/Backend/BackendFramework.csproj +++ b/Backend/BackendFramework.csproj @@ -19,11 +19,11 @@ NU1701 - + - + diff --git a/Backend/Dockerfile b/Backend/Dockerfile index f26a36cdbe..310a616e31 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -1,5 +1,5 @@ # Docker multi-stage build -FROM mcr.microsoft.com/dotnet/sdk:6.0.414-focal-amd64 AS builder +FROM mcr.microsoft.com/dotnet/sdk:6.0.416-focal-amd64 AS builder WORKDIR /app # Copy csproj and restore (fetch dependencies) as distinct layers. @@ -11,7 +11,7 @@ COPY . ./ RUN dotnet publish -c Release -o build # Build runtime image. -FROM mcr.microsoft.com/dotnet/aspnet:6.0.22-focal-amd64 +FROM mcr.microsoft.com/dotnet/aspnet:6.0.24-focal-amd64 ENV ASPNETCORE_URLS=http://+:5000 ENV COMBINE_IS_IN_CONTAINER=1 diff --git a/deploy/requirements.txt b/deploy/requirements.txt index 7ea4a37dae..694935a0b3 100644 --- a/deploy/requirements.txt +++ b/deploy/requirements.txt @@ -8,7 +8,7 @@ ansible==8.5.0 # via -r requirements.in ansible-core==2.15.5 # via ansible -cachetools==5.3.1 +cachetools==5.3.2 # via google-auth certifi==2023.7.22 # via @@ -16,13 +16,13 @@ certifi==2023.7.22 # requests cffi==1.16.0 # via cryptography -charset-normalizer==3.3.1 +charset-normalizer==3.3.2 # via requests -cryptography==41.0.4 +cryptography==41.0.5 # via # ansible-core # pyopenssl -google-auth==2.23.3 +google-auth==2.23.4 # via kubernetes idna==3.4 # via requests @@ -51,7 +51,7 @@ pyasn1-modules==0.3.0 # via google-auth pycparser==2.21 # via cffi -pyopenssl==23.2.0 +pyopenssl==23.3.0 # via -r requirements.in python-dateutil==2.8.2 # via kubernetes diff --git a/dev-requirements.txt b/dev-requirements.txt index 76ff8c0aad..5feb8b4c51 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -8,13 +8,13 @@ attrs==23.1.0 # via # flake8-bugbear # flake8-eradicate -babel==2.13.0 +babel==2.13.1 # via mkdocs-material beautifulsoup4==4.12.2 # via mkdocs-htmlproofer-plugin -black==23.10.0 +black==23.10.1 # via -r dev-requirements.in -cachetools==5.3.1 +cachetools==5.3.2 # via # google-auth # tox @@ -26,7 +26,7 @@ cffi==1.16.0 # via cryptography chardet==5.2.0 # via tox -charset-normalizer==3.3.1 +charset-normalizer==3.3.2 # via requests click==8.1.7 # via @@ -37,7 +37,7 @@ colorama==0.4.6 # -r dev-requirements.in # mkdocs-material # tox -cryptography==41.0.4 +cryptography==41.0.5 # via # pyopenssl # types-pyopenssl @@ -47,7 +47,7 @@ dnspython==2.4.2 # via pymongo eradicate==2.3.0 # via flake8-eradicate -filelock==3.12.4 +filelock==3.13.1 # via # tox # virtualenv @@ -69,7 +69,7 @@ flake8-eradicate==1.5.0 # via -r dev-requirements.in ghp-import==2.1.0 # via mkdocs -google-auth==2.23.3 +google-auth==2.23.4 # via kubernetes humanfriendly==10.0 # via -r dev-requirements.in @@ -87,7 +87,7 @@ jinja2-base64-filters==0.1.4 # via -r dev-requirements.in kubernetes==27.2.0 # via -r dev-requirements.in -markdown==3.5 +markdown==3.5.1 # via # mkdocs # mkdocs-htmlproofer-plugin @@ -108,11 +108,11 @@ mkdocs==1.5.3 # mkdocs-static-i18n mkdocs-htmlproofer-plugin==1.0.0 # via -r dev-requirements.in -mkdocs-material==9.4.6 +mkdocs-material==9.4.7 # via -r dev-requirements.in mkdocs-material-extensions==1.3 # via mkdocs-material -mkdocs-static-i18n==1.1.1 +mkdocs-static-i18n==1.2.0 # via -r dev-requirements.in mypy==1.6.1 # via -r dev-requirements.in @@ -164,7 +164,7 @@ pymdown-extensions==10.3.1 # via mkdocs-material pymongo==4.5.0 # via -r dev-requirements.in -pyopenssl==23.2.0 +pyopenssl==23.3.0 # via -r dev-requirements.in pyproject-api==1.6.1 # via tox @@ -209,7 +209,7 @@ tomli==2.0.1 # tox tox==4.11.3 # via -r dev-requirements.in -types-pyopenssl==23.2.0.2 +types-pyopenssl==23.3.0.0 # via -r dev-requirements.in types-python-dateutil==2.8.19.14 # via -r dev-requirements.in @@ -226,7 +226,7 @@ urllib3==2.0.7 # kubernetes # requests # types-requests -virtualenv==20.24.5 +virtualenv==20.24.6 # via tox watchdog==3.0.0 # via mkdocs diff --git a/docs/user_guide/assets/licenses/backend_licenses.txt b/docs/user_guide/assets/licenses/backend_licenses.txt index 730366d472..a62464cc26 100644 --- a/docs/user_guide/assets/licenses/backend_licenses.txt +++ b/docs/user_guide/assets/licenses/backend_licenses.txt @@ -97,11 +97,11 @@ license Type: #################################################################################################### Package:Microsoft.AspNetCore.Authentication.JwtBearer -Version:6.0.22 +Version:7.0.3 project URL:https://asp.net/ Description:ASP.NET Core middleware that enables an application to receive an OpenID Connect bearer token. -This package was built from the source code at https://github.com/dotnet/aspnetcore/tree/564969bca155b40432d101ec35f24a0e81e6afa0 +This package was built from the source code at https://github.com/dotnet/aspnetcore/tree/febee99db845fd8766a13bdb391a07c3ee90b4ba licenseUrl:https://licenses.nuget.org/MIT license Type:MIT @@ -604,7 +604,7 @@ license Type:MIT #################################################################################################### Package:Microsoft.IdentityModel.JsonWebTokens -Version:6.15.0 +Version:6.15.1 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Includes types that provide support for creating, serializing and validating JSON Web Tokens. licenseUrl:https://licenses.nuget.org/MIT @@ -620,15 +620,7 @@ license Type:MIT #################################################################################################### Package:Microsoft.IdentityModel.Logging -Version:6.10.0 -project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet -Description:Includes Event Source based logging support. -licenseUrl:https://licenses.nuget.org/MIT -license Type:MIT - -#################################################################################################### -Package:Microsoft.IdentityModel.Logging -Version:6.15.0 +Version:6.15.1 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Includes Event Source based logging support. licenseUrl:https://licenses.nuget.org/MIT @@ -644,7 +636,7 @@ license Type:MIT #################################################################################################### Package:Microsoft.IdentityModel.Protocols -Version:6.10.0 +Version:6.15.1 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Provides base protocol support for OpenIdConnect and WsFederation. licenseUrl:https://licenses.nuget.org/MIT @@ -652,7 +644,7 @@ license Type:MIT #################################################################################################### Package:Microsoft.IdentityModel.Protocols.OpenIdConnect -Version:6.10.0 +Version:6.15.1 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Includes types that provide support for OpenIdConnect protocol. licenseUrl:https://licenses.nuget.org/MIT @@ -660,7 +652,7 @@ license Type:MIT #################################################################################################### Package:Microsoft.IdentityModel.Tokens -Version:6.15.0 +Version:6.15.1 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Includes types that provide support for SecurityTokens, Cryptographic operations: Signing, Verifying Signatures, Encryption. licenseUrl:https://licenses.nuget.org/MIT @@ -817,26 +809,26 @@ license Type:MIT #################################################################################################### Package:MongoDB.Bson -Version:2.21.0 +Version:2.22.0 project URL:https://www.mongodb.com/docs/drivers/csharp/ Description:MongoDB's Official Bson Library. -licenseUrl:https://www.nuget.org/packages/MongoDB.Bson/2.21.0/License +licenseUrl:https://www.nuget.org/packages/MongoDB.Bson/2.22.0/License license Type:LICENSE.md #################################################################################################### Package:MongoDB.Driver -Version:2.21.0 +Version:2.22.0 project URL:https://www.mongodb.com/docs/drivers/csharp/ Description:Official .NET driver for MongoDB. -licenseUrl:https://www.nuget.org/packages/MongoDB.Driver/2.21.0/License +licenseUrl:https://www.nuget.org/packages/MongoDB.Driver/2.22.0/License license Type:LICENSE.md #################################################################################################### Package:MongoDB.Driver.Core -Version:2.21.0 +Version:2.22.0 project URL:https://www.mongodb.com/docs/drivers/csharp/ Description:Core Component of the Official MongoDB .NET Driver. -licenseUrl:https://www.nuget.org/packages/MongoDB.Driver.Core/2.21.0/License +licenseUrl:https://www.nuget.org/packages/MongoDB.Driver.Core/2.22.0/License license Type:LICENSE.md #################################################################################################### @@ -1734,7 +1726,7 @@ license Type:MS-EULA #################################################################################################### Package:System.IdentityModel.Tokens.Jwt -Version:6.15.0 +Version:6.15.1 project URL:https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet Description:Includes types that provide support for creating, serializing and validating JSON Web Tokens. licenseUrl:https://licenses.nuget.org/MIT diff --git a/docs/user_guide/assets/licenses/frontend_licenses.txt b/docs/user_guide/assets/licenses/frontend_licenses.txt index 35e5393379..df1d3e895f 100644 --- a/docs/user_guide/assets/licenses/frontend_licenses.txt +++ b/docs/user_guide/assets/licenses/frontend_licenses.txt @@ -1,4 +1,4 @@ -@babel/code-frame 7.18.6 +@babel/code-frame 7.22.13 MIT MIT License @@ -50,7 +50,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@babel/helper-string-parser 7.19.4 +@babel/helper-string-parser 7.22.5 MIT MIT License @@ -76,7 +76,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@babel/helper-validator-identifier 7.19.1 +@babel/helper-validator-identifier 7.22.20 MIT MIT License @@ -102,7 +102,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@babel/highlight 7.18.6 +@babel/highlight 7.22.20 MIT MIT License @@ -128,7 +128,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@babel/runtime 7.23.1 +@babel/runtime 7.23.2 MIT MIT License @@ -154,7 +154,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@babel/types 7.20.7 +@babel/types 7.23.0 MIT MIT License @@ -434,7 +434,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@emotion/react 11.11.0 +@emotion/react 11.11.1 MIT MIT License @@ -1138,7 +1138,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@mui/base 5.0.0-beta.18 +@mui/base 5.0.0-beta.22 MIT The MIT License (MIT) @@ -1163,7 +1163,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@mui/core-downloads-tracker 5.14.12 +@mui/core-downloads-tracker 5.14.16 MIT The MIT License (MIT) @@ -1213,7 +1213,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@mui/material 5.14.12 +@mui/material 5.14.16 MIT The MIT License (MIT) @@ -1238,7 +1238,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@mui/private-theming 5.14.12 +@mui/private-theming 5.14.16 MIT The MIT License (MIT) @@ -1263,7 +1263,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@mui/styled-engine 5.14.12 +@mui/styled-engine 5.14.16 MIT The MIT License (MIT) @@ -1288,7 +1288,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@mui/styles 5.14.3 +@mui/styles 5.14.16 MIT The MIT License (MIT) @@ -1313,7 +1313,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@mui/system 5.14.12 +@mui/system 5.14.16 MIT The MIT License (MIT) @@ -1338,7 +1338,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@mui/types 7.2.5 +@mui/types 7.2.8 MIT The MIT License (MIT) @@ -1363,7 +1363,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -@mui/utils 5.14.12 +@mui/utils 5.14.16 MIT The MIT License (MIT) @@ -40472,7 +40472,7 @@ MIT SOFTWARE -@types/prop-types 15.7.8 +@types/prop-types 15.7.9 MIT MIT License @@ -40547,7 +40547,7 @@ MIT SOFTWARE -@types/react-transition-group 4.4.6 +@types/react-transition-group 4.4.8 MIT MIT License @@ -41042,7 +41042,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -crypto-js 4.1.1 +crypto-js 4.2.0 MIT # License @@ -41739,7 +41739,7 @@ Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors THE SOFTWARE. -function-bind 1.1.1 +function-bind 1.1.2 MIT Copyright (c) 2013 Raynos. @@ -41801,30 +41801,29 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -has 1.0.3 +hasown 2.0.0 MIT -Copyright (c) 2013 Thiago de Arruda +MIT License -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: +Copyright (c) Jordan Harband and contributors -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. hey-listen 1.0.8 @@ -42331,7 +42330,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -is-core-module 2.13.0 +is-core-module 2.13.1 MIT The MIT License (MIT) @@ -43400,7 +43399,7 @@ SOFTWARE. -react-i18next 12.3.1 +react-i18next 13.3.1 MIT The MIT License (MIT) @@ -43924,7 +43923,7 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -resolve 1.22.1 +resolve 1.22.8 MIT MIT License diff --git a/maintenance/requirements.txt b/maintenance/requirements.txt index 766dd79ba7..9d16371e1e 100644 --- a/maintenance/requirements.txt +++ b/maintenance/requirements.txt @@ -4,7 +4,7 @@ # # pip-compile requirements.in # -cachetools==5.3.1 +cachetools==5.3.2 # via google-auth certifi==2023.7.22 # via @@ -12,13 +12,13 @@ certifi==2023.7.22 # requests cffi==1.16.0 # via cryptography -charset-normalizer==3.3.1 +charset-normalizer==3.3.2 # via requests -cryptography==41.0.4 +cryptography==41.0.5 # via pyopenssl dnspython==2.4.2 # via pymongo -google-auth==2.23.3 +google-auth==2.23.4 # via kubernetes humanfriendly==10.0 # via -r requirements.in @@ -40,7 +40,7 @@ pycparser==2.21 # via cffi pymongo==4.5.0 # via -r requirements.in -pyopenssl==23.2.0 +pyopenssl==23.3.0 # via -r requirements.in python-dateutil==2.8.2 # via kubernetes diff --git a/package-lock.json b/package-lock.json index 60c1dac5d4..98400a71a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,8 @@ "@matt-block/react-recaptcha-v2": "^2.0.1", "@microsoft/signalr": "^6.0.7", "@mui/icons-material": "^5.14.11", - "@mui/material": "^5.14.12", - "@mui/styles": "^5.14.3", + "@mui/material": "^5.14.16", + "@mui/styles": "^5.14.16", "@redux-devtools/extension": "^3.2.5", "@reduxjs/toolkit": "^1.9.5", "@segment/analytics-next": "^1.55.0", @@ -60,12 +60,12 @@ "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.5.1", - "@types/crypto-js": "^4.1.2", + "@types/crypto-js": "^4.1.3", "@types/css-mediaquery": "^0.1.2", "@types/jest": "^29.5.5", "@types/loadable__component": "^5.13.5", "@types/node": "^20.5.1", - "@types/nspell": "^2.1.1", + "@types/nspell": "^2.1.5", "@types/react": "^17.0.34", "@types/react-beautiful-dnd": "^13.1.4", "@types/react-dom": "^17.0.11", @@ -81,7 +81,7 @@ "css-mediaquery": "^0.1.2", "eslint": "^8.51.0", "eslint-import-resolver-typescript": "^3.6.0", - "eslint-plugin-import": "^2.28.1", + "eslint-plugin-import": "^2.29.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-unused-imports": "^3.0.0", @@ -2211,9 +2211,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", - "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -4573,14 +4573,14 @@ } }, "node_modules/@mui/base": { - "version": "5.0.0-beta.18", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.18.tgz", - "integrity": "sha512-e9ZCy/ndhyt5MTshAS3qAUy/40UiO0jX+kAo6a+XirrPJE+rrQW+mKPSI0uyp+5z4Vh+z0pvNoJ2S2gSrNz3BQ==", + "version": "5.0.0-beta.22", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.22.tgz", + "integrity": "sha512-l4asGID5tmyerx9emJfXOKLyXzaBtdXNIFE3M+IrSZaFtGFvaQKHhc3+nxxSxPf1+G44psjczM0ekRQCdXx9HA==", "dependencies": { - "@babel/runtime": "^7.23.1", + "@babel/runtime": "^7.23.2", "@floating-ui/react-dom": "^2.0.2", - "@mui/types": "^7.2.5", - "@mui/utils": "^5.14.12", + "@mui/types": "^7.2.8", + "@mui/utils": "^5.14.16", "@popperjs/core": "^2.11.8", "clsx": "^2.0.0", "prop-types": "^15.8.1" @@ -4612,9 +4612,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.14.12", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.12.tgz", - "integrity": "sha512-WZhCkKqhrXaSVBzoC6LNcVkIawS000OOt7gmnp4g9HhyvN0PSclRXc/JrkC7EwfzUAZJh+hiK2LaVsbtOpNuOg==", + "version": "5.14.16", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.16.tgz", + "integrity": "sha512-97isBjzH2v1K7oB4UH2f4NOkBShOynY6dhnoR2XlUk/g6bb7ZBv2I3D1hvvqPtpEigKu93e7f/jAYr5d9LOc5w==", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui" @@ -4646,17 +4646,17 @@ } }, "node_modules/@mui/material": { - "version": "5.14.12", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.12.tgz", - "integrity": "sha512-EelF2L46VcVqhg3KjzIGBBpOtcBgRh0MMy9Efuk6Do81QdcZsFC9RebCVAflo5jIdbHiBmxBs5/l5Q9NjONozg==", - "dependencies": { - "@babel/runtime": "^7.23.1", - "@mui/base": "5.0.0-beta.18", - "@mui/core-downloads-tracker": "^5.14.12", - "@mui/system": "^5.14.12", - "@mui/types": "^7.2.5", - "@mui/utils": "^5.14.12", - "@types/react-transition-group": "^4.4.6", + "version": "5.14.16", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.16.tgz", + "integrity": "sha512-W4zZ4vnxgGk6/HqBwgsDHKU7x2l2NhX+r8gAwfg58Rhu3ikfY7NkIS6y8Gl3NkATc4GG1FNaGjjpQKfJx3U6Jw==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@mui/base": "5.0.0-beta.22", + "@mui/core-downloads-tracker": "^5.14.16", + "@mui/system": "^5.14.16", + "@mui/types": "^7.2.8", + "@mui/utils": "^5.14.16", + "@types/react-transition-group": "^4.4.8", "clsx": "^2.0.0", "csstype": "^3.1.2", "prop-types": "^15.8.1", @@ -4703,12 +4703,12 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/@mui/private-theming": { - "version": "5.14.12", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.12.tgz", - "integrity": "sha512-TWwm+9+BgHFpoR3w04FG+IqID4ALa74A27RuKq2CEaWgxliBZB24EVeI6djfjFt5t4FYmIb8BMw2ZJEir7YjLQ==", + "version": "5.14.16", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.16.tgz", + "integrity": "sha512-FNlL0pTSEBh8nXsVWreCHDSHk+jG8cBx1sxRbT8JVtL+PYbYPi802zfV4B00Kkf0LNRVRvAVQwojMWSR/MYGng==", "dependencies": { - "@babel/runtime": "^7.23.1", - "@mui/utils": "^5.14.12", + "@babel/runtime": "^7.23.2", + "@mui/utils": "^5.14.16", "prop-types": "^15.8.1" }, "engines": { @@ -4729,11 +4729,11 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.14.12", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.12.tgz", - "integrity": "sha512-bocxt1nDmXfB3gpLfCCmFCyJ7sVmscFs+PuheO210QagZwHVp47UIRT1AiswLDYSQo1ZqmVGn7KLEJEYK0d4Xw==", + "version": "5.14.16", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.16.tgz", + "integrity": "sha512-FfvYvTG/Zd+KXMMImbcMYEeQAbONGuX5Vx3gBmmtB6KyA7Mvm9Pma1ly3R0gc44yeoFd+2wBjn1feS8h42HW5w==", "dependencies": { - "@babel/runtime": "^7.23.1", + "@babel/runtime": "^7.23.2", "@emotion/cache": "^11.11.0", "csstype": "^3.1.2", "prop-types": "^15.8.1" @@ -4760,15 +4760,15 @@ } }, "node_modules/@mui/styles": { - "version": "5.14.3", - "resolved": "https://registry.npmjs.org/@mui/styles/-/styles-5.14.3.tgz", - "integrity": "sha512-6a3znEs0WsHKW5j4458Sc9WqE2MfmPBnAbcyizHsmCKoF196e3uaO6PBXJmIjjNqFMiEmqiFQyGn3zs3jY7AyQ==", + "version": "5.14.16", + "resolved": "https://registry.npmjs.org/@mui/styles/-/styles-5.14.16.tgz", + "integrity": "sha512-pBA2eLBEfqLv/jmu9qGcErwml27upH2YBFRuRU2loZm5R57di5y/GjpM9EWc77+49axaTlHfO8LWbic4kPvxoQ==", "dependencies": { - "@babel/runtime": "^7.22.6", + "@babel/runtime": "^7.23.2", "@emotion/hash": "^0.9.1", - "@mui/private-theming": "^5.13.7", - "@mui/types": "^7.2.4", - "@mui/utils": "^5.14.3", + "@mui/private-theming": "^5.14.16", + "@mui/types": "^7.2.8", + "@mui/utils": "^5.14.16", "clsx": "^2.0.0", "csstype": "^3.1.2", "hoist-non-react-statics": "^3.3.2", @@ -4808,15 +4808,15 @@ } }, "node_modules/@mui/system": { - "version": "5.14.12", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.12.tgz", - "integrity": "sha512-6DXfjjLhW0/ia5qU3Crke7j+MnfDbMBOHlLIrqbrEqNs0AuSBv8pXniEGb+kqO0H804NJreRTEJRjCngwOX5CA==", - "dependencies": { - "@babel/runtime": "^7.23.1", - "@mui/private-theming": "^5.14.12", - "@mui/styled-engine": "^5.14.12", - "@mui/types": "^7.2.5", - "@mui/utils": "^5.14.12", + "version": "5.14.16", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.16.tgz", + "integrity": "sha512-uKnPfsDqDs8bbN54TviAuoGWOmFiQLwNZ3Wvj+OBkJCzwA6QnLb/sSeCB7Pk3ilH4h4jQ0BHtbR+Xpjy9wlOuA==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@mui/private-theming": "^5.14.16", + "@mui/styled-engine": "^5.14.16", + "@mui/types": "^7.2.8", + "@mui/utils": "^5.14.16", "clsx": "^2.0.0", "csstype": "^3.1.2", "prop-types": "^15.8.1" @@ -4855,9 +4855,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.5.tgz", - "integrity": "sha512-S2BwfNczr7VwS6ki8GoAXJyARoeSJDLuxOEPs3vEMyTALlf9PrdHv+sluX7kk3iKrCg/ML2mIWwapZvWbkMCQA==", + "version": "7.2.8", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.8.tgz", + "integrity": "sha512-9u0ji+xspl96WPqvrYJF/iO+1tQ1L5GTaDOeG3vCR893yy7VcWwRNiVMmPdPNpMDqx0WV1wtEW9OMwK9acWJzQ==", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0" }, @@ -4868,12 +4868,12 @@ } }, "node_modules/@mui/utils": { - "version": "5.14.12", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.12.tgz", - "integrity": "sha512-RFNXnhKQlzIkIUig6mmv0r5VbtjPdWoaBPYicq25LETdZux59HAqoRdWw15T7lp3c7gXOoE8y67+hTB8C64m2g==", + "version": "5.14.16", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.16.tgz", + "integrity": "sha512-3xV31GposHkwRbQzwJJuooWpK2ybWdEdeUPtRjv/6vjomyi97F3+68l+QVj9tPTvmfSbr2sx5c/NuvDulrdRmA==", "dependencies": { - "@babel/runtime": "^7.23.1", - "@types/prop-types": "^15.7.7", + "@babel/runtime": "^7.23.2", + "@types/prop-types": "^15.7.9", "prop-types": "^15.8.1", "react-is": "^18.2.0" }, @@ -8683,9 +8683,9 @@ } }, "node_modules/@types/crypto-js": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.2.tgz", - "integrity": "sha512-t33RNmTu5ufG/sorROIafiCVJMx3jz95bXUMoPAZcUD14fxMXnuTzqzXZoxpR0tNx2xpw11Dlmem9vGCsrSOfA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.3.tgz", + "integrity": "sha512-YP1sYYayLe7Eg5oXyLLvOLfxBfZ5Fgpz6sVWkpB18wDMywCLPWmqzRz+9gyuOoLF0fzDTTFwlyNbx7koONUwqA==", "dev": true }, "node_modules/@types/css-mediaquery": { @@ -8876,9 +8876,9 @@ "dev": true }, "node_modules/@types/nspell": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@types/nspell/-/nspell-2.1.3.tgz", - "integrity": "sha512-4tVesNT7TGA6KkfFBxUQAifZm5VYubs4sRnXf1rGsbySzTiIiOqloqZKSQYgM+oevck17AAwRXQ7iedhTk+0Pg==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/nspell/-/nspell-2.1.5.tgz", + "integrity": "sha512-vI5W59b9b+GD378foZ1pBeZ9sSFFEKmF9D4aJM6DKOv9YGtsx9mdtjoJYGzjiO/CRDLlEjDYzc4PdmzXi+QgsA==", "dev": true, "dependencies": { "@types/node": "*" @@ -8896,9 +8896,9 @@ "dev": true }, "node_modules/@types/prop-types": { - "version": "15.7.8", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.8.tgz", - "integrity": "sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ==" + "version": "15.7.9", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", + "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" }, "node_modules/@types/q": { "version": "1.5.5", @@ -8976,9 +8976,9 @@ } }, "node_modules/@types/react-transition-group": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", - "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.8.tgz", + "integrity": "sha512-QmQ22q+Pb+HQSn04NL3HtrqHwYMf4h3QKArOy5F8U5nEVMaihBs3SR10WiOM1iwPz5jIo8x/u11al+iEGZZrvg==", "dependencies": { "@types/react": "*" } @@ -10221,15 +10221,15 @@ "dev": true }, "node_modules/array-includes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "is-string": "^1.0.7" }, "engines": { @@ -10249,16 +10249,16 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz", - "integrity": "sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.1.3" + "get-intrinsic": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -10268,14 +10268,14 @@ } }, "node_modules/array.prototype.flat": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", - "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -10286,14 +10286,14 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", - "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -13267,14 +13267,14 @@ } }, "node_modules/eslint-import-resolver-node": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", - "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, "dependencies": { "debug": "^3.2.7", - "is-core-module": "^2.11.0", - "resolve": "^1.22.1" + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" } }, "node_modules/eslint-import-resolver-node/node_modules/debug": { @@ -13373,26 +13373,26 @@ "dev": true }, "node_modules/eslint-plugin-import": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", - "integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==", + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", + "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", "dev": true, "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.findlastindex": "^1.2.2", - "array.prototype.flat": "^1.3.1", - "array.prototype.flatmap": "^1.3.1", + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.7", + "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.8.0", - "has": "^1.0.3", - "is-core-module": "^2.13.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.6", - "object.groupby": "^1.0.0", - "object.values": "^1.1.6", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", "semver": "^6.3.1", "tsconfig-paths": "^3.14.2" }, @@ -14694,9 +14694,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -15036,6 +15039,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -15112,6 +15116,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -15845,11 +15860,11 @@ } }, "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -20826,14 +20841,14 @@ } }, "node_modules/object.fromentries": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", - "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -20861,14 +20876,14 @@ } }, "node_modules/object.groupby": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.0.tgz", - "integrity": "sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", - "es-abstract": "^1.21.2", + "es-abstract": "^1.22.1", "get-intrinsic": "^1.2.1" } }, @@ -20886,14 +20901,14 @@ } }, "node_modules/object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -25430,11 +25445,11 @@ "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, diff --git a/package.json b/package.json index 7608c59fb4..f3296a6e4e 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,8 @@ "@matt-block/react-recaptcha-v2": "^2.0.1", "@microsoft/signalr": "^6.0.7", "@mui/icons-material": "^5.14.11", - "@mui/material": "^5.14.12", - "@mui/styles": "^5.14.3", + "@mui/material": "^5.14.16", + "@mui/styles": "^5.14.16", "@redux-devtools/extension": "^3.2.5", "@reduxjs/toolkit": "^1.9.5", "@segment/analytics-next": "^1.55.0", @@ -88,12 +88,12 @@ "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.5.1", - "@types/crypto-js": "^4.1.2", + "@types/crypto-js": "^4.1.3", "@types/css-mediaquery": "^0.1.2", "@types/jest": "^29.5.5", "@types/loadable__component": "^5.13.5", "@types/node": "^20.5.1", - "@types/nspell": "^2.1.1", + "@types/nspell": "^2.1.5", "@types/react": "^17.0.34", "@types/react-beautiful-dnd": "^13.1.4", "@types/react-dom": "^17.0.11", @@ -109,7 +109,7 @@ "css-mediaquery": "^0.1.2", "eslint": "^8.51.0", "eslint-import-resolver-typescript": "^3.6.0", - "eslint-plugin-import": "^2.28.1", + "eslint-plugin-import": "^2.29.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-unused-imports": "^3.0.0", From 21787af28b0acf0b10e687b32376c43748965574 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Wed, 1 Nov 2023 14:18:26 -0400 Subject: [PATCH 2/7] Port Login to use redux-toolkit (#2748) --- .vscode/settings.json | 1 + src/components/App/DefaultState.ts | 2 +- .../Login/LoginPage/LoginComponent.tsx | 8 +- src/components/Login/LoginPage/index.ts | 9 +- .../LoginPage/tests/LoginComponent.test.tsx | 5 +- src/components/Login/Redux/LoginActions.ts | 98 +++++----- src/components/Login/Redux/LoginReducer.ts | 117 +++++------ src/components/Login/Redux/LoginReduxTypes.ts | 39 ++-- .../Login/Redux/tests/LoginActions.test.tsx | 127 ++++++++++++ .../Login/SignUpPage/SignUpComponent.tsx | 8 +- src/components/Login/SignUpPage/index.ts | 5 +- .../SignUpPage/tests/SignUpComponent.test.tsx | 9 +- .../Login/tests/LoginActions.test.tsx | 183 ------------------ .../Login/tests/LoginReducer.test.tsx | 131 ------------- .../ProjectInvite/ProjectInvite.tsx | 10 +- src/rootReducer.ts | 2 +- 16 files changed, 266 insertions(+), 488 deletions(-) create mode 100644 src/components/Login/Redux/tests/LoginActions.test.tsx delete mode 100644 src/components/Login/tests/LoginActions.test.tsx delete mode 100644 src/components/Login/tests/LoginReducer.test.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 328f2aa278..baa9e6cdb4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -76,6 +76,7 @@ "recaptcha", "reportgenerator", "sched", + "signup", "sillsdev", "Sldr", "subtag", diff --git a/src/components/App/DefaultState.ts b/src/components/App/DefaultState.ts index 1294ddf45d..5e981553ad 100644 --- a/src/components/App/DefaultState.ts +++ b/src/components/App/DefaultState.ts @@ -1,5 +1,5 @@ import { defaultState as goalTimelineState } from "components/GoalTimeline/DefaultState"; -import { defaultState as loginState } from "components/Login/Redux/LoginReducer"; +import { defaultState as loginState } from "components/Login/Redux/LoginReduxTypes"; import { defaultState as currentProjectState } from "components/Project/ProjectReduxTypes"; import { defaultState as exportProjectState } from "components/ProjectExport/Redux/ExportProjectReduxTypes"; import { defaultState as pronunciationsState } from "components/Pronunciations/Redux/PronunciationsReduxTypes"; diff --git a/src/components/Login/LoginPage/LoginComponent.tsx b/src/components/Login/LoginPage/LoginComponent.tsx index 3291d02cff..12e3084cbc 100644 --- a/src/components/Login/LoginPage/LoginComponent.tsx +++ b/src/components/Login/LoginPage/LoginComponent.tsx @@ -16,6 +16,7 @@ import { BannerType } from "api/models"; import { getBannerText } from "backend"; import router from "browserRouter"; import { LoadingButton } from "components/Buttons"; +import { LoginStatus } from "components/Login/Redux/LoginReduxTypes"; import { Path } from "types/path"; import { RuntimeConfig } from "types/runtimeConfig"; import theme from "types/theme"; @@ -34,8 +35,7 @@ export interface LoginDispatchProps { } export interface LoginStateProps { - loginAttempt?: boolean; - loginFailure?: boolean; + status: LoginStatus; } interface LoginProps @@ -173,7 +173,7 @@ export class Login extends Component { )} {/* "Failed to log in" */} - {this.props.loginFailure && ( + {this.props.status === LoginStatus.Failure && ( { color: "primary", }} disabled={!this.state.isVerified} - loading={this.props.loginAttempt} + loading={this.props.status === LoginStatus.InProgress} > {this.props.t("login.login")} diff --git a/src/components/Login/LoginPage/index.ts b/src/components/Login/LoginPage/index.ts index 992fffffd6..6cec33ab08 100644 --- a/src/components/Login/LoginPage/index.ts +++ b/src/components/Login/LoginPage/index.ts @@ -5,7 +5,7 @@ import Login, { LoginStateProps, } from "components/Login/LoginPage/LoginComponent"; import { - asyncLogin, + asyncLogIn, logoutAndResetStore, } from "components/Login/Redux/LoginActions"; import { reset } from "rootActions"; @@ -13,16 +13,13 @@ import { StoreState } from "types"; import { StoreStateDispatch } from "types/Redux/actions"; function mapStateToProps(state: StoreState): LoginStateProps { - return { - loginAttempt: state.loginState && state.loginState.loginAttempt, - loginFailure: state.loginState && state.loginState.loginFailure, - }; + return { status: state.loginState.loginStatus }; } function mapDispatchToProps(dispatch: StoreStateDispatch): LoginDispatchProps { return { login: (username: string, password: string) => { - dispatch(asyncLogin(username, password)); + dispatch(asyncLogIn(username, password)); }, logout: () => { dispatch(logoutAndResetStore()); diff --git a/src/components/Login/LoginPage/tests/LoginComponent.test.tsx b/src/components/Login/LoginPage/tests/LoginComponent.test.tsx index 45ee12c976..93d89fa73f 100644 --- a/src/components/Login/LoginPage/tests/LoginComponent.test.tsx +++ b/src/components/Login/LoginPage/tests/LoginComponent.test.tsx @@ -8,6 +8,7 @@ import { import "tests/reactI18nextMock"; import Login from "components/Login/LoginPage/LoginComponent"; +import { LoginStatus } from "components/Login/Redux/LoginReduxTypes"; jest.mock( "@matt-block/react-recaptcha-v2", @@ -31,7 +32,9 @@ const MOCK_EVENT = { preventDefault: jest.fn(), target: { value: DATA } }; describe("Testing login component", () => { beforeEach(async () => { await act(async () => { - loginMaster = create(); + loginMaster = create( + + ); }); loginHandle = loginMaster.root.findByType(Login); LOGOUT.mockClear(); diff --git a/src/components/Login/Redux/LoginActions.ts b/src/components/Login/Redux/LoginActions.ts index af21bf0376..58b67658f7 100644 --- a/src/components/Login/Redux/LoginActions.ts +++ b/src/components/Login/Redux/LoginActions.ts @@ -1,52 +1,65 @@ +import { PayloadAction } from "@reduxjs/toolkit"; import Hex from "crypto-js/enc-hex"; import sha256 from "crypto-js/sha256"; import * as backend from "backend"; import router from "browserRouter"; import { - LoginActionTypes, - UserAction, -} from "components/Login/Redux/LoginReduxTypes"; + setLoginAttemptAction, + setLoginFailureAction, + setLoginSuccessAction, + setSignupAttemptAction, + setSignupFailureAction, + setSignupSuccessAction, +} from "components/Login/Redux/LoginReducer"; import { reset } from "rootActions"; import { StoreStateDispatch } from "types/Redux/actions"; import { Path } from "types/path"; import { newUser } from "types/user"; -// thunk action creator -export function asyncLogin(username: string, password: string) { +// Action Creation Functions + +export function loginAttempt(username: string): PayloadAction { + return setLoginAttemptAction(username); +} + +export function loginFailure(error: string): PayloadAction { + return setLoginFailureAction(error); +} + +export function loginSuccess(): PayloadAction { + return setLoginSuccessAction(); +} + +export function signupAttempt(username: string): PayloadAction { + return setSignupAttemptAction(username); +} + +export function signupFailure(error: string): PayloadAction { + return setSignupFailureAction(error); +} + +export function signupSuccess(): PayloadAction { + return setSignupSuccessAction(); +} + +// Dispatch Functions + +export function asyncLogIn(username: string, password: string) { return async (dispatch: StoreStateDispatch) => { dispatch(loginAttempt(username)); await backend .authenticateUser(username, password) .then(async (user) => { - dispatch(loginSuccess(user.username)); + dispatch(loginSuccess()); // hash the user name and use it in analytics.identify const analyticsId = Hex.stringify(sha256(user.id)); analytics.identify(analyticsId); router.navigate(Path.ProjScreen); }) - .catch(() => dispatch(loginFailure(username))); - }; -} - -export function loginAttempt(username: string): UserAction { - return { - type: LoginActionTypes.LOGIN_ATTEMPT, - payload: { username }, - }; -} - -export function loginFailure(username: string): UserAction { - return { - type: LoginActionTypes.LOGIN_FAILURE, - payload: { username }, - }; -} - -export function loginSuccess(username: string): UserAction { - return { - type: LoginActionTypes.LOGIN_SUCCESS, - payload: { username }, + .catch((err) => + dispatch(loginFailure(err.response?.data ?? err.message)) + ); }; } @@ -63,41 +76,20 @@ export function asyncSignUp( password: string ) { return async (dispatch: StoreStateDispatch) => { - dispatch(signUpAttempt(username)); + dispatch(signupAttempt(username)); // Create new user const user = newUser(name, username, password); user.email = email; await backend .addUser(user) .then(() => { - dispatch(signUpSuccess(username)); + dispatch(signupSuccess()); setTimeout(() => { - dispatch(asyncLogin(username, password)); + dispatch(asyncLogIn(username, password)); }, 1000); }) .catch((err) => - dispatch(signUpFailure(err.response?.data ?? err.message)) + dispatch(signupFailure(err.response?.data ?? err.message)) ); }; } - -export function signUpAttempt(username: string): UserAction { - return { - type: LoginActionTypes.SIGN_UP_ATTEMPT, - payload: { username }, - }; -} - -export function signUpFailure(errorMessage: string): UserAction { - return { - type: LoginActionTypes.SIGN_UP_FAILURE, - payload: { username: errorMessage }, - }; -} - -export function signUpSuccess(username: string): UserAction { - return { - type: LoginActionTypes.SIGN_UP_SUCCESS, - payload: { username }, - }; -} diff --git a/src/components/Login/Redux/LoginReducer.ts b/src/components/Login/Redux/LoginReducer.ts index a69bb5d2d2..f1c0ccc846 100644 --- a/src/components/Login/Redux/LoginReducer.ts +++ b/src/components/Login/Redux/LoginReducer.ts @@ -1,72 +1,53 @@ +import { createSlice } from "@reduxjs/toolkit"; + import { - LoginActionTypes, - LoginState, - UserAction, + LoginStatus, + defaultState, } from "components/Login/Redux/LoginReduxTypes"; -import { StoreAction, StoreActionTypes } from "rootActions"; +import { StoreActionTypes } from "rootActions"; + +const loginSlice = createSlice({ + name: "loginState", + initialState: defaultState, + reducers: { + setLoginAttemptAction: (state, action) => { + state.error = ""; + state.loginStatus = LoginStatus.InProgress; + state.signupStatus = LoginStatus.Default; + state.username = action.payload; + }, + setLoginFailureAction: (state, action) => { + state.error = action.payload; + state.loginStatus = LoginStatus.Failure; + }, + setLoginSuccessAction: (state) => { + state.loginStatus = LoginStatus.Success; + }, + setSignupAttemptAction: (state, action) => { + state.error = ""; + state.loginStatus = LoginStatus.Default; + state.signupStatus = LoginStatus.InProgress; + state.username = action.payload; + }, + setSignupFailureAction: (state, action) => { + state.error = action.payload; + state.signupStatus = LoginStatus.Failure; + }, + setSignupSuccessAction: (state) => { + state.signupStatus = LoginStatus.Success; + }, + }, + extraReducers: (builder) => + builder.addCase(StoreActionTypes.RESET, () => defaultState), +}); -export const defaultState: LoginState = { - username: "", - loginAttempt: false, - loginFailure: false, - loginSuccess: false, - signUpAttempt: false, - signUpFailure: "", - signUpSuccess: false, -}; +export const { + setLoginAttemptAction, + setLoginFailureAction, + setLoginSuccessAction, + setSignupAttemptAction, + setSignupFailureAction, + setSignupSuccessAction, +} = loginSlice.actions; -export const loginReducer = ( - state: LoginState = defaultState, //createStore() calls each reducer with undefined state - action: StoreAction | UserAction -): LoginState => { - switch (action.type) { - case LoginActionTypes.LOGIN_ATTEMPT: - return { - ...state, - username: action.payload.username, - loginAttempt: true, - loginSuccess: false, - loginFailure: false, - }; - case LoginActionTypes.LOGIN_FAILURE: - return { - ...state, - username: action.payload.username, - loginAttempt: false, - loginFailure: true, - loginSuccess: false, - }; - case LoginActionTypes.LOGIN_SUCCESS: - return { - ...state, - username: action.payload.username, - loginSuccess: true, - }; - case LoginActionTypes.SIGN_UP_ATTEMPT: - return { - ...state, - username: action.payload.username, - signUpAttempt: true, - signUpFailure: "", - signUpSuccess: false, - }; - case LoginActionTypes.SIGN_UP_SUCCESS: - return { - ...state, - username: action.payload.username, - signUpAttempt: false, - signUpSuccess: true, - }; - case LoginActionTypes.SIGN_UP_FAILURE: - return { - ...state, - signUpAttempt: false, - signUpFailure: action.payload.username, - signUpSuccess: false, - }; - case StoreActionTypes.RESET: - return defaultState; - default: - return state; - } -}; +export default loginSlice.reducer; diff --git a/src/components/Login/Redux/LoginReduxTypes.ts b/src/components/Login/Redux/LoginReduxTypes.ts index 2211ead752..24d18f0d68 100644 --- a/src/components/Login/Redux/LoginReduxTypes.ts +++ b/src/components/Login/Redux/LoginReduxTypes.ts @@ -1,36 +1,25 @@ -export enum LoginActionTypes { - LOGIN_ATTEMPT = "LOGIN_ATTEMPT", - LOGIN_FAILURE = "LOGIN_FAILURE", - LOGIN_SUCCESS = "LOGIN_SUCCESS", - SIGN_UP_ATTEMPT = "SIGN_UP_ATTEMPT", - SIGN_UP_FAILURE = "SIGN_UP_FAILURE", - SIGN_UP_SUCCESS = "SIGN_UP_SUCCESS", +export enum LoginStatus { + Default = "Default", + Failure = "Failure", + InProgress = "InProgress", + Success = "Success", } export interface LoginState { + error: string; + loginStatus: LoginStatus; + signupStatus: LoginStatus; username: string; - loginAttempt: boolean; - loginFailure: boolean; - loginSuccess: boolean; - signUpAttempt: boolean; - signUpFailure: string; - signUpSuccess: boolean; } -export type LoginType = - | typeof LoginActionTypes.LOGIN_ATTEMPT - | typeof LoginActionTypes.LOGIN_FAILURE - | typeof LoginActionTypes.LOGIN_SUCCESS - | typeof LoginActionTypes.SIGN_UP_ATTEMPT - | typeof LoginActionTypes.SIGN_UP_FAILURE - | typeof LoginActionTypes.SIGN_UP_SUCCESS; +export const defaultState: LoginState = { + error: "", + loginStatus: LoginStatus.Default, + signupStatus: LoginStatus.Default, + username: "", +}; export interface LoginData { username: string; password?: string; } - -export interface UserAction { - type: LoginType; - payload: LoginData; -} diff --git a/src/components/Login/Redux/tests/LoginActions.test.tsx b/src/components/Login/Redux/tests/LoginActions.test.tsx new file mode 100644 index 0000000000..48c79e1de4 --- /dev/null +++ b/src/components/Login/Redux/tests/LoginActions.test.tsx @@ -0,0 +1,127 @@ +import { PreloadedState } from "redux"; + +import { User } from "api/models"; +import { defaultState } from "components/App/DefaultState"; +import { + asyncLogIn, + asyncSignUp, + logoutAndResetStore, +} from "components/Login/Redux/LoginActions"; +import { LoginStatus } from "components/Login/Redux/LoginReduxTypes"; +import { RootState, setupStore } from "store"; +import { newUser } from "types/user"; + +jest.mock("backend", () => ({ + addUser: (user: User) => mockAddUser(user), + authenticateUser: (...args: any[]) => mockAuthenticateUser(...args), +})); + +// Mock the track and identify methods of segment analytics. +global.analytics = { identify: jest.fn(), track: jest.fn() } as any; + +const mockAddUser = jest.fn(); +const mockAuthenticateUser = jest.fn(); + +const mockEmail = "test@e.mail"; +const mockName = "testName"; +const mockPassword = "testPass"; +const mockUsername = "testUsername"; +const mockUser = { + ...newUser(mockName, mockUsername, mockPassword), + email: mockEmail, +}; + +// Preloaded values for store when testing +const persistedDefaultState: PreloadedState = { + ...defaultState, + _persist: { version: 1, rehydrated: false }, +}; + +beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); +}); + +describe("LoginAction", () => { + describe("asyncLogIn", () => { + it("correctly affects state on failure", async () => { + const store = setupStore(); + mockAuthenticateUser.mockRejectedValueOnce({}); + await store.dispatch(asyncLogIn(mockUsername, mockPassword)); + const loginState = store.getState().loginState; + expect(loginState.error).not.toEqual(""); + expect(loginState.loginStatus).toEqual(LoginStatus.Failure); + expect(loginState.signupStatus).toEqual(LoginStatus.Default); + expect(loginState.username).toEqual(mockUsername); + }); + + it("correctly affects state on success", async () => { + const store = setupStore(); + mockAuthenticateUser.mockResolvedValueOnce(mockUser); + await store.dispatch(asyncLogIn(mockUsername, mockPassword)); + const loginState = store.getState().loginState; + expect(loginState.error).toEqual(""); + expect(loginState.loginStatus).toEqual(LoginStatus.Success); + expect(loginState.signupStatus).toEqual(LoginStatus.Default); + expect(loginState.username).toEqual(mockUsername); + }); + }); + + describe("asyncSignUp", () => { + it("correctly affects state on failure", async () => { + const store = setupStore(); + mockAddUser.mockRejectedValueOnce({}); + await store.dispatch( + asyncSignUp(mockName, mockUsername, mockEmail, mockPassword) + ); + const loginState = store.getState().loginState; + expect(loginState.error).not.toEqual(""); + expect(loginState.loginStatus).toEqual(LoginStatus.Default); + expect(loginState.signupStatus).toEqual(LoginStatus.Failure); + expect(loginState.username).toEqual(mockUsername); + + // A failed signup does not trigger a login. + jest.runAllTimers(); + expect(mockAuthenticateUser).not.toBeCalled(); + }); + + it("correctly affects state on success", async () => { + const store = setupStore(); + mockAddUser.mockResolvedValueOnce({}); + await store.dispatch( + asyncSignUp(mockName, mockUsername, mockEmail, mockPassword) + ); + const loginState = store.getState().loginState; + expect(loginState.error).toEqual(""); + expect(loginState.loginStatus).toEqual(LoginStatus.Default); + expect(loginState.signupStatus).toEqual(LoginStatus.Success); + expect(loginState.username).toEqual(mockUsername); + + // A successful signup triggers a login using `setTimeout`. + mockAuthenticateUser.mockRejectedValueOnce({}); + jest.runAllTimers(); + expect(mockAuthenticateUser).toBeCalledTimes(1); + }); + }); + + describe("logoutAndResetStore", () => { + it("correctly affects state", async () => { + const nonDefaultState = { + error: "nonempty-string", + loginStatus: LoginStatus.Success, + signupStatus: LoginStatus.Failure, + username: "nonempty-string", + }; + const store = setupStore({ + ...persistedDefaultState, + loginState: nonDefaultState, + }); + store.dispatch(logoutAndResetStore()); + const loginState = store.getState().loginState; + expect(loginState.error).toEqual(""); + expect(loginState.loginStatus).toEqual(LoginStatus.Default); + expect(loginState.signupStatus).toEqual(LoginStatus.Default); + expect(loginState.username).toEqual(""); + }); + }); +}); diff --git a/src/components/Login/SignUpPage/SignUpComponent.tsx b/src/components/Login/SignUpPage/SignUpComponent.tsx index d06f888c6e..c27ab63e66 100644 --- a/src/components/Login/SignUpPage/SignUpComponent.tsx +++ b/src/components/Login/SignUpPage/SignUpComponent.tsx @@ -13,6 +13,7 @@ import { withTranslation, WithTranslation } from "react-i18next"; import router from "browserRouter"; import { LoadingDoneButton } from "components/Buttons"; import { captchaStyle } from "components/Login/LoginPage/LoginComponent"; +import { LoginStatus } from "components/Login/Redux/LoginReduxTypes"; import { Path } from "types/path"; import { RuntimeConfig } from "types/runtimeConfig"; import { @@ -38,9 +39,8 @@ interface SignUpDispatchProps { } export interface SignUpStateProps { - inProgress?: boolean; - success?: boolean; failureMessage: string; + status: LoginStatus; } interface SignUpProps @@ -296,8 +296,8 @@ export class SignUp extends Component { { beforeEach(async () => { await act(async () => { - signUpMaster = create(); + signUpMaster = create( + + ); }); signUpHandle = signUpMaster.root.findByType(SignUp); mockReset.mockClear(); diff --git a/src/components/Login/tests/LoginActions.test.tsx b/src/components/Login/tests/LoginActions.test.tsx deleted file mode 100644 index 587f4762a2..0000000000 --- a/src/components/Login/tests/LoginActions.test.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import configureMockStore from "redux-mock-store"; -import thunk from "redux-thunk"; - -import { User } from "api/models"; -import * as LocalStorage from "backend/localStorage"; -import * as LoginAction from "components/Login/Redux/LoginActions"; -import * as LoginReducer from "components/Login/Redux/LoginReducer"; -import { - LoginActionTypes, - LoginType, - UserAction, -} from "components/Login/Redux/LoginReduxTypes"; -import * as RootAction from "rootActions"; -import { newUser } from "types/user"; - -jest.mock("backend", () => ({ - addUser: (user: User) => mockAddUser(user), - authenticateUser: (username: string, password: string) => - mockAuthenticateUser(username, password), -})); - -// Mock the track and identify methods of segment analytics. -global.analytics = { identify: jest.fn(), track: jest.fn() } as any; - -const mockAddUser = jest.fn(); -const mockAuthenticateUser = jest.fn(); - -const createMockStore = configureMockStore([thunk]); -const mockState = LoginReducer.defaultState; - -const mockUser = { - ...newUser("testName", "testUsername", "testPass"), - token: "testToken", - email: "test@e.mail", -}; -const loginAttempt: UserAction = { - type: LoginActionTypes.LOGIN_ATTEMPT, - payload: { username: mockUser.username }, -}; -const loginFailure: UserAction = { - type: LoginActionTypes.LOGIN_FAILURE, - payload: { username: mockUser.username }, -}; -const loginSuccess: UserAction = { - type: LoginActionTypes.LOGIN_SUCCESS, - payload: { username: mockUser.username }, -}; -const reset: RootAction.StoreAction = { - type: RootAction.StoreActionTypes.RESET, -}; -const signUpAttempt: UserAction = { - type: LoginActionTypes.SIGN_UP_ATTEMPT, - payload: { username: mockUser.username }, -}; -const signUpFailure: UserAction = { - type: LoginActionTypes.SIGN_UP_FAILURE, - payload: { username: mockUser.username }, -}; -const signUpSuccess: UserAction = { - type: LoginActionTypes.SIGN_UP_SUCCESS, - payload: { username: mockUser.username }, -}; - -beforeEach(() => { - jest.clearAllMocks(); -}); - -describe("LoginAction", () => { - test("sign up returns correct value", () => { - expect(LoginAction.signUpAttempt(mockUser.username)).toEqual(signUpAttempt); - }); - - describe("asyncLogin", () => { - it("login failure correctly affects state", async () => { - mockAuthenticateUser.mockRejectedValue(new Error(mockUser.username)); - const mockStore = createMockStore(mockState); - await mockStore.dispatch( - LoginAction.asyncLogin(mockUser.username, mockUser.password) - ); - expect(mockStore.getActions()).toEqual([loginAttempt, loginFailure]); - }); - - it("login success correctly affects state", async () => { - mockAuthenticateUser.mockResolvedValue(mockUser); - const mockStore = createMockStore(mockState); - await mockStore.dispatch( - LoginAction.asyncLogin(mockUser.username, mockUser.password) - ); - expect(mockStore.getActions()).toEqual([loginAttempt, loginSuccess]); - }); - }); - - describe("asyncSignUp", () => { - it("sign up failure correctly affects state", async () => { - mockAddUser.mockRejectedValue(new Error(mockUser.username)); - const mockStore = createMockStore(mockState); - await mockStore.dispatch( - LoginAction.asyncSignUp( - mockUser.name, - mockUser.username, - mockUser.email, - mockUser.password - ) - ); - expect(mockStore.getActions()).toEqual([signUpAttempt, signUpFailure]); - }); - - it("sign up success correctly affects state", async () => { - mockAddUser.mockResolvedValue(mockUser); - const mockStore = createMockStore(mockState); - await mockStore.dispatch( - LoginAction.asyncSignUp( - mockUser.name, - mockUser.username, - mockUser.email, - mockUser.password - ) - ); - expect(mockStore.getActions()).toEqual([signUpAttempt, signUpSuccess]); - }); - }); - - describe("Action creators return correct value.", () => { - test("loginAttempt", () => { - testActionCreatorAgainst( - LoginAction.loginAttempt, - LoginActionTypes.LOGIN_ATTEMPT - ); - }); - - test("loginFailure", () => { - testActionCreatorAgainst( - LoginAction.loginFailure, - LoginActionTypes.LOGIN_FAILURE - ); - }); - - test("loginSuccess", () => { - testActionCreatorAgainst( - LoginAction.loginSuccess, - LoginActionTypes.LOGIN_SUCCESS - ); - }); - - test("signUpAttempt", () => { - testActionCreatorAgainst( - LoginAction.signUpAttempt, - LoginActionTypes.SIGN_UP_ATTEMPT - ); - }); - - test("signUpFailure", () => { - testActionCreatorAgainst( - LoginAction.signUpFailure, - LoginActionTypes.SIGN_UP_FAILURE - ); - }); - - test("signUpSuccess", () => { - testActionCreatorAgainst( - LoginAction.signUpSuccess, - LoginActionTypes.SIGN_UP_SUCCESS - ); - }); - }); - - test("logout creates a proper action", () => { - LocalStorage.setCurrentUser(mockUser); - const mockStore = createMockStore(mockState); - mockStore.dispatch(LoginAction.logoutAndResetStore()); - expect(mockStore.getActions()).toEqual([reset]); - }); -}); - -function testActionCreatorAgainst( - LoginAction: (name: string) => UserAction, - type: LoginType -): void { - expect(LoginAction(mockUser.username)).toEqual({ - type: type, - payload: { username: mockUser.username }, - }); -} diff --git a/src/components/Login/tests/LoginReducer.test.tsx b/src/components/Login/tests/LoginReducer.test.tsx deleted file mode 100644 index 2fa47d7a3b..0000000000 --- a/src/components/Login/tests/LoginReducer.test.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import * as LoginReducer from "components/Login/Redux/LoginReducer"; -import { - LoginActionTypes, - LoginData, - LoginState, - UserAction, -} from "components/Login/Redux/LoginReduxTypes"; -import { StoreAction, StoreActionTypes } from "rootActions"; - -const user: LoginData = { - username: "testUsername", - password: "testPassword", -}; - -describe("LoginReducer Tests", () => { - const dummyState: LoginState = { - ...LoginReducer.defaultState, - username: user.username, - loginSuccess: false, - }; - - //The state while attempting to log in - const loginAttemptState: LoginState = { - username: "testUsername", - loginAttempt: true, - loginFailure: false, - loginSuccess: false, - signUpAttempt: false, - signUpFailure: "", - signUpSuccess: false, - }; - - const action: UserAction = { - type: LoginActionTypes.LOGIN_ATTEMPT, - payload: user, - }; - - // Test with no state - test("no state, expecting login attempt", () => { - action.type = LoginActionTypes.LOGIN_ATTEMPT; - expect(LoginReducer.loginReducer(undefined, action)).toEqual( - loginAttemptState - ); - }); - - test("default state, expecting login attempt", () => { - action.type = LoginActionTypes.LOGIN_ATTEMPT; - expect(LoginReducer.loginReducer(dummyState, action)).toEqual( - loginAttemptState - ); - }); - - test("failed login, expecting no success", () => { - const loginFailureState: LoginState = { - ...LoginReducer.defaultState, - username: user.username, - loginAttempt: false, - loginFailure: true, - loginSuccess: false, - }; - - action.type = LoginActionTypes.LOGIN_FAILURE; - expect(LoginReducer.loginReducer(dummyState, action)).toEqual( - loginFailureState - ); - }); - - test("default state, expecting sign up", () => { - const resultState: LoginState = { - username: "testUsername", - loginAttempt: false, - loginFailure: false, - loginSuccess: false, - signUpAttempt: true, - signUpFailure: "", - signUpSuccess: false, - }; - action.type = LoginActionTypes.SIGN_UP_ATTEMPT; - - expect(LoginReducer.loginReducer(dummyState, action)).toEqual(resultState); - }); - - test("default state, expecting login success", () => { - const loginSuccessState: LoginState = { - ...dummyState, - username: user.username, - loginSuccess: true, - }; - action.type = LoginActionTypes.LOGIN_SUCCESS; - - expect(LoginReducer.loginReducer(dummyState, action)).toEqual( - loginSuccessState - ); - }); - - test("default state, expecting sign up success", () => { - const signUpSuccessState: LoginState = { - ...dummyState, - username: user.username, - signUpAttempt: false, - signUpSuccess: true, - }; - action.type = LoginActionTypes.SIGN_UP_SUCCESS; - expect(LoginReducer.loginReducer(dummyState, action)).toEqual( - signUpSuccessState - ); - }); - - test("default state, expecting sign up failure", () => { - const signUpFailureState: LoginState = { - ...dummyState, - signUpAttempt: false, - signUpFailure: "testUsername", - signUpSuccess: false, - }; - action.type = LoginActionTypes.SIGN_UP_FAILURE; - expect(LoginReducer.loginReducer(dummyState, action)).toEqual( - signUpFailureState - ); - }); - - test("non-default state, expecting reset", () => { - const resetAction: StoreAction = { - type: StoreActionTypes.RESET, - }; - - expect(LoginReducer.loginReducer({} as LoginState, resetAction)).toEqual( - LoginReducer.defaultState - ); - }); -}); diff --git a/src/components/ProjectInvite/ProjectInvite.tsx b/src/components/ProjectInvite/ProjectInvite.tsx index fcbe7c3d5c..ee23c4a811 100644 --- a/src/components/ProjectInvite/ProjectInvite.tsx +++ b/src/components/ProjectInvite/ProjectInvite.tsx @@ -10,11 +10,8 @@ import { useAppDispatch, useAppSelector } from "types/hooks"; import { Path } from "types/path"; export default function ProjectInvite(): ReactElement { - const inProgress = useAppSelector((state) => state.loginState.signUpAttempt); - const success = useAppSelector((state) => state.loginState.signUpSuccess); - const failureMessage = useAppSelector( - (state) => state.loginState.signUpFailure - ); + const status = useAppSelector((state) => state.loginState.signupStatus); + const failureMessage = useAppSelector((state) => state.loginState.error); const dispatch = useAppDispatch(); const navigate = useNavigate(); @@ -38,8 +35,7 @@ export default function ProjectInvite(): ReactElement { return isValidLink ? ( { dispatch(asyncSignUp(name, user, email, password)); diff --git a/src/rootReducer.ts b/src/rootReducer.ts index b0863ed7f0..a617d965fc 100644 --- a/src/rootReducer.ts +++ b/src/rootReducer.ts @@ -1,7 +1,7 @@ import { combineReducers, Reducer } from "redux"; import goalsReducer from "components/GoalTimeline/Redux/GoalReducer"; -import { loginReducer } from "components/Login/Redux/LoginReducer"; +import loginReducer from "components/Login/Redux/LoginReducer"; import { projectReducer } from "components/Project/ProjectReducer"; import exportProjectReducer from "components/ProjectExport/Redux/ExportProjectReducer"; import { pronunciationsReducer } from "components/Pronunciations/Redux/PronunciationsReducer"; From b9252e26fcca511a29c4057866c068c145bc3735 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Thu, 2 Nov 2023 17:13:47 -0400 Subject: [PATCH 3/7] [DataEntry] Prevent RecentEntry re-renders when typing in NewEntry (#2752) --- .../DataEntry/DataEntryTable/RecentEntry.tsx | 26 +- .../DataEntry/DataEntryTable/index.tsx | 403 +++++++++--------- .../DataEntryTable/tests/RecentEntry.test.tsx | 6 +- .../DataEntryTable/tests/index.test.tsx | 4 +- 4 files changed, 234 insertions(+), 205 deletions(-) diff --git a/src/components/DataEntry/DataEntryTable/RecentEntry.tsx b/src/components/DataEntry/DataEntryTable/RecentEntry.tsx index 95165d2514..de27a2b4d3 100644 --- a/src/components/DataEntry/DataEntryTable/RecentEntry.tsx +++ b/src/components/DataEntry/DataEntryTable/RecentEntry.tsx @@ -1,5 +1,5 @@ import { Grid } from "@mui/material"; -import { ReactElement, useState } from "react"; +import { ReactElement, memo, useState } from "react"; import { Word, WritingSystem } from "api/models"; import { @@ -19,10 +19,10 @@ export interface RecentEntryProps { rowIndex: number; entry: Word; senseGuid: string; - updateGloss: (gloss: string) => void; - updateNote: (newText: string) => Promise; - updateVern: (newVernacular: string, targetWordId?: string) => void; - removeEntry: () => void; + updateGloss: (index: number, gloss: string) => void; + updateNote: (index: number, newText: string) => Promise; + updateVern: (index: number, newVern: string, targetWordId?: string) => void; + removeEntry: (index: number) => void; addAudioToWord: (wordId: string, audioFile: File) => void; deleteAudioFromWord: (wordId: string, fileName: string) => void; focusNewEntry: () => void; @@ -34,7 +34,7 @@ export interface RecentEntryProps { /** * Displays a recently entered word that a user can still edit */ -export default function RecentEntry(props: RecentEntryProps): ReactElement { +export function RecentEntry(props: RecentEntryProps): ReactElement { const sense = props.entry.senses.find((s) => s.guid === props.senseGuid)!; if (sense.glosses.length < 1) { sense.glosses.push(newGloss("", props.analysisLang.bcp47)); @@ -44,20 +44,24 @@ export default function RecentEntry(props: RecentEntryProps): ReactElement { function conditionallyUpdateGloss(): void { if (firstGlossText(sense) !== gloss) { - props.updateGloss(gloss); + props.updateGloss(props.rowIndex, gloss); } } function conditionallyUpdateVern(): void { if (vernacular.trim()) { if (props.entry.vernacular !== vernacular) { - props.updateVern(vernacular); + props.updateVern(props.rowIndex, vernacular); } } else { setVernacular(props.entry.vernacular); } } + const handleRemoveEntry = (): void => props.removeEntry(props.rowIndex); + const handleUpdateNote = (noteText: string): Promise => + props.updateNote(props.rowIndex, noteText); + return ( )} @@ -152,7 +156,7 @@ export default function RecentEntry(props: RecentEntryProps): ReactElement { > {!props.disabled && ( props.removeEntry()} + removeEntry={handleRemoveEntry} buttonId={`${idAffix}-${props.rowIndex}-delete`} confirmId={"addWords.deleteRowWarning"} wordId={props.entry.id} @@ -162,3 +166,5 @@ export default function RecentEntry(props: RecentEntryProps): ReactElement { ); } + +export default memo(RecentEntry); diff --git a/src/components/DataEntry/DataEntryTable/index.tsx b/src/components/DataEntry/DataEntryTable/index.tsx index e3d37e5e0e..607c3bde3f 100644 --- a/src/components/DataEntry/DataEntryTable/index.tsx +++ b/src/components/DataEntry/DataEntryTable/index.tsx @@ -249,26 +249,29 @@ export default function DataEntryTable( * to make sure that word doesn't get edited by two different functions. * Use this with newId to specify the replacement of a defunct word. */ - const defunctWord = (oldId: string, newId?: string): void => { - if (!newId && state.defunctWordIds[oldId]) { - return; - } - if (!newId) { + const defunctWord = useCallback( + (oldId: string, newId?: string): void => { + if (!newId && state.defunctWordIds[oldId]) { + return; + } + if (!newId) { + setState((prevState) => { + const defunctWordIds = { ...prevState.defunctWordIds }; + defunctWordIds[oldId] = DefunctStatus.Recent; + return { ...prevState, defunctWordIds }; + }); + return; + } setState((prevState) => { + const defunctUpdates = { ...prevState.defunctUpdates }; const defunctWordIds = { ...prevState.defunctWordIds }; + defunctUpdates[oldId] = newId; defunctWordIds[oldId] = DefunctStatus.Recent; - return { ...prevState, defunctWordIds }; + return { ...prevState, defunctUpdates, defunctWordIds }; }); - return; - } - setState((prevState) => { - const defunctUpdates = { ...prevState.defunctUpdates }; - const defunctWordIds = { ...prevState.defunctWordIds }; - defunctUpdates[oldId] = newId; - defunctWordIds[oldId] = DefunctStatus.Recent; - return { ...prevState, defunctUpdates, defunctWordIds }; - }); - }; + }, + [state.defunctWordIds] + ); /*** Update a recent entry to a different sense of the same word. */ const switchSense = useCallback( @@ -291,18 +294,21 @@ export default function DataEntryTable( ); /*** Add to recent entries every sense of the word with the current semantic domain. */ - const addAllSensesToDisplay = (word: Word): void => { - const domId = props.semanticDomain.id; - setState((prevState) => { - const recentWords = [...prevState.recentWords]; - word.senses.forEach((s) => { - if (s.semanticDomains.find((dom) => dom.id === domId)) { - recentWords.push({ word, senseGuid: s.guid }); - } + const addAllSensesToDisplay = useCallback( + (word: Word): void => { + const domId = props.semanticDomain.id; + setState((prevState) => { + const recentWords = [...prevState.recentWords]; + word.senses.forEach((s) => { + if (s.semanticDomains.find((dom) => dom.id === domId)) { + recentWords.push({ word, senseGuid: s.guid }); + } + }); + return { ...prevState, recentWords }; }); - return { ...prevState, recentWords }; - }); - }; + }, + [props.semanticDomain.id] + ); /*** Add one-sense word to the display of recent entries. */ const addToDisplay = (wordAccess: WordAccess, insertIndex?: number): void => { @@ -558,98 +564,103 @@ export default function DataEntryTable( //////////////////////////////////// /*** Given an array of audio file urls, add them all to specified word. */ - const addAudiosToBackend = async ( - oldId: string, - audioURLs: string[] - ): Promise => { - if (!audioURLs.length) { - return oldId; - } - defunctWord(oldId); - let newId = oldId; - for (const audioURL of audioURLs) { - newId = await uploadFileFromUrl(newId, audioURL); - } - defunctWord(oldId, newId); - return newId; - }; + const addAudiosToBackend = useCallback( + async (oldId: string, audioURLs: string[]): Promise => { + if (!audioURLs.length) { + return oldId; + } + defunctWord(oldId); + let newId = oldId; + for (const audioURL of audioURLs) { + newId = await uploadFileFromUrl(newId, audioURL); + } + defunctWord(oldId, newId); + return newId; + }, + [defunctWord] + ); /*** Given a single audio file, add to specified word. */ - const addAudioFileToWord = async ( - oldId: string, - audioFile: File - ): Promise => { - defunctWord(oldId); - const newId = await backend.uploadAudio(oldId, audioFile); - defunctWord(oldId, newId); - }; + const addAudioFileToWord = useCallback( + async (oldId: string, audioFile: File): Promise => { + defunctWord(oldId); + const newId = await backend.uploadAudio(oldId, audioFile); + defunctWord(oldId, newId); + }, + [defunctWord] + ); /*** Add a word determined to be a duplicate. * Ensures the updated word has representation in the display. * Note: Only for use after backend.getDuplicateId(). */ - const addDuplicateWord = async ( - word: Word, - audioURLs: string[], - oldId: string - ): Promise => { - const isInDisplay = - state.recentWords.findIndex((w) => w.word.id === oldId) > -1; + const addDuplicateWord = useCallback( + async (word: Word, audioURLs: string[], oldId: string): Promise => { + const isInDisplay = + state.recentWords.findIndex((w) => w.word.id === oldId) > -1; - defunctWord(oldId); - const newWord = await backend.updateDuplicate(oldId, word); - defunctWord(oldId, newWord.id); + defunctWord(oldId); + const newWord = await backend.updateDuplicate(oldId, word); + defunctWord(oldId, newWord.id); - const newId = await addAudiosToBackend(newWord.id, audioURLs); + const newId = await addAudiosToBackend(newWord.id, audioURLs); - if (!isInDisplay) { - addAllSensesToDisplay(await backend.getWord(newId)); - } - }; + if (!isInDisplay) { + addAllSensesToDisplay(await backend.getWord(newId)); + } + }, + [addAllSensesToDisplay, addAudiosToBackend, defunctWord, state.recentWords] + ); /*** Deletes specified audio file from specified word. */ - const deleteAudioFromWord = async ( - oldId: string, - fileName: string - ): Promise => { - defunctWord(oldId); - const newId = await backend.deleteAudio(oldId, fileName); - defunctWord(oldId, newId); - }; + const deleteAudioFromWord = useCallback( + async (oldId: string, fileName: string): Promise => { + defunctWord(oldId); + const newId = await backend.deleteAudio(oldId, fileName); + defunctWord(oldId, newId); + }, + [defunctWord] + ); /*** Updates word. */ - const updateWordInBackend = async (word: Word): Promise => { - defunctWord(word.id); - const newWord = await backend.updateWord(word); - defunctWord(word.id, newWord.id); - return newWord; - }; + const updateWordInBackend = useCallback( + async (word: Word): Promise => { + defunctWord(word.id); + const newWord = await backend.updateWord(word); + defunctWord(word.id, newWord.id); + return newWord; + }, + [defunctWord] + ); ///////////////////////////////// // General async functions. ///////////////////////////////// /*** Add a new word to the project, or update if new word is a duplicate. */ - const addNewWord = async ( - wordToAdd: Word, - audioURLs: string[], - insertIndex?: number - ): Promise => { - wordToAdd.note.language = analysisLang.bcp47; - - // Check if word is duplicate to existing word. - const dupId = await backend.getDuplicateId(wordToAdd); - if (dupId) { - return await addDuplicateWord(wordToAdd, audioURLs, dupId); - } + const addNewWord = useCallback( + async ( + wordToAdd: Word, + audioURLs: string[], + insertIndex?: number + ): Promise => { + wordToAdd.note.language = analysisLang.bcp47; + + // Check if word is duplicate to existing word. + const dupId = await backend.getDuplicateId(wordToAdd); + if (dupId) { + return await addDuplicateWord(wordToAdd, audioURLs, dupId); + } - let word = await backend.createWord(wordToAdd); - const wordId = await addAudiosToBackend(word.id, audioURLs); - if (wordId !== word.id) { - word = await backend.getWord(wordId); - } - addToDisplay({ word, senseGuid: word.senses[0].guid }, insertIndex); - }; + let word = await backend.createWord(wordToAdd); + const wordId = await addAudiosToBackend(word.id, audioURLs); + if (wordId !== word.id) { + word = await backend.getWord(wordId); + } + addToDisplay({ word, senseGuid: word.senses[0].guid }, insertIndex); + }, + [addAudiosToBackend, addDuplicateWord, analysisLang.bcp47] + ); /*** Update the word in the backend and the frontend. */ const updateWordBackAndFront = async ( @@ -747,93 +758,111 @@ export default function DataEntryTable( ///////////////////////////////// /*** Retract a recent entry. */ - const undoRecentEntry = async (eIndex: number): Promise => { - const { word, senseGuid } = state.recentWords[eIndex]; - const sIndex = word.senses.findIndex((s) => s.guid === senseGuid); - if (sIndex === -1) { - throw new Error("Entry does not have specified sense."); - } - defunctWord(word.id); - removeRecentEntry(eIndex); - const senses = [...word.senses]; - const oldSense = senses[sIndex]; - const oldDoms = oldSense.semanticDomains; - if (oldDoms.length > 1) { - // If there is more than one semantic domain in this sense, only remove the domain. - const doms = oldDoms.filter((d) => d.id !== props.semanticDomain.id); - const newSense: Sense = { ...oldSense, semanticDomains: doms }; - senses.splice(sIndex, 1, newSense); - await updateWordInBackend({ ...word, senses }); - } else if (senses.length > 1) { - // If there is more than one sense in this word, only remove this sense. - senses.splice(sIndex, 1); - await updateWordInBackend({ ...word, senses }); - } else { - // Since this is the only sense, delete the word. - await backend.deleteFrontierWord(word.id); - } - }; + const undoRecentEntry = useCallback( + async (eIndex: number): Promise => { + const { word, senseGuid } = state.recentWords[eIndex]; + const sIndex = word.senses.findIndex((s) => s.guid === senseGuid); + if (sIndex === -1) { + throw new Error("Entry does not have specified sense."); + } + defunctWord(word.id); + removeRecentEntry(eIndex); + const senses = [...word.senses]; + const oldSense = senses[sIndex]; + const oldDoms = oldSense.semanticDomains; + if (oldDoms.length > 1) { + // If there is more than one semantic domain in this sense, only remove the domain. + const doms = oldDoms.filter((d) => d.id !== props.semanticDomain.id); + const newSense: Sense = { ...oldSense, semanticDomains: doms }; + senses.splice(sIndex, 1, newSense); + await updateWordInBackend({ ...word, senses }); + } else if (senses.length > 1) { + // If there is more than one sense in this word, only remove this sense. + senses.splice(sIndex, 1); + await updateWordInBackend({ ...word, senses }); + } else { + // Since this is the only sense, delete the word. + await backend.deleteFrontierWord(word.id); + } + }, + [ + defunctWord, + props.semanticDomain.id, + state.recentWords, + updateWordInBackend, + ] + ); /*** Update the vernacular in a recent entry. */ - const updateRecentVern = async ( - index: number, - vernacular: string, - targetWordId?: string - ): Promise => { - if (targetWordId !== undefined) { - throw new Error("VernDialog on RecentEntry is not yet supported."); - } - const oldEntry = state.recentWords[index]; - const oldSenses = oldEntry.word.senses; - const oldSense = oldSenses.find((s) => s.guid === oldEntry.senseGuid); - if (!oldSense) { - throw new Error("Entry does not have specified sense."); - } + const updateRecentVern = useCallback( + async ( + index: number, + vernacular: string, + targetWordId?: string + ): Promise => { + if (targetWordId !== undefined) { + throw new Error("VernDialog on RecentEntry is not yet supported."); + } + const oldEntry = state.recentWords[index]; + const oldSenses = oldEntry.word.senses; + const oldSense = oldSenses.find((s) => s.guid === oldEntry.senseGuid); + if (!oldSense) { + throw new Error("Entry does not have specified sense."); + } - if (oldSenses.length === 1 && oldSense.semanticDomains.length === 1) { - // The word can simply be updated as it stand - await updateWordInBackend({ ...oldEntry.word, vernacular }); - } else { - // Retract and replaced with a new entry. - const word = simpleWord(vernacular, firstGlossText(oldSense)); - word.id = ""; - await undoRecentEntry(index); - await addNewWord(word, [], index); - } - }; + if (oldSenses.length === 1 && oldSense.semanticDomains.length === 1) { + // The word can simply be updated as it stand + await updateWordInBackend({ ...oldEntry.word, vernacular }); + } else { + // Retract and replaced with a new entry. + const word = simpleWord(vernacular, firstGlossText(oldSense)); + word.id = ""; + await undoRecentEntry(index); + await addNewWord(word, [], index); + } + }, + [addNewWord, state.recentWords, undoRecentEntry, updateWordInBackend] + ); /*** Update the gloss def in a recent entry. */ - const updateRecentGloss = async ( - index: number, - def: string - ): Promise => { - const oldEntry = state.recentWords[index]; - defunctWord(oldEntry.word.id); - const newWord = updateEntryGloss(oldEntry, def, props.semanticDomain.id); - await updateWordInBackend(newWord); - - // If a sense with a new guid was added, it needs to replace the old sense in the display. - if (newWord.senses.length > oldEntry.word.senses.length) { - const newSense = newWord.senses.find( - (sense) => !oldEntry.word.senses.find((s) => s.guid === sense.guid) - ); - if (newSense) { - queueSenseSwitch(oldEntry.senseGuid, newSense.guid); + const updateRecentGloss = useCallback( + async (index: number, def: string): Promise => { + const oldEntry = state.recentWords[index]; + defunctWord(oldEntry.word.id); + const newWord = updateEntryGloss(oldEntry, def, props.semanticDomain.id); + await updateWordInBackend(newWord); + + // If a sense with a new guid was added, it needs to replace the old sense in the display. + if (newWord.senses.length > oldEntry.word.senses.length) { + const newSense = newWord.senses.find( + (sense) => !oldEntry.word.senses.find((s) => s.guid === sense.guid) + ); + if (newSense) { + queueSenseSwitch(oldEntry.senseGuid, newSense.guid); + } } - } - }; + }, + [ + defunctWord, + props.semanticDomain.id, + state.recentWords, + updateWordInBackend, + ] + ); + + const handleFocusNewEntry = useCallback(() => focusInput(newVernInput), []); /*** Update the note text in a recent entry. */ - const updateRecentNote = async ( - index: number, - text: string - ): Promise => { - const oldWord = state.recentWords[index].word; - if (text !== oldWord.note.text) { - const note: Note = { ...oldWord.note, text }; - await updateWordInBackend({ ...oldWord, note }); - } - }; + const updateRecentNote = useCallback( + async (index: number, text: string): Promise => { + const oldWord = state.recentWords[index].word; + if (text !== oldWord.note.text) { + const note: Note = { ...oldWord.note, text }; + await updateWordInBackend({ ...oldWord, note }); + } + }, + [state.recentWords, updateWordInBackend] + ); return (
) => e?.preventDefault()}> @@ -874,19 +903,13 @@ export default function DataEntryTable( rowIndex={index} entry={wordAccess.word} senseGuid={wordAccess.senseGuid} - updateGloss={(newDef: string) => updateRecentGloss(index, newDef)} - updateNote={(newText: string) => updateRecentNote(index, newText)} - updateVern={(newVernacular: string, targetWordId?: string) => - updateRecentVern(index, newVernacular, targetWordId) - } - removeEntry={() => undoRecentEntry(index)} - addAudioToWord={(wordId: string, audioFile: File) => - addAudioFileToWord(wordId, audioFile) - } - deleteAudioFromWord={(wordId: string, fileName: string) => - deleteAudioFromWord(wordId, fileName) - } - focusNewEntry={() => focusInput(newVernInput)} + updateGloss={updateRecentGloss} + updateNote={updateRecentNote} + updateVern={updateRecentVern} + removeEntry={undoRecentEntry} + addAudioToWord={addAudioFileToWord} + deleteAudioFromWord={deleteAudioFromWord} + focusNewEntry={handleFocusNewEntry} analysisLang={analysisLang} vernacularLang={vernacularLang} disabled={Object.keys(state.defunctWordIds).includes( diff --git a/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx b/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx index fc8525608b..43772b8622 100644 --- a/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx +++ b/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx @@ -107,7 +107,7 @@ describe("ExistingEntry", () => { await updateVernAndBlur(mockVern); expect(mockUpdateVern).toBeCalledTimes(0); await updateVernAndBlur(mockText); - expect(mockUpdateVern).toBeCalledWith(mockText); + expect(mockUpdateVern).toBeCalledWith(0, mockText); }); }); @@ -125,7 +125,7 @@ describe("ExistingEntry", () => { await updateGlossAndBlur(mockGloss); expect(mockUpdateGloss).toBeCalledTimes(0); await updateGlossAndBlur(mockText); - expect(mockUpdateGloss).toBeCalledWith(mockText); + expect(mockUpdateGloss).toBeCalledWith(0, mockText); }); }); @@ -136,7 +136,7 @@ describe("ExistingEntry", () => { await act(async () => { testHandle.props.updateText(mockText); }); - expect(mockUpdateNote).toBeCalledWith(mockText); + expect(mockUpdateNote).toBeCalledWith(0, mockText); }); }); }); diff --git a/src/components/DataEntry/DataEntryTable/tests/index.test.tsx b/src/components/DataEntry/DataEntryTable/tests/index.test.tsx index e8e0e70342..5ea0c26f3e 100644 --- a/src/components/DataEntry/DataEntryTable/tests/index.test.tsx +++ b/src/components/DataEntry/DataEntryTable/tests/index.test.tsx @@ -431,7 +431,7 @@ describe("DataEntryTable", () => { await addRecentEntry(); const recentEntry = testRenderer.root.findByType(MockRecentEntry); await act(async () => { - await recentEntry.props.removeEntry(); + await recentEntry.props.removeEntry(recentEntry.props.rowIndex); }); expect(testRenderer.root.findAllByType(MockRecentEntry)).toHaveLength(0); }); @@ -454,7 +454,7 @@ describe("DataEntryTable", () => { // Update the vernacular const newVern = "not the vern generated in addRecentEntry"; await act(async () => { - await recentEntry.props.updateVern(newVern); + await recentEntry.props.updateVern(recentEntry.props.rowIndex, newVern); }); // Confirm the backend update was correctly called From 06f64b8a7052e3f685511c162346ad9d6ead6983 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Fri, 3 Nov 2023 11:38:32 -0400 Subject: [PATCH 4/7] Upload Cobertura coverage rather than Clover (#2742) --- .github/workflows/frontend.yml | 4 ++-- package.json | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 731844e02a..b98e84f796 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -72,7 +72,7 @@ jobs: with: if-no-files-found: error name: coverage - path: coverage/clover.xml + path: coverage/cobertura-coverage.xml retention-days: 7 upload_coverage: @@ -102,7 +102,7 @@ jobs: uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4 with: fail_ci_if_error: true - files: clover.xml + files: cobertura-coverage.xml flags: frontend name: Frontend diff --git a/package.json b/package.json index f3296a6e4e..67e5a699bc 100644 --- a/package.json +++ b/package.json @@ -209,6 +209,12 @@ } }, "jest": { + "coverageReporters": [ + "cobertura", + "lcov", + "text", + "text-summary" + ], "transformIgnorePatterns": [ "/node_modules/(!${axios})" ] From 3b54858434c8f381c54ecbfe9dabb8ae8454f426 Mon Sep 17 00:00:00 2001 From: Jim Grady Date: Mon, 6 Nov 2023 11:37:25 -0500 Subject: [PATCH 5/7] Port MergeDups goal to use redux-toolkit (#2704) * Move changes to state to reducer * Create getMergeWords action Co-authored-by: D. Ror --- .../MergeDupsStep/MergeDragDrop/DropWord.tsx | 11 +- .../MergeDupsStep/MergeDragDrop/index.tsx | 20 +- .../MergeDuplicates/MergeDupsTreeTypes.ts | 4 +- .../MergeDuplicates/Redux/MergeDupsActions.ts | 324 ++-------- .../MergeDuplicates/Redux/MergeDupsReducer.ts | 567 +++++++++++------- .../Redux/MergeDupsReduxTypes.ts | 100 +-- .../Redux/tests/MergeDupsActions.test.tsx | 268 ++------- .../Redux/tests/MergeDupsDataMock.ts | 280 ++++++++- .../Redux/tests/MergeDupsReducer.test.tsx | 161 +++-- src/rootReducer.ts | 2 +- 10 files changed, 929 insertions(+), 808 deletions(-) diff --git a/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DropWord.tsx b/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DropWord.tsx index 4fd9e7c98b..07aeddae21 100644 --- a/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DropWord.tsx +++ b/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DropWord.tsx @@ -43,7 +43,7 @@ export default function DropWord(props: DropWordProps): ReactElement { // reset vern if not in vern list if (treeWord && !verns.includes(treeWord.vern)) { - dispatch(setVern(props.wordId, verns[0] || "")); + dispatch(setVern({ wordId: props.wordId, vern: verns[0] || "" })); } return ( @@ -69,7 +69,12 @@ export default function DropWord(props: DropWordProps): ReactElement { variant="standard" value={treeWord.vern} onChange={(e) => - dispatch(setVern(props.wordId, e.target.value as string)) + dispatch( + setVern({ + wordId: props.wordId, + vern: e.target.value as string, + }) + ) } > {verns.map((vern) => ( @@ -94,7 +99,7 @@ export default function DropWord(props: DropWordProps): ReactElement { { - dispatch(flagWord(props.wordId, newFlag)); + dispatch(flagWord({ wordId: props.wordId, flag: newFlag })); }} buttonId={`word-${props.wordId}-flag`} /> diff --git a/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/index.tsx b/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/index.tsx index 3889aa5ac1..be213702a9 100644 --- a/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/index.tsx +++ b/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/index.tsx @@ -55,7 +55,13 @@ export default function MergeDragDrop(): ReactElement { // Case 2a: Cannot merge a protected sense into another sense. if (sourceId !== res.combine.droppableId) { // The target sense is in a different word, so move instead of combine. - dispatch(moveSense(senseRef, res.combine.droppableId, 0)); + dispatch( + moveSense({ + ref: senseRef, + destWordId: res.combine.droppableId, + destOrder: 0, + }) + ); } return; } @@ -66,7 +72,7 @@ export default function MergeDragDrop(): ReactElement { // Case 2b: If the target is a sidebar sub-sense, it cannot receive a combine. return; } - dispatch(combineSense(senseRef, combineRef)); + dispatch(combineSense({ src: senseRef, dest: combineRef })); } else if (res.destination) { const destId = res.destination.droppableId; // Case 3: The sense was dropped in a droppable. @@ -77,7 +83,13 @@ export default function MergeDragDrop(): ReactElement { return; } // Move the sense to the dest MergeWord. - dispatch(moveSense(senseRef, destId, res.destination.index)); + dispatch( + moveSense({ + ref: senseRef, + destWordId: destId, + destOrder: res.destination.index, + }) + ); } else { // Case 3b: The source & dest droppables are the same, so we reorder, not move. const order = res.destination.index; @@ -90,7 +102,7 @@ export default function MergeDragDrop(): ReactElement { // If the sense wasn't moved or was moved within the sidebar above a protected sense, do nothing. return; } - dispatch(orderSense(senseRef, order)); + dispatch(orderSense({ ref: senseRef, order: order })); } } } diff --git a/src/goals/MergeDuplicates/MergeDupsTreeTypes.ts b/src/goals/MergeDuplicates/MergeDupsTreeTypes.ts index 9d86a37c2c..f333343797 100644 --- a/src/goals/MergeDuplicates/MergeDupsTreeTypes.ts +++ b/src/goals/MergeDuplicates/MergeDupsTreeTypes.ts @@ -55,12 +55,12 @@ export function newMergeTreeWord( } export function convertSenseToMergeTreeSense( - sense?: Sense, + sense: Sense, srcWordId = "", order = 0 ): MergeTreeSense { return { - ...(sense ?? newSense()), + ...sense, srcWordId, order, protected: sense?.accessibility === Status.Protected, diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts b/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts index 31b8b06706..88a697a4ce 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts @@ -1,12 +1,6 @@ -import { - Definition, - Flag, - GramCatGroup, - MergeSourceWord, - MergeWords, - Status, - Word, -} from "api/models"; +import { Action, PayloadAction } from "@reduxjs/toolkit"; + +import { Word } from "api/models"; import * as backend from "backend"; import { addCompletedMergeToGoal, @@ -15,216 +9,94 @@ import { import { defaultSidebar, MergeTreeReference, - MergeTreeSense, Sidebar, } from "goals/MergeDuplicates/MergeDupsTreeTypes"; import { MergeDups, MergeStepData, ReviewDeferredDups, - newMergeWords, } from "goals/MergeDuplicates/MergeDupsTypes"; import { - ClearTreeMergeAction, - CombineSenseMergeAction, - DeleteSenseMergeAction, - FlagWord, - MergeTreeActionTypes, - MergeTreeState, - MoveDuplicateMergeAction, - MoveSenseMergeAction, - OrderDuplicateMergeAction, - OrderSenseMergeAction, - SetDataMergeAction, - SetSidebarMergeAction, - SetVernacularMergeAction, + clearMergeWordsAction, + clearTreeAction, + combineSenseAction, + deleteSenseAction, + flagWordAction, + getMergeWordsAction, + moveDuplicateAction, + moveSenseAction, + orderDuplicateAction, + orderSenseAction, + setDataAction, + setSidebarAction, + setVernacularAction, +} from "goals/MergeDuplicates/Redux/MergeDupsReducer"; +import { + CombineSenseMergePayload, + FlagWordPayload, + MoveSensePayload, + OrderSensePayload, + SetVernacularPayload, } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; import { StoreState } from "types"; import { StoreStateDispatch } from "types/Redux/actions"; -import { Hash } from "types/hash"; -import { compareFlags } from "utilities/wordUtilities"; -// Action Creators +// Action Creation Functions -export function clearTree(): ClearTreeMergeAction { - return { type: MergeTreeActionTypes.CLEAR_TREE }; +export function clearMergeWords(): Action { + return clearMergeWordsAction(); } -export function combineSense( - src: MergeTreeReference, - dest: MergeTreeReference -): CombineSenseMergeAction { - return { type: MergeTreeActionTypes.COMBINE_SENSE, payload: { src, dest } }; +export function clearTree(): Action { + return clearTreeAction(); } -export function deleteSense(src: MergeTreeReference): DeleteSenseMergeAction { - return { type: MergeTreeActionTypes.DELETE_SENSE, payload: { src } }; +export function combineSense(payload: CombineSenseMergePayload): PayloadAction { + return combineSenseAction(payload); } -export function flagWord(wordId: string, flag: Flag): FlagWord { - return { type: MergeTreeActionTypes.FLAG_WORD, payload: { wordId, flag } }; +export function deleteSense(payload: MergeTreeReference): PayloadAction { + return deleteSenseAction(payload); } -export function moveSense( - ref: MergeTreeReference, - destWordId: string, - destOrder: number -): MoveDuplicateMergeAction | MoveSenseMergeAction { - if (ref.order === undefined) { - return { - type: MergeTreeActionTypes.MOVE_SENSE, - payload: { ...ref, destWordId, destOrder }, - }; +export function flagWord(payload: FlagWordPayload): PayloadAction { + return flagWordAction(payload); +} + +export function getMergeWords(): Action { + return getMergeWordsAction(); +} + +export function moveSense(payload: MoveSensePayload): PayloadAction { + if (payload.ref.order === undefined) { + return moveSenseAction(payload); + } else { + return moveDuplicateAction(payload); } - // If ref.order is defined, the sense is being moved out of the sidebar. - return { - type: MergeTreeActionTypes.MOVE_DUPLICATE, - payload: { ref, destWordId, destOrder }, - }; } -export function orderSense( - ref: MergeTreeReference, - order: number -): OrderDuplicateMergeAction | OrderSenseMergeAction { - if (ref.order === undefined) { - return { - type: MergeTreeActionTypes.ORDER_SENSE, - payload: { ...ref, order }, - }; +export function orderSense(payload: OrderSensePayload): PayloadAction { + if (payload.ref.order === undefined) { + return orderSenseAction(payload); + } else { + return orderDuplicateAction(payload); } - // If ref.order is defined, the sense is being ordered within the sidebar. - return { - type: MergeTreeActionTypes.ORDER_DUPLICATE, - payload: { ref, order }, - }; } -export function setSidebar(sidebar?: Sidebar): SetSidebarMergeAction { - return { - type: MergeTreeActionTypes.SET_SIDEBAR, - payload: sidebar ?? defaultSidebar, - }; +export function setSidebar(sidebar?: Sidebar): PayloadAction { + return setSidebarAction(sidebar ?? defaultSidebar); } -export function setWordData(words: Word[]): SetDataMergeAction { - return { type: MergeTreeActionTypes.SET_DATA, payload: words }; +export function setData(words: Word[]): PayloadAction { + return setDataAction(words); } -export function setVern( - wordId: string, - vern: string -): SetVernacularMergeAction { - return { - type: MergeTreeActionTypes.SET_VERNACULAR, - payload: { wordId, vern }, - }; +export function setVern(payload: SetVernacularPayload): PayloadAction { + return setVernacularAction(payload); } // Dispatch Functions -// Given a wordId, constructs from the state the corresponding MergeWords. -// Returns the MergeWords, or undefined if the parent and child are identical. -function getMergeWords( - wordId: string, - mergeTree: MergeTreeState -): MergeWords | undefined { - // Find and build MergeSourceWord[]. - const word = mergeTree.tree.words[wordId]; - if (word) { - const data = mergeTree.data; - - // List of all non-deleted senses. - const nonDeleted = Object.values(mergeTree.tree.words).flatMap((w) => - Object.values(w.sensesGuids).flatMap((s) => s) - ); - - // Create list of all senses and add merge type tags slit by src word. - const senses: Hash = {}; - - // Build senses array. - for (const senseGuids of Object.values(word.sensesGuids)) { - for (const guid of senseGuids) { - const senseData = data.senses[guid]; - const wordId = senseData.srcWordId; - - if (!senses[wordId]) { - const dbWord = data.words[wordId]; - - // Add each sense into senses as separate or deleted. - senses[wordId] = []; - for (const sense of dbWord.senses) { - senses[wordId].push({ - ...sense, - srcWordId: wordId, - order: senses[wordId].length, - accessibility: nonDeleted.includes(sense.guid) - ? Status.Separate - : Status.Deleted, - protected: sense.accessibility === Status.Protected, - }); - } - } - } - } - - // Set sense and duplicate senses. - Object.values(word.sensesGuids).forEach((guids) => { - const sensesToCombine = guids - .map((g) => data.senses[g]) - .map((s) => senses[s.srcWordId][s.order]); - combineIntoFirstSense(sensesToCombine); - }); - - // Clean order of senses in each src word to reflect backend order. - Object.values(senses).forEach((wordSenses) => { - wordSenses = wordSenses.sort((a, b) => a.order - b.order); - senses[wordSenses[0].srcWordId] = wordSenses; - }); - - // Don't return empty merges: when the only child is the parent word - // and has the same number of senses as parent (all Active/Protected) - // and has the same flag. - if (Object.values(senses).length === 1) { - const onlyChild = Object.values(senses)[0]; - if ( - onlyChild[0].srcWordId === wordId && - onlyChild.length === data.words[wordId].senses.length && - !onlyChild.find( - (s) => ![Status.Active, Status.Protected].includes(s.accessibility) - ) && - compareFlags(word.flag, data.words[wordId].flag) === 0 - ) { - return undefined; - } - } - - // Construct parent and children. - const parent: Word = { ...data.words[wordId], senses: [], flag: word.flag }; - if (!parent.vernacular) { - parent.vernacular = word.vern; - } - const children: MergeSourceWord[] = Object.values(senses).map((sList) => { - sList.forEach((sense) => { - if ([Status.Active, Status.Protected].includes(sense.accessibility)) { - parent.senses.push({ - guid: sense.guid, - definitions: sense.definitions, - glosses: sense.glosses, - semanticDomains: sense.semanticDomains, - accessibility: sense.accessibility, - grammaticalInfo: sense.grammaticalInfo, - }); - } - }); - const getAudio = !sList.find((s) => s.accessibility === Status.Separate); - return { srcWordId: sList[0].srcWordId, getAudio }; - }); - - return newMergeWords(parent, children); - } -} - export function deferMerge() { return async (_: StoreStateDispatch, getState: () => StoreState) => { const mergeTree = getState().mergeDuplicateGoal; @@ -239,27 +111,11 @@ export function mergeAll() { // Add to blacklist. await backend.blacklistAdd(Object.keys(mergeTree.data.words)); - // Handle words with all senses deleted. - const possibleWords = Object.values(mergeTree.data.words); - const nonDeletedSenses = Object.values(mergeTree.tree.words).flatMap((w) => - Object.values(w.sensesGuids).flatMap((s) => s) - ); - const deletedWords = possibleWords.filter( - (w) => - !w.senses.map((s) => s.guid).find((g) => nonDeletedSenses.includes(g)) - ); - const mergeWordsArray = deletedWords.map((w) => - newMergeWords(w, [{ srcWordId: w.id, getAudio: false }], true) - ); - // Merge words. - const wordIds = Object.keys(mergeTree.tree.words); - wordIds.forEach((id) => { - const wordsToMerge = getMergeWords(id, mergeTree); - if (wordsToMerge) { - mergeWordsArray.push(wordsToMerge); - } - }); + dispatch(getMergeWords()); + + const mergeWordsArray = [...getState().mergeDuplicateGoal.mergeWords]; + dispatch(clearMergeWords()); if (mergeWordsArray.length) { const parentIds = await backend.mergeWords(mergeWordsArray); const childIds = [ @@ -268,10 +124,8 @@ export function mergeAll() { ), ]; const completedMerge = { childIds, parentIds }; - if (getState().goalsState.currentGoal) { - dispatch(addCompletedMergeToGoal(completedMerge)); - await dispatch(asyncUpdateGoal()); - } + dispatch(addCompletedMergeToGoal(completedMerge)); + await dispatch(asyncUpdateGoal()); } }; } @@ -283,59 +137,7 @@ export function dispatchMergeStepData(goal: MergeDups | ReviewDeferredDups) { const stepData = goal.steps[goal.currentStep] as MergeStepData; if (stepData) { const stepWords = stepData.words ?? []; - dispatch(setWordData(stepWords)); + dispatch(setData(stepWords)); } }; } - -/** Modifies the mutable input sense list. */ -export function combineIntoFirstSense(senses: MergeTreeSense[]): void { - // Set the first sense to be merged as Active/Protected. - // This was the top sense when the sidebar was opened. - const mainSense = senses[0]; - mainSense.accessibility = mainSense.protected - ? Status.Protected - : Status.Active; - - // Merge the rest as duplicates. - // These were senses dropped into another sense. - senses.slice(1).forEach((dupSense) => { - dupSense.accessibility = Status.Duplicate; - // Put the duplicate's definitions in the main sense. - dupSense.definitions.forEach((def) => - mergeDefinitionIntoSense(mainSense, def) - ); - // Use the duplicate's part of speech if not specified in the main sense. - if (mainSense.grammaticalInfo.catGroup === GramCatGroup.Unspecified) { - mainSense.grammaticalInfo = { ...dupSense.grammaticalInfo }; - } - // Put the duplicate's domains in the main sense. - dupSense.semanticDomains.forEach((dom) => { - if (!mainSense.semanticDomains.find((d) => d.id === dom.id)) { - mainSense.semanticDomains.push({ ...dom }); - } - }); - }); -} - -/** Modifies the mutable input sense. */ -export function mergeDefinitionIntoSense( - sense: MergeTreeSense, - def: Definition, - sep = ";" -): void { - if (!def.text.length) { - return; - } - const defIndex = sense.definitions.findIndex( - (d) => d.language === def.language - ); - if (defIndex === -1) { - sense.definitions.push({ ...def }); - } else { - const oldText = sense.definitions[defIndex].text; - if (!oldText.split(sep).includes(def.text)) { - sense.definitions[defIndex].text = `${oldText}${sep}${def.text}`; - } - } -} diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts b/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts index 90974b1d9d..9334e402db 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts @@ -1,76 +1,76 @@ +import { createSlice } from "@reduxjs/toolkit"; import { v4 } from "uuid"; -import { Word } from "api/models"; +import { + GramCatGroup, + MergeSourceWord, + MergeWords, + Status, + Word, +} from "api/models"; import { convertSenseToMergeTreeSense, convertWordToMergeTreeWord, defaultSidebar, defaultTree, - MergeTree, + MergeData, MergeTreeSense, MergeTreeWord, newMergeTreeWord, } from "goals/MergeDuplicates/MergeDupsTreeTypes"; -import { - MergeTreeAction, - MergeTreeActionTypes, - MergeTreeState, -} from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; -import { StoreAction, StoreActionTypes } from "rootActions"; +import { newMergeWords } from "goals/MergeDuplicates/MergeDupsTypes"; +import { MergeTreeState } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; +import { StoreActionTypes } from "rootActions"; import { Hash } from "types/hash"; +import { compareFlags } from "utilities/wordUtilities"; const defaultData = { words: {}, senses: {} }; export const defaultState: MergeTreeState = { data: defaultData, tree: defaultTree, + mergeWords: [], }; -export const mergeDupStepReducer = ( - state: MergeTreeState = defaultState, //createStore() calls each reducer with undefined state - action: MergeTreeAction | StoreAction -): MergeTreeState => { - switch (action.type) { - case MergeTreeActionTypes.CLEAR_TREE: { +const mergeDuplicatesSlice = createSlice({ + name: "mergeDupStepReducer", + initialState: defaultState, + reducers: { + clearMergeWordsAction: (state) => { + state.mergeWords = []; + }, + clearTreeAction: () => { return defaultState; - } - - case MergeTreeActionTypes.COMBINE_SENSE: { + }, + combineSenseAction: (state, action) => { const srcRef = action.payload.src; const destRef = action.payload.dest; // Ignore dropping a sense (or one of its sub-senses) into itself. - if (srcRef.mergeSenseId === destRef.mergeSenseId) { - return state; - } - - const words: Hash = JSON.parse( - JSON.stringify(state.tree.words) - ); - const srcWordId = srcRef.wordId; - const srcGuids = words[srcWordId].sensesGuids[srcRef.mergeSenseId]; - const destGuids: string[] = []; - if (srcRef.order === undefined || srcGuids.length === 1) { - destGuids.push(...srcGuids); - delete words[srcWordId].sensesGuids[srcRef.mergeSenseId]; - if (!Object.keys(words[srcWordId].sensesGuids).length) { - delete words[srcWordId]; + if (srcRef.mergeSenseId !== destRef.mergeSenseId) { + const words = state.tree.words; + const srcWordId = srcRef.wordId; + const srcGuids = words[srcWordId].sensesGuids[srcRef.mergeSenseId]; + const destGuids: string[] = []; + if (srcRef.order === undefined || srcGuids.length === 1) { + destGuids.push(...srcGuids); + delete words[srcWordId].sensesGuids[srcRef.mergeSenseId]; + if (!Object.keys(words[srcWordId].sensesGuids).length) { + delete words[srcWordId]; + } + } else { + destGuids.push(srcGuids.splice(srcRef.order, 1)[0]); } - } else { - destGuids.push(srcGuids.splice(srcRef.order, 1)[0]); - } - - words[destRef.wordId].sensesGuids[destRef.mergeSenseId].push( - ...destGuids - ); - return { ...state, tree: { ...state.tree, words } }; - } - - case MergeTreeActionTypes.DELETE_SENSE: { - const srcRef = action.payload.src; + words[destRef.wordId].sensesGuids[destRef.mergeSenseId].push( + ...destGuids + ); + state.tree.words = words; + } + }, + deleteSenseAction: (state, action) => { + const srcRef = action.payload; const srcWordId = srcRef.wordId; - const tree: MergeTree = JSON.parse(JSON.stringify(state.tree)); - const words = tree.words; + const words = state.tree.words; const sensesGuids = words[srcWordId].sensesGuids; if (srcRef.order !== undefined) { @@ -85,216 +85,365 @@ export const mergeDupStepReducer = ( delete words[srcWordId]; } - let sidebar = tree.sidebar; + const sidebar = state.tree.sidebar; + // If the sense is being deleted from the words column + // and the sense is also shown in the sidebar, + // then reset the sidebar. if ( sidebar.wordId === srcRef.wordId && sidebar.mergeSenseId === srcRef.mergeSenseId && srcRef.order === undefined ) { - sidebar = defaultSidebar; + state.tree.sidebar = defaultSidebar; } - - return { ...state, tree: { ...state.tree, words, sidebar } }; - } - - case MergeTreeActionTypes.FLAG_WORD: { - const words: Hash = JSON.parse( - JSON.stringify(state.tree.words) + }, + flagWordAction: (state, action) => { + state.tree.words[action.payload.wordId].flag = action.payload.flag; + }, + getMergeWordsAction: (state) => { + // Handle words with all senses deleted. + const possibleWords = Object.values(state.data.words); + // List of all non-deleted senses. + const nonDeletedSenses = Object.values(state.tree.words).flatMap((w) => + Object.values(w.sensesGuids).flatMap((s) => s) ); - words[action.payload.wordId].flag = action.payload.flag; - return { ...state, tree: { ...state.tree, words } }; - } - - case MergeTreeActionTypes.MOVE_DUPLICATE: { - const srcRef = action.payload.ref; - const destWordId = action.payload.destWordId; - const words: Hash = JSON.parse( - JSON.stringify(state.tree.words) + const deletedWords = possibleWords.filter( + (w) => + !w.senses.map((s) => s.guid).find((g) => nonDeletedSenses.includes(g)) + ); + state.mergeWords = deletedWords.map((w) => + newMergeWords(w, [{ srcWordId: w.id, getAudio: false }], true) ); - const srcWordId = srcRef.wordId; - let mergeSenseId = srcRef.mergeSenseId; - - // Get guid of sense being restored from the sidebar. - if (srcRef.order === undefined) { - return state; + for (const wordId in state.tree.words) { + const mergeWord = state.tree.words[wordId]; + const mergeSenses = buildSenses( + mergeWord.sensesGuids, + state.data, + nonDeletedSenses + ); + const mergeWords = createMergeWords( + wordId, + mergeWord, + mergeSenses, + state.data.words[wordId] + ); + if (mergeWords) { + state.mergeWords.push(mergeWords); + } } - const srcGuids = words[srcWordId].sensesGuids[mergeSenseId]; - const guid = srcGuids.splice(srcRef.order, 1)[0]; + }, + moveSenseAction: (state, action) => { + const srcWordId = action.payload.ref.wordId; + const destWordId = action.payload.destWordId; + const srcOrder = action.payload.ref.order; + if (srcOrder === undefined && srcWordId !== destWordId) { + const mergeSenseId = action.payload.ref.mergeSenseId; + + const words = state.tree.words; + + // Check if dropping the sense into a new word. + if (words[destWordId] === undefined) { + if (Object.keys(words[srcWordId].sensesGuids).length === 1) { + return; + } + words[destWordId] = newMergeTreeWord(); + } - // Check if dropping the sense into a new word. - if (words[destWordId] === undefined) { - words[destWordId] = newMergeTreeWord(); - } + // Update the destWord. + const guids = words[srcWordId].sensesGuids[mergeSenseId]; + const sensesPairs = Object.entries(words[destWordId].sensesGuids); + sensesPairs.splice(action.payload.destOrder, 0, [mergeSenseId, guids]); + const newSensesGuids: Hash = {}; + sensesPairs.forEach(([key, value]) => (newSensesGuids[key] = value)); + words[destWordId].sensesGuids = newSensesGuids; - if (srcGuids.length === 0) { - // If there are no guids left, this is a full move. - if (srcWordId === destWordId) { - return state; - } + // Cleanup the srcWord. delete words[srcWordId].sensesGuids[mergeSenseId]; if (!Object.keys(words[srcWordId].sensesGuids).length) { delete words[srcWordId]; } - } else { - // Otherwise, create a new sense in the destWord. - mergeSenseId = v4(); } + }, + moveDuplicateAction: (state, action) => { + const srcRef = action.payload.ref; + // Verify that the ref.order field is defined + if (srcRef.order !== undefined) { + const destWordId = action.payload.destWordId; + const words = state.tree.words; - // Update the destWord. - const sensesPairs = Object.entries(words[destWordId].sensesGuids); - sensesPairs.splice(action.payload.destOrder, 0, [mergeSenseId, [guid]]); - const newSensesGuids: Hash = {}; - sensesPairs.forEach(([key, value]) => (newSensesGuids[key] = value)); - words[destWordId].sensesGuids = newSensesGuids; - - return { ...state, tree: { ...state.tree, words } }; - } - - case MergeTreeActionTypes.MOVE_SENSE: { - const srcWordId = action.payload.wordId; - const mergeSenseId = action.payload.mergeSenseId; - const destWordId = action.payload.destWordId; + const srcWordId = srcRef.wordId; + let mergeSenseId = srcRef.mergeSenseId; - if (srcWordId === destWordId) { - return state; - } - const words: Hash = JSON.parse( - JSON.stringify(state.tree.words) - ); + // Get guid of sense being restored from the sidebar. + const srcGuids = words[srcWordId].sensesGuids[mergeSenseId]; + const guid = srcGuids.splice(srcRef.order, 1)[0]; - // Check if dropping the sense into a new word. - if (words[destWordId] === undefined) { - if (Object.keys(words[srcWordId].sensesGuids).length === 1) { - return state; + // Check if dropping the sense into a new word. + if (words[destWordId] === undefined) { + words[destWordId] = newMergeTreeWord(); } - words[destWordId] = newMergeTreeWord(); - } - // Update the destWord. - const guids = [...words[srcWordId].sensesGuids[mergeSenseId]]; - const sensesPairs = Object.entries(words[destWordId].sensesGuids); - sensesPairs.splice(action.payload.destOrder, 0, [mergeSenseId, guids]); - const newSensesGuids: Hash = {}; - sensesPairs.forEach(([key, value]) => (newSensesGuids[key] = value)); - words[destWordId].sensesGuids = newSensesGuids; + if (srcGuids.length === 0) { + // If there are no guids left, this is a full move. + if (srcWordId === destWordId) { + return; + } + delete words[srcWordId].sensesGuids[mergeSenseId]; + if (!Object.keys(words[srcWordId].sensesGuids).length) { + delete words[srcWordId]; + } + } else { + // Otherwise, create a new sense in the destWord. + mergeSenseId = v4(); + } - // Cleanup the srcWord. - delete words[srcWordId].sensesGuids[mergeSenseId]; - if (!Object.keys(words[srcWordId].sensesGuids).length) { - delete words[srcWordId]; + // Update the destWord. + const sensesPairs = Object.entries(words[destWordId].sensesGuids); + sensesPairs.splice(action.payload.destOrder, 0, [mergeSenseId, [guid]]); + const newSensesGuids: Hash = {}; + sensesPairs.forEach(([key, value]) => (newSensesGuids[key] = value)); + words[destWordId].sensesGuids = newSensesGuids; } - - return { ...state, tree: { ...state.tree, words } }; - } - - case MergeTreeActionTypes.ORDER_DUPLICATE: { + }, + orderDuplicateAction: (state, action) => { const ref = action.payload.ref; const oldOrder = ref.order; const newOrder = action.payload.order; // Ensure the reorder is valid. - if (oldOrder === undefined || oldOrder === newOrder) { - return state; - } - - // Move the guid. - const oldSensesGuids = state.tree.words[ref.wordId].sensesGuids; - const guids = [...oldSensesGuids[ref.mergeSenseId]]; - const guid = guids.splice(oldOrder, 1)[0]; - guids.splice(newOrder, 0, guid); - - // - const sensesGuids = { ...oldSensesGuids }; - sensesGuids[ref.mergeSenseId] = guids; - - const word: MergeTreeWord = { - ...state.tree.words[ref.wordId], - sensesGuids, - }; - - const words = { ...state.tree.words }; - words[ref.wordId] = word; + if (oldOrder !== undefined && oldOrder !== newOrder) { + // Move the guid. + const oldSensesGuids = state.tree.words[ref.wordId].sensesGuids; + const guids = [...oldSensesGuids[ref.mergeSenseId]]; + const guid = guids.splice(oldOrder, 1)[0]; + guids.splice(newOrder, 0, guid); - return { ...state, tree: { ...state.tree, words } }; - } + const sensesGuids = { ...oldSensesGuids }; + sensesGuids[ref.mergeSenseId] = guids; - case MergeTreeActionTypes.ORDER_SENSE: { - const word: MergeTreeWord = JSON.parse( - JSON.stringify(state.tree.words[action.payload.wordId]) - ); + state.tree.words[ref.wordId].sensesGuids = sensesGuids; + } + }, + orderSenseAction: (state, action) => { + const word = state.tree.words[action.payload.ref.wordId]; // Convert the Hash to an array to expose the order. const sensePairs = Object.entries(word.sensesGuids); - const mergeSenseId = action.payload.mergeSenseId; + const mergeSenseId = action.payload.ref.mergeSenseId; const oldOrder = sensePairs.findIndex((p) => p[0] === mergeSenseId); const newOrder = action.payload.order; // Ensure the move is valid. - if (oldOrder === -1 || newOrder === undefined || oldOrder === newOrder) { - return state; - } - - // Move the sense pair to its new place. - const pair = sensePairs.splice(oldOrder, 1)[0]; - sensePairs.splice(newOrder, 0, pair); + if (oldOrder !== -1 && newOrder !== undefined && oldOrder !== newOrder) { + // Move the sense pair to its new place. + const pair = sensePairs.splice(oldOrder, 1)[0]; + sensePairs.splice(newOrder, 0, pair); + + // Rebuild the Hash. + word.sensesGuids = {}; + for (const [key, value] of sensePairs) { + word.sensesGuids[key] = value; + } - // Rebuild the Hash. - word.sensesGuids = {}; - for (const [key, value] of sensePairs) { - word.sensesGuids[key] = value; + state.tree.words[action.payload.ref.wordId] = word; } - - const words = { ...state.tree.words }; - words[action.payload.wordId] = word; - - return { ...state, tree: { ...state.tree, words } }; - } - - case MergeTreeActionTypes.SET_DATA: { + }, + setSidebarAction: (state, action) => { + state.tree.sidebar = action.payload; + }, + setDataAction: (state, action) => { if (action.payload.length === 0) { - return defaultState; - } - const words: Hash = {}; - const senses: Hash = {}; - const wordsTree: Hash = {}; - action.payload.forEach((word) => { - words[word.id] = JSON.parse(JSON.stringify(word)); - word.senses.forEach((s, order) => { - senses[s.guid] = convertSenseToMergeTreeSense(s, word.id, order); + state = defaultState; + } else { + const words: Hash = {}; + const senses: Hash = {}; + const wordsTree: Hash = {}; + action.payload.forEach((word: Word) => { + words[word.id] = JSON.parse(JSON.stringify(word)); + word.senses.forEach((s, order) => { + senses[s.guid] = convertSenseToMergeTreeSense(s, word.id, order); + }); + wordsTree[word.id] = convertWordToMergeTreeWord(word); }); - wordsTree[word.id] = convertWordToMergeTreeWord(word); - }); - return { - ...state, - tree: { ...state.tree, words: wordsTree }, - data: { senses, words }, - }; - } - - case MergeTreeActionTypes.SET_SIDEBAR: { - const sidebar = action.payload; - return { ...state, tree: { ...state.tree, sidebar } }; + state.tree.words = wordsTree; + state.data = { senses, words }; + state.mergeWords = []; + } + }, + setVernacularAction: (state, action) => { + state.tree.words[action.payload.wordId].vern = action.payload.vern; + }, + }, + extraReducers: (builder) => + builder.addCase(StoreActionTypes.RESET, () => defaultState), +}); + +// Helper Functions + +/** Create hash of senses keyed by id of src word. */ +function buildSenses( + sensesGuids: Hash, + data: MergeData, + nonDeletedSenses: string[] +): Hash { + const senses: Hash = {}; + for (const senseGuids of Object.values(sensesGuids)) { + for (const guid of senseGuids) { + const senseData = data.senses[guid]; + const wordId = senseData.srcWordId; + + if (!senses[wordId]) { + const dbWord = data.words[wordId]; + + // Add each sense into senses as separate or deleted. + senses[wordId] = []; + for (const sense of dbWord.senses) { + senses[wordId].push({ + ...sense, + srcWordId: wordId, + order: senses[wordId].length, + accessibility: nonDeletedSenses.includes(sense.guid) + ? Status.Separate + : Status.Deleted, + protected: sense.accessibility === Status.Protected, + }); + } + } } + } - case MergeTreeActionTypes.SET_VERNACULAR: { - const word = { ...state.tree.words[action.payload.wordId] }; - word.vern = action.payload.vern; - - const words = { ...state.tree.words }; - words[action.payload.wordId] = word; - - return { ...state, tree: { ...state.tree, words } }; + // Set sense and duplicate senses. + Object.values(sensesGuids).forEach((guids) => { + const sensesToCombine = guids + .map((g) => data.senses[g]) + .map((s) => senses[s.srcWordId][s.order]); + combineIntoFirstSense(sensesToCombine); + }); + + // Clean order of senses in each src word to reflect backend order. + Object.values(senses).forEach((wordSenses) => { + wordSenses = wordSenses.sort((a, b) => a.order - b.order); + senses[wordSenses[0].srcWordId] = wordSenses; + }); + + return senses; +} + +function createMergeWords( + wordId: string, + mergeWord: MergeTreeWord, + mergeSenses: Hash, + word: Word +): MergeWords | undefined { + // Don't return empty merges: when the only child is the parent word + // and has the same number of senses as parent (all Active/Protected) + // and has the same flag. + if (Object.values(mergeSenses).length === 1) { + const onlyChild = Object.values(mergeSenses)[0]; + if ( + onlyChild[0].srcWordId === wordId && + onlyChild.length === word.senses.length && + !onlyChild.find( + (s) => ![Status.Active, Status.Protected].includes(s.accessibility) + ) && + compareFlags(mergeWord.flag, word.flag) === 0 + ) { + return; } + } - case StoreActionTypes.RESET: { - return defaultState; + // Construct parent and children. + const parent: Word = { + ...word, + senses: [], + flag: mergeWord.flag, + }; + if (!parent.vernacular) { + parent.vernacular = mergeWord.vern; + } + const children: MergeSourceWord[] = Object.values(mergeSenses).map( + (sList) => { + sList.forEach((sense) => { + if ([Status.Active, Status.Protected].includes(sense.accessibility)) { + parent.senses.push({ + guid: sense.guid, + definitions: sense.definitions, + glosses: sense.glosses, + semanticDomains: sense.semanticDomains, + accessibility: sense.accessibility, + grammaticalInfo: sense.grammaticalInfo, + }); + } + }); + const getAudio = !sList.find((s) => s.accessibility === Status.Separate); + return { srcWordId: sList[0].srcWordId, getAudio }; } - - default: { - return state; + ); + + return newMergeWords(parent, children); +} + +function combineIntoFirstSense(senses: MergeTreeSense[]): void { + // Set the first sense to be merged as Active/Protected. + // This was the top sense when the sidebar was opened. + const mainSense = senses[0]; + mainSense.accessibility = mainSense.protected + ? Status.Protected + : Status.Active; + + // Merge the rest as duplicates. + // These were senses dropped into another sense. + senses.slice(1).forEach((dupSense) => { + dupSense.accessibility = Status.Duplicate; + // Put the duplicate's definitions in the main sense. + const sep = ";"; + dupSense.definitions.forEach((def) => { + if (def.text.length) { + const defIndex = mainSense.definitions.findIndex( + (d) => d.language === def.language + ); + if (defIndex === -1) { + mainSense.definitions.push({ ...def }); + } else { + const oldText = mainSense.definitions[defIndex].text; + if (!oldText.split(sep).includes(def.text)) { + mainSense.definitions[ + defIndex + ].text = `${oldText}${sep}${def.text}`; + } + } + } + }); + // Use the duplicate's part of speech if not specified in the main sense. + if (mainSense.grammaticalInfo.catGroup === GramCatGroup.Unspecified) { + mainSense.grammaticalInfo = { ...dupSense.grammaticalInfo }; } - } -}; + // Put the duplicate's domains in the main sense. + dupSense.semanticDomains.forEach((dom) => { + if (!mainSense.semanticDomains.find((d) => d.id === dom.id)) { + mainSense.semanticDomains.push({ ...dom }); + } + }); + }); +} + +export const { + clearMergeWordsAction, + clearTreeAction, + combineSenseAction, + deleteSenseAction, + flagWordAction, + getMergeWordsAction, + moveDuplicateAction, + moveSenseAction, + orderDuplicateAction, + orderSenseAction, + setDataAction, + setSidebarAction, + setVernacularAction, +} = mergeDuplicatesSlice.actions; + +export default mergeDuplicatesSlice.reducer; diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts b/src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts index b679ec586c..9f526b59cb 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts @@ -1,98 +1,38 @@ -import { Flag, Word } from "api/models"; +import { Flag, MergeWords } from "api/models"; import { MergeData, MergeTree, MergeTreeReference, - Sidebar, } from "goals/MergeDuplicates/MergeDupsTreeTypes"; -export enum MergeTreeActionTypes { - CLEAR_TREE = "CLEAR_TREE", - COMBINE_SENSE = "COMBINE_SENSE", - DELETE_SENSE = "DELETE_SENSE", - FLAG_WORD = "FLAG_WORD", - MOVE_DUPLICATE = "MOVE_DUPLICATE", - MOVE_SENSE = "MOVE_SENSE", - ORDER_DUPLICATE = "ORDER_DUPLICATE", - ORDER_SENSE = "ORDER_SENSE", - SET_DATA = "SET_DATA", - SET_SIDEBAR = "SET_SIDEBAR", - SET_VERNACULAR = "SET_VERNACULAR", +export interface CombineSenseMergePayload { + src: MergeTreeReference; + dest: MergeTreeReference; +} + +export interface FlagWordPayload { + wordId: string; + flag: Flag; } export interface MergeTreeState { data: MergeData; tree: MergeTree; + mergeWords: MergeWords[]; } -export interface ClearTreeMergeAction { - type: MergeTreeActionTypes.CLEAR_TREE; -} - -export interface CombineSenseMergeAction { - type: MergeTreeActionTypes.COMBINE_SENSE; - payload: { src: MergeTreeReference; dest: MergeTreeReference }; -} - -export interface DeleteSenseMergeAction { - type: MergeTreeActionTypes.DELETE_SENSE; - payload: { src: MergeTreeReference }; -} - -export interface FlagWord { - type: MergeTreeActionTypes.FLAG_WORD; - payload: { wordId: string; flag: Flag }; -} - -export interface MoveDuplicateMergeAction { - type: MergeTreeActionTypes.MOVE_DUPLICATE; - payload: { ref: MergeTreeReference; destWordId: string; destOrder: number }; +export interface MoveSensePayload { + ref: MergeTreeReference; + destWordId: string; + destOrder: number; } -export interface MoveSenseMergeAction { - type: MergeTreeActionTypes.MOVE_SENSE; - payload: { - wordId: string; - mergeSenseId: string; - destWordId: string; - destOrder: number; - }; +export interface OrderSensePayload { + ref: MergeTreeReference; + order: number; } -export interface OrderDuplicateMergeAction { - type: MergeTreeActionTypes.ORDER_DUPLICATE; - payload: { ref: MergeTreeReference; order: number }; +export interface SetVernacularPayload { + wordId: string; + vern: string; } - -export interface OrderSenseMergeAction { - type: MergeTreeActionTypes.ORDER_SENSE; - payload: MergeTreeReference; -} - -export interface SetDataMergeAction { - type: MergeTreeActionTypes.SET_DATA; - payload: Word[]; -} - -export interface SetSidebarMergeAction { - type: MergeTreeActionTypes.SET_SIDEBAR; - payload: Sidebar; -} - -export interface SetVernacularMergeAction { - type: MergeTreeActionTypes.SET_VERNACULAR; - payload: { wordId: string; vern: string }; -} - -export type MergeTreeAction = - | ClearTreeMergeAction - | CombineSenseMergeAction - | DeleteSenseMergeAction - | FlagWord - | MoveDuplicateMergeAction - | MoveSenseMergeAction - | OrderDuplicateMergeAction - | OrderSenseMergeAction - | SetDataMergeAction - | SetSidebarMergeAction - | SetVernacularMergeAction; diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx index e026ecf36e..c5fcccc0b2 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDupsActions.test.tsx @@ -1,42 +1,23 @@ -import configureMockStore from "redux-mock-store"; -import thunk from "redux-thunk"; - -import { GramCatGroup, MergeWords, Sense, Status, Word } from "api/models"; +import { MergeWords, Sense, Status, Word } from "api/models"; +import { defaultState } from "components/App/DefaultState"; import { defaultTree, MergeData, MergeTree, - MergeTreeReference, - MergeTreeSense, newMergeTreeSense, newMergeTreeWord, } from "goals/MergeDuplicates/MergeDupsTreeTypes"; import { MergeDups, newMergeWords } from "goals/MergeDuplicates/MergeDupsTypes"; import { - combineIntoFirstSense, + deferMerge, dispatchMergeStepData, mergeAll, - mergeDefinitionIntoSense, - moveSense, - orderSense, + setData, } from "goals/MergeDuplicates/Redux/MergeDupsActions"; -import { - MergeTreeAction, - MergeTreeActionTypes, - MergeTreeState, -} from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; import { goalDataMock } from "goals/MergeDuplicates/Redux/tests/MergeDupsDataMock"; -import { GoalsState, GoalType } from "types/goals"; -import { newSemanticDomain } from "types/semanticDomain"; -import { - multiSenseWord, - newDefinition, - newFlag, - newGrammaticalInfo, - newSense, - newWord, -} from "types/word"; -import { Bcp47Code } from "types/writingSystem"; +import { setupStore } from "store"; +import { GoalType } from "types/goals"; +import { multiSenseWord, newFlag, newWord } from "types/word"; // Used when the guids don't matter. function wordAnyGuids(vern: string, senses: Sense[], id: string): Word { @@ -48,11 +29,13 @@ function wordAnyGuids(vern: string, senses: Sense[], id: string): Word { }; } +const mockGraylistAdd = jest.fn(); const mockMergeWords = jest.fn(); jest.mock("backend", () => ({ blacklistAdd: jest.fn(), getWord: jest.fn(), + graylistAdd: () => mockGraylistAdd(), mergeWords: (mergeWordsArray: MergeWords[]) => mockMergeWords(mergeWordsArray), })); @@ -60,12 +43,9 @@ jest.mock("backend", () => ({ const mockGoal = new MergeDups(); mockGoal.data = goalDataMock; mockGoal.steps = [{ words: [] }, { words: [] }]; -const createMockStore = configureMockStore([thunk]); -const mockStoreState: { - goalsState: GoalsState; - mergeDuplicateGoal: MergeTreeState; -} = { +const preloadedState = { + ...defaultState, goalsState: { allGoalTypes: [], currentGoal: new MergeDups(), @@ -73,7 +53,12 @@ const mockStoreState: { history: [mockGoal], previousGoalType: GoalType.Default, }, - mergeDuplicateGoal: { data: {} as MergeData, tree: {} as MergeTree }, + mergeDuplicateGoal: { + data: {} as MergeData, + tree: {} as MergeTree, + mergeWords: [], + }, + _persist: { version: 1, rehydrated: false }, }; const vernA = "AAA"; @@ -112,11 +97,11 @@ describe("MergeDupActions", () => { const WA = newMergeTreeWord(vernA, { ID1: [S1], ID2: [S2] }); const WB = newMergeTreeWord(vernB, { ID1: [S3], ID2: [S4] }); const tree: MergeTree = { ...defaultTree, words: { WA, WB } }; - const mockStore = createMockStore({ - ...mockStoreState, - mergeDuplicateGoal: { data, tree }, + const store = setupStore({ + ...preloadedState, + mergeDuplicateGoal: { data, tree, mergeWords: [] }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).not.toHaveBeenCalled(); }); @@ -126,11 +111,11 @@ describe("MergeDupActions", () => { const WA = newMergeTreeWord(vernA, { ID1: [S1, S3], ID2: [S2] }); const WB = newMergeTreeWord(vernB, { ID1: [S4] }); const tree: MergeTree = { ...defaultTree, words: { WA, WB } }; - const mockStore = createMockStore({ - ...mockStoreState, - mergeDuplicateGoal: { data, tree }, + const store = setupStore({ + ...preloadedState, + mergeDuplicateGoal: { data, tree, mergeWords: [] }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); const parentA = wordAnyGuids(vernA, [senses["S1"], senses["S2"]], idA); @@ -151,11 +136,11 @@ describe("MergeDupActions", () => { const WA = newMergeTreeWord(vernA, { ID1: [S1], ID2: [S2], ID3: [S3] }); const WB = newMergeTreeWord(vernB, { ID1: [S4] }); const tree: MergeTree = { ...defaultTree, words: { WA, WB } }; - const mockStore = createMockStore({ - ...mockStoreState, - mergeDuplicateGoal: { data, tree }, + const store = setupStore({ + ...preloadedState, + mergeDuplicateGoal: { data, tree, mergeWords: [] }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); const parentA = wordAnyGuids( @@ -180,11 +165,11 @@ describe("MergeDupActions", () => { const WA = newMergeTreeWord(vernA, { ID1: [S1, S2] }); const WB = newMergeTreeWord(vernB, { ID1: [S3], ID2: [S4] }); const tree: MergeTree = { ...defaultTree, words: { WA, WB } }; - const mockStore = createMockStore({ - ...mockStoreState, - mergeDuplicateGoal: { data, tree }, + const store = setupStore({ + ...preloadedState, + mergeDuplicateGoal: { data, tree, mergeWords: [] }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); @@ -199,11 +184,11 @@ describe("MergeDupActions", () => { const WA = newMergeTreeWord(vernA, { ID1: [S1] }); const WB = newMergeTreeWord(vernB, { ID1: [S3], ID2: [S4] }); const tree: MergeTree = { ...defaultTree, words: { WA, WB } }; - const mockStore = createMockStore({ - ...mockStoreState, - mergeDuplicateGoal: { data, tree }, + const store = setupStore({ + ...preloadedState, + mergeDuplicateGoal: { data, tree, mergeWords: [] }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); const parent = wordAnyGuids(vernA, [senses["S1"]], idA); @@ -216,11 +201,11 @@ describe("MergeDupActions", () => { it("delete all senses from a word", async () => { const WA = newMergeTreeWord(vernA, { ID1: [S1], ID2: [S2] }); const tree: MergeTree = { ...defaultTree, words: { WA } }; - const mockStore = createMockStore({ - ...mockStoreState, - mergeDuplicateGoal: { data, tree }, + const store = setupStore({ + ...preloadedState, + mergeDuplicateGoal: { data, tree, mergeWords: [] }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); const child = { srcWordId: idB, getAudio: false }; @@ -234,11 +219,11 @@ describe("MergeDupActions", () => { WA.flag = newFlag("New flag"); const WB = newMergeTreeWord(vernB, { ID1: [S3], ID2: [S4] }); const tree: MergeTree = { ...defaultTree, words: { WA, WB } }; - const mockStore = createMockStore({ - ...mockStoreState, - mergeDuplicateGoal: { data, tree }, + const store = setupStore({ + ...preloadedState, + mergeDuplicateGoal: { data, tree, mergeWords: [] }, }); - await mockStore.dispatch(mergeAll()); + await store.dispatch(mergeAll()); expect(mockMergeWords).toHaveBeenCalledTimes(1); @@ -251,158 +236,29 @@ describe("MergeDupActions", () => { }); describe("dispatchMergeStepData", () => { - it("creates an action to add MergeDups data", async () => { + it("creates an action to add MergeDups data", () => { const goal = new MergeDups(); goal.steps = [{ words: [...goalDataMock.plannedWords[0]] }]; - const mockStore = createMockStore(); - await mockStore.dispatch(dispatchMergeStepData(goal)); - const setWordData: MergeTreeAction = { - type: MergeTreeActionTypes.SET_DATA, - payload: [...goalDataMock.plannedWords[0]], - }; - expect(mockStore.getActions()).toEqual([setWordData]); - }); - }); - - describe("moveSense", () => { - const wordId = "mockWordId"; - const mergeSenseId = "mockSenseId"; - - it("creates a MOVE_SENSE action when going from word to word", () => { - const mockRef: MergeTreeReference = { wordId, mergeSenseId }; - const resultAction = moveSense(mockRef, wordId, -1); - expect(resultAction.type).toEqual(MergeTreeActionTypes.MOVE_SENSE); - }); - - it("creates a MOVE_DUPLICATE action when going from sidebar to word", () => { - const mockRef: MergeTreeReference = { wordId, mergeSenseId, order: 0 }; - const resultAction = moveSense(mockRef, wordId, -1); - expect(resultAction.type).toEqual(MergeTreeActionTypes.MOVE_DUPLICATE); + const store = setupStore(); + store.dispatch(dispatchMergeStepData(goal)); + const setDataAction = setData(goalDataMock.plannedWords[0]); + expect(setDataAction.type).toEqual("mergeDupStepReducer/setDataAction"); }); }); - describe("orderSense", () => { - const wordId = "mockWordId"; - const mergeSenseId = "mockSenseId"; - const mockOrder = 0; - - it("creates an ORDER_SENSE action when moving within a word", () => { - const mockRef: MergeTreeReference = { wordId, mergeSenseId }; - const resultAction = orderSense(mockRef, mockOrder); - expect(resultAction.type).toEqual(MergeTreeActionTypes.ORDER_SENSE); - }); - - it("creates an ORDER_DUPLICATE action when moving within the sidebar", () => { - const mockRef: MergeTreeReference = { wordId, mergeSenseId, order: 0 }; - const resultAction = orderSense(mockRef, mockOrder); - expect(resultAction.type).toEqual(MergeTreeActionTypes.ORDER_DUPLICATE); - }); - }); - - describe("mergeDefinitionIntoSense", () => { - const defAEn = newDefinition("a", Bcp47Code.En); - const defAFr = newDefinition("a", Bcp47Code.Fr); - const defBEn = newDefinition("b", Bcp47Code.En); - let sense: MergeTreeSense; - - beforeEach(() => { - sense = newSense() as MergeTreeSense; - }); - - it("ignores definitions with empty text", () => { - mergeDefinitionIntoSense(sense, newDefinition()); - expect(sense.definitions).toHaveLength(0); - mergeDefinitionIntoSense(sense, newDefinition("", Bcp47Code.En)); - expect(sense.definitions).toHaveLength(0); - }); - - it("adds definitions with new languages", () => { - mergeDefinitionIntoSense(sense, defAEn); - expect(sense.definitions).toHaveLength(1); - mergeDefinitionIntoSense(sense, defAFr); - expect(sense.definitions).toHaveLength(2); - }); - - it("only adds definitions with new text", () => { - sense.definitions.push({ ...defAEn }, { ...defAFr }); - - mergeDefinitionIntoSense(sense, defAFr); - expect(sense.definitions).toHaveLength(2); - expect( - sense.definitions.find((d) => d.language === Bcp47Code.Fr)!.text - ).toEqual(defAFr.text); - - const twoEnTexts = `${defAEn.text};${defBEn.text}`; - mergeDefinitionIntoSense(sense, defBEn); - expect(sense.definitions).toHaveLength(2); - expect( - sense.definitions.find((d) => d.language === Bcp47Code.En)!.text - ).toEqual(twoEnTexts); - mergeDefinitionIntoSense(sense, defAEn); - expect(sense.definitions).toHaveLength(2); - expect( - sense.definitions.find((d) => d.language === Bcp47Code.En)!.text - ).toEqual(twoEnTexts); - }); - }); - - describe("combineIntoFirstSense", () => { - it("sets all but the first sense to duplicate status", () => { - const s4 = [newSense(), newSense(), newSense(), newSense()].map( - (s) => s as MergeTreeSense - ); - combineIntoFirstSense(s4); - expect(s4[0].accessibility).not.toBe(Status.Duplicate); - expect( - s4.filter((s) => s.accessibility === Status.Duplicate) - ).toHaveLength(s4.length - 1); - }); - - it("gives the first sense the earliest part of speech found in all senses", () => { - const s3 = [newSense(), newSense(), newSense()].map( - (s) => s as MergeTreeSense - ); - const gramInfo = { - catGroup: GramCatGroup.Verb, - grammaticalCategory: "vt", - }; - s3[1].grammaticalInfo = { ...gramInfo }; - s3[2].grammaticalInfo = { - catGroup: GramCatGroup.Preverb, - grammaticalCategory: "prev", - }; - combineIntoFirstSense(s3); - expect(s3[0].grammaticalInfo).toEqual(gramInfo); - - // Ensure the first sense's grammaticalInfo doesn't get overwritten. - s3[1].grammaticalInfo = newGrammaticalInfo(); - combineIntoFirstSense(s3); - expect(s3[0].grammaticalInfo).toEqual(gramInfo); - }); - - it("adds domains to first sense from other senses", () => { - const s3 = [newSense(), newSense(), newSense()].map( - (s) => s as MergeTreeSense - ); - s3[1].semanticDomains = [ - newSemanticDomain("1", "uno"), - newSemanticDomain("2", "dos"), - ]; - s3[2].semanticDomains = [newSemanticDomain("3", "three")]; - combineIntoFirstSense(s3); - expect(s3[0].semanticDomains).toHaveLength(3); - }); - - it("doesn't adds domains it already has", () => { - const s2 = [newSense(), newSense()].map((s) => s as MergeTreeSense); - s2[0].semanticDomains = [newSemanticDomain("1", "one")]; - s2[1].semanticDomains = [ - newSemanticDomain("1", "uno"), - newSemanticDomain("2", "dos"), - ]; - combineIntoFirstSense(s2); - expect(s2[0].semanticDomains).toHaveLength(2); + describe("deferMerge", () => { + it("add merge to graylist", () => { + const WA = newMergeTreeWord(vernA, { ID1: [S1], ID2: [S2] }); + WA.flag = newFlag("New flag"); + const WB = newMergeTreeWord(vernB, { ID1: [S3], ID2: [S4] }); + const tree: MergeTree = { ...defaultTree, words: { WA, WB } }; + const store = setupStore({ + ...preloadedState, + mergeDuplicateGoal: { data, tree, mergeWords: [] }, + }); + store.dispatch(deferMerge()); + expect(mockGraylistAdd).toHaveBeenCalledTimes(1); }); }); }); diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDupsDataMock.ts b/src/goals/MergeDuplicates/Redux/tests/MergeDupsDataMock.ts index c4786a8cef..0f70b78534 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDupsDataMock.ts +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDupsDataMock.ts @@ -1,6 +1,14 @@ -import { Word } from "api/models"; +import type { PreloadedState } from "@reduxjs/toolkit"; +import { Definition, SemanticDomain, Word } from "api/models"; +import { defaultState } from "components/App/DefaultState"; +import { + convertSenseToMergeTreeSense, + convertWordToMergeTreeWord, + newMergeTreeWord, +} from "goals/MergeDuplicates/MergeDupsTreeTypes"; import { MergeDupsData } from "goals/MergeDuplicates/MergeDupsTypes"; -import { simpleWord } from "types/word"; +import { RootState } from "store"; +import { newSense, newWord, simpleWord } from "types/word"; const wordsArrayMock = (): Word[] => [ // Each simpleWord() has a randomly generated id @@ -26,3 +34,271 @@ export const goalDataMock: MergeDupsData = { wordsArrayMock(), ], }; + +// Words/Senses to be used for a preloaded mergeDuplicateGoal state +// in the unit tests for MergeDuplicates Actions/Reducer +const semDomSocial: SemanticDomain = { + guid: "00000000-0000-0000-0000-000000000000", + name: "Social behavior", + id: "4", + lang: "", +}; + +const semDomLanguage: SemanticDomain = { + guid: "00000000-0000-0000-0000-000000000000", + name: "Language and thought", + id: "3", + lang: "", +}; + +const definitionBah = { language: "en", text: "defBah" }; +const definitionBag = { language: "en", text: "defBag" }; +const definitionBagBah = { language: "en", text: "defBag;defBah" }; + +const senseBag = { + ...newSense("bag"), + guid: "guid-sense-bag", + semanticDomains: [semDomLanguage], + definitions: [definitionBag], +}; +const senseBah = { + ...newSense("bah"), + guid: "guid-sense-bah", + semanticDomains: [semDomSocial], + definitions: [definitionBah], +}; +const senseBar = { + ...newSense("bar"), + guid: "guid-sense-bar", + semanticDomains: [semDomLanguage], +}; +const senseBaz = { ...newSense("baz"), guid: "guid-sense-baz" }; + +const wordFoo1 = { ...newWord("foo"), id: "wordId-foo1", senses: [senseBah] }; +const wordFoo2 = { + ...newWord("foo"), + id: "wordId-foo2", + senses: [senseBar, senseBaz], +}; + +// Preloaded values for store when testing the MergeDups Goal +const persistedDefaultState: PreloadedState = { + ...defaultState, + _persist: { version: 1, rehydrated: false }, +}; + +export type ExpectedScenarioResult = { + // wordId for the parent word + parent: string; + // sense guids in the parent word + senses: string[]; + // semantic domain ids in the parent word + semDoms: string[]; + // definitions in the merged sense + defs: Definition[][]; + // child source word ids + children: string[]; +}; + +export type GetMergeWordsScenario = { + initialState: () => PreloadedState; + expectedResult: ExpectedScenarioResult[]; +}; + +// Scenario: +// Word1: +// vern: foo +// senses: bah +// +// Word2: +// vern: foo +// senses: bar, baz +// +// Sense "bah" is dragged to "Word2" as an additional sense +export const mergeTwoWordsScenario: GetMergeWordsScenario = { + initialState: () => { + return { + ...persistedDefaultState, + mergeDuplicateGoal: { + data: { + senses: { + "guid-sense-bah": convertSenseToMergeTreeSense( + senseBah, + wordFoo1.id, + 0 + ), + "guid-sense-bar": convertSenseToMergeTreeSense( + senseBar, + wordFoo2.id, + 0 + ), + "guid-sense-baz": convertSenseToMergeTreeSense( + senseBaz, + wordFoo2.id, + 1 + ), + }, + words: { + "wordId-foo1": wordFoo1, + "wordId-foo2": wordFoo2, + }, + }, + tree: { + sidebar: { + senses: [], + wordId: "", + mergeSenseId: "", + }, + words: { + "wordId-foo2": convertWordToMergeTreeWord({ + ...wordFoo2, + senses: [senseBar, senseBaz, senseBah], + }), + }, + }, + mergeWords: [], + }, + }; + }, + expectedResult: [ + { + parent: "wordId-foo2", + senses: ["guid-sense-bah", "guid-sense-bar", "guid-sense-baz"], + semDoms: ["3", "4"], + defs: [[], [], [definitionBah]], + children: ["wordId-foo1", "wordId-foo2"], + }, + ], +}; + +// Scenario: +// Word1: +// vern: foo +// senses: bah +// +// Word2: +// vern: foo +// senses: bar, baz +// +// Sense "bah" is dragged to Word2 and merged with sense "bar" +export const mergeTwoSensesScenario: GetMergeWordsScenario = { + initialState: () => { + return { + ...persistedDefaultState, + mergeDuplicateGoal: { + data: { + senses: { + "guid-sense-bah": convertSenseToMergeTreeSense( + senseBah, + wordFoo1.id, + 0 + ), + "guid-sense-bar": convertSenseToMergeTreeSense( + senseBar, + wordFoo2.id, + 0 + ), + "guid-sense-baz": convertSenseToMergeTreeSense( + senseBaz, + wordFoo2.id, + 1 + ), + }, + words: { + "wordId-foo1": wordFoo1, + "wordId-foo2": wordFoo2, + }, + }, + tree: { + sidebar: { + senses: [], + wordId: "", + mergeSenseId: "", + }, + words: { + "wordId-foo2": newMergeTreeWord(wordFoo2.vernacular, { + word2_senseA: [senseBar.guid], + word2_senseB: [senseBaz.guid, senseBah.guid], + }), + }, + }, + mergeWords: [], + }, + }; + }, + expectedResult: [ + { + parent: "wordId-foo2", + senses: ["guid-sense-bar", "guid-sense-baz"], + semDoms: ["3", "4"], + defs: [[], [definitionBah]], + children: ["wordId-foo1", "wordId-foo2"], + }, + ], +}; + +// Scenario: +// Word1: +// vern: foo +// senses: bah +// +// Word2: +// vern: foo +// senses: bar, bag +// +// Sense "bah" is dragged to Word2 and merged with sense "bag" +export const mergeTwoDefinitionsScenario: GetMergeWordsScenario = { + initialState: () => { + return { + ...persistedDefaultState, + mergeDuplicateGoal: { + data: { + senses: { + "guid-sense-bah": convertSenseToMergeTreeSense( + senseBah, + wordFoo1.id, + 0 + ), + "guid-sense-bar": convertSenseToMergeTreeSense( + senseBar, + wordFoo2.id, + 0 + ), + "guid-sense-bag": convertSenseToMergeTreeSense( + senseBag, + wordFoo2.id, + 1 + ), + }, + words: { + "wordId-foo1": wordFoo1, + "wordId-foo2": { ...wordFoo2, senses: [senseBar, senseBag] }, + }, + }, + tree: { + sidebar: { + senses: [], + wordId: "", + mergeSenseId: "", + }, + words: { + "wordId-foo2": newMergeTreeWord(wordFoo2.vernacular, { + word2_senseA: [senseBar.guid], + word2_senseB: [senseBag.guid, senseBah.guid], + }), + }, + }, + mergeWords: [], + }, + }; + }, + expectedResult: [ + { + parent: "wordId-foo2", + senses: ["guid-sense-bag", "guid-sense-bar"], + semDoms: ["3", "3", "4"], + defs: [[], [definitionBagBah]], + children: ["wordId-foo1", "wordId-foo2"], + }, + ], +}; diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx index d0ce755111..8147bfee19 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx @@ -1,3 +1,5 @@ +import { Action, PayloadAction } from "@reduxjs/toolkit"; + import { convertSenseToMergeTreeSense, defaultSidebar, @@ -5,17 +7,27 @@ import { MergeTreeWord, newMergeTreeWord, } from "goals/MergeDuplicates/MergeDupsTreeTypes"; -import * as Actions from "goals/MergeDuplicates/Redux/MergeDupsActions"; import { + clearTree, + combineSense, + deleteSense, + flagWord, + getMergeWords, + moveSense, + orderSense, + setData, +} from "goals/MergeDuplicates/Redux/MergeDupsActions"; +import mergeDupStepReducer, { defaultState, - mergeDupStepReducer, } from "goals/MergeDuplicates/Redux/MergeDupsReducer"; +import { MergeTreeState } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; import { - MergeTreeAction, - MergeTreeActionTypes, - MergeTreeState, -} from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; + mergeTwoDefinitionsScenario, + mergeTwoSensesScenario, + mergeTwoWordsScenario, +} from "goals/MergeDuplicates/Redux/tests/MergeDupsDataMock"; import { StoreAction, StoreActionTypes } from "rootActions"; +import { setupStore } from "store"; import { Hash } from "types/hash"; import { newFlag, testWordList } from "types/word"; @@ -37,13 +49,7 @@ beforeEach(() => { mockUuid.v4.mockImplementation(getMockUuid); }); -describe("MergeDupReducer", () => { - // a state with no duplicate senses - const initState = mergeDupStepReducer( - undefined, - Actions.setWordData(testWordList()) - ); - +describe("MergeDupsReducer", () => { // helper functions for working with a tree const getRefByGuid = ( guid: string, @@ -62,8 +68,12 @@ describe("MergeDupReducer", () => { }; test("clearTree", () => { - const newState = mergeDupStepReducer(initState, Actions.clearTree()); - expect(JSON.stringify(newState)).toEqual(JSON.stringify(defaultState)); + const store = setupStore(); + store.dispatch(setData(testWordList())); + store.dispatch(clearTree()); + expect(JSON.stringify(store.getState().mergeDuplicateGoal)).toEqual( + JSON.stringify(defaultState) + ); }); function testTreeWords(): Hash { @@ -89,9 +99,10 @@ describe("MergeDupReducer", () => { sidebar: defaultSidebar, words: testTreeWords(), }, + mergeWords: [], }; function checkTreeWords( - action: MergeTreeAction, + action: Action | PayloadAction, expected: Hash ): void { const result = mergeDupStepReducer(mockState, action).tree.words; @@ -115,7 +126,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${destWordId}_senseA`, }; - const testAction = Actions.combineSense(srcRef, destRef); + const testAction = combineSense({ src: srcRef, dest: destRef }); const expectedWords = testTreeWords(); expectedWords[srcWordId].sensesGuids = { @@ -142,7 +153,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${destWordId}_senseA`, }; - const testAction = Actions.combineSense(srcRef, destRef); + const testAction = combineSense({ src: srcRef, dest: destRef }); const expectedWords = testTreeWords(); expectedWords[destWordId].sensesGuids = { @@ -169,7 +180,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${destWordId}_senseA`, }; - const testAction = Actions.combineSense(srcRef, destRef); + const testAction = combineSense({ src: srcRef, dest: destRef }); const expectedWords = testTreeWords(); delete expectedWords[srcWordId]; @@ -192,7 +203,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${wordId}_senseA`, }; - const testAction = Actions.combineSense(srcRef, destRef); + const testAction = combineSense({ src: srcRef, dest: destRef }); const expectedWords = testTreeWords(); expectedWords[wordId].sensesGuids = { @@ -215,7 +226,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${destWordId}_senseA`, }; - const testAction = Actions.combineSense(srcRef, destRef); + const testAction = combineSense({ src: srcRef, dest: destRef }); const expectedWords = testTreeWords(); expectedWords[srcWordId].sensesGuids = { @@ -241,7 +252,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${destWordId}_senseA`, }; - const testAction = Actions.combineSense(srcRef, destRef); + const testAction = combineSense({ src: srcRef, dest: destRef }); const expectedWords = testTreeWords(); delete expectedWords[srcWordId]; @@ -262,7 +273,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${wordId}_senseA`, }; - const testAction = Actions.deleteSense(testRef); + const testAction = deleteSense(testRef); const expectedWords = testTreeWords(); delete expectedWords[wordId].sensesGuids[testRef.mergeSenseId]; @@ -277,7 +288,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${wordId}_senseB`, }; - const testAction = Actions.deleteSense(testRef); + const testAction = deleteSense(testRef); const expectedWords = testTreeWords(); delete expectedWords[wordId].sensesGuids[testRef.mergeSenseId]; @@ -292,7 +303,7 @@ describe("MergeDupReducer", () => { mergeSenseId: `${wordId}_senseA`, }; - const testAction = Actions.deleteSense(testRef); + const testAction = deleteSense(testRef); const expectedWords = testTreeWords(); delete expectedWords[wordId]; @@ -308,7 +319,7 @@ describe("MergeDupReducer", () => { order: 0, }; - const testAction = Actions.deleteSense(testRef); + const testAction = deleteSense(testRef); const expectedWords = testTreeWords(); expectedWords[wordId].sensesGuids = { word2_senseA: ["word2_senseA_1"] }; @@ -324,7 +335,7 @@ describe("MergeDupReducer", () => { order: 0, }; - const testAction = Actions.deleteSense(srcRef); + const testAction = deleteSense(srcRef); const expectedWords = testTreeWords(); delete expectedWords[srcWordId]; @@ -337,7 +348,7 @@ describe("MergeDupReducer", () => { it("adds a flag to a word", () => { const wordId = "word1"; const testFlag = newFlag("flagged"); - const testAction = Actions.flagWord(wordId, testFlag); + const testAction = flagWord({ wordId: wordId, flag: testFlag }); const expectedWords = testTreeWords(); expectedWords[wordId].flag = testFlag; @@ -346,6 +357,59 @@ describe("MergeDupReducer", () => { }); }); + describe("getMergeWords", () => { + it("sense moved from one word to another", () => { + const store = setupStore(mergeTwoWordsScenario.initialState()); + store.dispatch(getMergeWords()); + const mergeArray = store.getState().mergeDuplicateGoal.mergeWords; + const expectedResult = mergeTwoWordsScenario.expectedResult; + expect(mergeArray.length).toEqual(1); + expect(mergeArray[0].parent.id).toEqual(expectedResult[0].parent); + const senses = mergeArray[0].parent.senses.map((s) => s.guid).sort(); + expect(senses).toEqual(expectedResult[0].senses); + const semDoms = mergeArray[0].parent.senses + .flatMap((s) => s.semanticDomains.map((d) => d.id)) + .sort(); + expect(semDoms).toEqual(expectedResult[0].semDoms); + const defs = mergeArray[0].parent.senses.map((s) => s.definitions); + expect(defs).toEqual(expectedResult[0].defs); + }); + + it("sense from one word combined with sense in another", () => { + const store = setupStore(mergeTwoSensesScenario.initialState()); + store.dispatch(getMergeWords()); + const mergeArray = store.getState().mergeDuplicateGoal.mergeWords; + const expectedResult = mergeTwoSensesScenario.expectedResult; + expect(mergeArray.length).toEqual(1); + expect(mergeArray[0].parent.id).toEqual(expectedResult[0].parent); + const senses = mergeArray[0].parent.senses.map((s) => s.guid).sort(); + expect(senses).toEqual(expectedResult[0].senses); + const semDoms = mergeArray[0].parent.senses + .flatMap((s) => s.semanticDomains.map((d) => d.id)) + .sort(); + expect(semDoms).toEqual(expectedResult[0].semDoms); + const defs = mergeArray[0].parent.senses.map((s) => s.definitions); + expect(defs).toEqual(expectedResult[0].defs); + }); + + it("combine senses with definitions", () => { + const store = setupStore(mergeTwoDefinitionsScenario.initialState()); + store.dispatch(getMergeWords()); + const mergeArray = store.getState().mergeDuplicateGoal.mergeWords; + const expectedResult = mergeTwoDefinitionsScenario.expectedResult; + expect(mergeArray.length).toEqual(1); + expect(mergeArray[0].parent.id).toEqual(expectedResult[0].parent); + const senses = mergeArray[0].parent.senses.map((s) => s.guid).sort(); + expect(senses).toEqual(expectedResult[0].senses); + const semDoms = mergeArray[0].parent.senses + .flatMap((s) => s.semanticDomains.map((d) => d.id)) + .sort(); + expect(semDoms).toEqual(expectedResult[0].semDoms); + const defs = mergeArray[0].parent.senses.map((s) => s.definitions); + expect(defs).toEqual(expectedResult[0].defs); + }); + }); + describe("moveSense", () => { it("moves a sense out from sidebar to same word", () => { const wordId = "word2"; @@ -358,7 +422,11 @@ describe("MergeDupReducer", () => { // Intercept the uuid that will be assigned. const nextGuid = getMockUuid(false); - const testAction = Actions.moveSense(testRef, wordId, 1); + const testAction = moveSense({ + ref: testRef, + destWordId: wordId, + destOrder: 1, + }); const expectedWords = testTreeWords(); expectedWords[wordId].sensesGuids = { word2_senseA: ["word2_senseA_1"] }; @@ -382,7 +450,11 @@ describe("MergeDupReducer", () => { // Intercept the uuid that will be assigned. const nextGuid = getMockUuid(false); - const testAction = Actions.moveSense(testRef, destWordId, 2); + const testAction = moveSense({ + ref: testRef, + destWordId: destWordId, + destOrder: 2, + }); const expectedWords = testTreeWords(); expectedWords[srcWordId].sensesGuids = { @@ -405,7 +477,11 @@ describe("MergeDupReducer", () => { const destWordId = "word1"; - const testAction = Actions.moveSense(testRef, destWordId, 1); + const testAction = moveSense({ + ref: testRef, + destWordId: destWordId, + destOrder: 1, + }); const expectedWords = testTreeWords(); expectedWords[srcWordId].sensesGuids = { @@ -428,7 +504,11 @@ describe("MergeDupReducer", () => { const destWordId = "word1"; - const testAction = Actions.moveSense(testRef, destWordId, 1); + const testAction = moveSense({ + ref: testRef, + destWordId: destWordId, + destOrder: 1, + }); const expectedWords = testTreeWords(); expectedWords[srcWordId].sensesGuids = { @@ -451,8 +531,12 @@ describe("MergeDupReducer", () => { const destWordId = "word2"; - const testAction = Actions.moveSense(testRef, destWordId, 1); - expect(testAction.type).toEqual(MergeTreeActionTypes.MOVE_SENSE); + const testAction = moveSense({ + ref: testRef, + destWordId: destWordId, + destOrder: 1, + }); + expect(testAction.type).toEqual("mergeDupStepReducer/moveSenseAction"); const expectedWords = testTreeWords(); delete expectedWords[srcWordId]; @@ -471,7 +555,7 @@ describe("MergeDupReducer", () => { const mergeSenseId = `${wordId}_senseA`; const testRef: MergeTreeReference = { wordId, mergeSenseId, order: 0 }; - const testAction = Actions.orderSense(testRef, 1); + const testAction = orderSense({ ref: testRef, order: 1 }); const expectedWords = testTreeWords(); expectedWords[wordId].sensesGuids[mergeSenseId] = [ @@ -487,7 +571,7 @@ describe("MergeDupReducer", () => { const mergeSenseId = `${wordId}_senseA`; const testRef: MergeTreeReference = { wordId, mergeSenseId }; - const testAction = Actions.orderSense(testRef, 1); + const testAction = orderSense({ ref: testRef, order: 1 }); const expectedWords = testTreeWords(); expectedWords[wordId].sensesGuids = { @@ -511,10 +595,7 @@ describe("MergeDupReducer", () => { test("setWordData", () => { const wordList = testWordList(); - const treeState = mergeDupStepReducer( - undefined, - Actions.setWordData(wordList) - ); + const treeState = mergeDupStepReducer(undefined, setData(wordList)); // check if data has all words present for (const word of wordList) { const srcWordId = word.id; diff --git a/src/rootReducer.ts b/src/rootReducer.ts index a617d965fc..34dde802f9 100644 --- a/src/rootReducer.ts +++ b/src/rootReducer.ts @@ -7,7 +7,7 @@ import exportProjectReducer from "components/ProjectExport/Redux/ExportProjectRe import { pronunciationsReducer } from "components/Pronunciations/Redux/PronunciationsReducer"; import { treeViewReducer } from "components/TreeView/Redux/TreeViewReducer"; import { characterInventoryReducer } from "goals/CharacterInventory/Redux/CharacterInventoryReducer"; -import { mergeDupStepReducer } from "goals/MergeDuplicates/Redux/MergeDupsReducer"; +import mergeDupStepReducer from "goals/MergeDuplicates/Redux/MergeDupsReducer"; import { reviewEntriesReducer } from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReducer"; import { StoreState } from "types"; import { analyticsReducer } from "types/Redux/analytics"; From 8f9788c722c3cac7971de92d72d843af2ad821d9 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Mon, 6 Nov 2023 11:58:15 -0500 Subject: [PATCH 6/7] Split off eslintConfig from package.json into .eslintrc.cjs (#2740) --- .dockerignore | 5 +-- .eslintrc.cjs | 69 +++++++++++++++++++++++++++++++++++ package.json | 83 ------------------------------------------- scripts/setupMongo.ts | 2 +- tsconfig.json | 24 ++++++------- 5 files changed, 85 insertions(+), 98 deletions(-) create mode 100644 .eslintrc.cjs diff --git a/.dockerignore b/.dockerignore index 74a21c55e0..709fd21975 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,15 +2,16 @@ ** # Except the following. +!docs/user_guide !nginx !public !src -!docs/user_guide !.env +!.eslintrc.cjs !dev-requirements.txt !package*.json -!tsconfig.json !tox.ini +!tsconfig.json # Ignore user guide build directory. docs/user_guide/site diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000000..fe11647d20 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,69 @@ +module.exports = { + env: { + browser: true, + jest: true, + }, + extends: [ + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", + "plugin:import/recommended", + ], + ignorePatterns: ["*.dic.js"], + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaFeatures: { jsx: true }, + project: "./tsconfig.json", + }, + plugins: ["@typescript-eslint", "react", "unused-imports"], + root: true, + rules: { + "import/first": "warn", + "import/newline-after-import": "warn", + "import/no-duplicates": "warn", + "import/no-named-as-default": "off", + "import/no-named-as-default-member": "off", + "import/order": [ + "warn", + { + alphabetize: { order: "asc" }, + groups: [ + "builtin", + "external", + ["internal", "parent", "sibling", "index", "object", "type"], + ], + "newlines-between": "always", + }, + ], + "no-undef": "off", + "prefer-const": "warn", + "react/jsx-boolean-value": "warn", + "unused-imports/no-unused-imports": "warn", + }, + settings: { + react: { version: "detect" }, + "import/resolver": { + typescript: { alwaysTryTypes: true }, + }, + }, + overrides: [ + { + files: ["*.ts", "*.tsx"], + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:import/typescript", + ], + rules: { + "@typescript-eslint/explicit-function-return-type": [ + "warn", + { allowExpressions: true }, + ], + "@typescript-eslint/no-empty-interface": "warn", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-inferrable-types": "warn", + "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/switch-exhaustiveness-check": "warn", + }, + }, + ], +}; diff --git a/package.json b/package.json index 67e5a699bc..ff4b70113d 100644 --- a/package.json +++ b/package.json @@ -125,89 +125,6 @@ "source-map-explorer": "^2.5.3", "typescript": "4.9.5" }, - "eslintConfig": { - "extends": [ - "plugin:@typescript-eslint/recommended", - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:react-hooks/recommended", - "plugin:import/recommended", - "plugin:import/typescript" - ], - "env": { - "browser": true, - "jest": true - }, - "ignorePatterns": [ - "*.dic.js" - ], - "rules": { - "@typescript-eslint/explicit-function-return-type": [ - "warn", - { - "allowExpressions": true - } - ], - "@typescript-eslint/no-empty-interface": "warn", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-inferrable-types": "warn", - "@typescript-eslint/no-unused-vars": "warn", - "@typescript-eslint/switch-exhaustiveness-check": "warn", - "import/first": "warn", - "import/newline-after-import": "warn", - "import/no-duplicates": "warn", - "import/no-named-as-default": "off", - "import/no-named-as-default-member": "off", - "import/order": [ - "warn", - { - "groups": [ - "builtin", - "external", - [ - "internal", - "parent", - "sibling", - "index", - "object", - "type" - ] - ], - "alphabetize": { - "order": "asc" - }, - "newlines-between": "always" - } - ], - "no-undef": "off", - "prefer-const": "warn", - "react/jsx-boolean-value": "warn", - "unused-imports/no-unused-imports": "warn" - }, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "project": "./tsconfig.json" - }, - "plugins": [ - "@typescript-eslint", - "react", - "unused-imports" - ], - "root": true, - "settings": { - "react": { - "version": "detect" - }, - "import/resolver": { - "typescript": { - "alwaysTryTypes": true - } - } - } - }, "jest": { "coverageReporters": [ "cobertura", diff --git a/scripts/setupMongo.ts b/scripts/setupMongo.ts index 862f00bf9d..7f8f85a906 100644 --- a/scripts/setupMongo.ts +++ b/scripts/setupMongo.ts @@ -2,7 +2,7 @@ import * as makeDir from "make-dir"; const directory = "./mongo_database"; -const makeMongoDirectory = async () => { +const makeMongoDirectory = async (): Promise => { await makeDir(directory); }; diff --git a/tsconfig.json b/tsconfig.json index f6222becae..9f9b6736f9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,23 +1,23 @@ { "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, "allowSyntheticDefaultImports": true, - "strict": true, + "baseUrl": "src", + "downlevelIteration": true, + "esModuleInterop": true, "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": ["dom", "dom.iterable", "esnext"], "module": "esnext", "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx", - "downlevelIteration": true, - "baseUrl": "src", - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "es5" }, "exclude": ["*.dic.js"], - "include": ["src", "scripts"] + "include": [".eslintrc.cjs", "src", "scripts"] } From 93866e586ce534b5d7e1a9808b4f75d12d035963 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Mon, 6 Nov 2023 14:06:06 -0500 Subject: [PATCH 7/7] Prevent DataEntryTable flash when opening TreeView first (#2781) --- src/components/DataEntry/index.tsx | 61 ++++++++++++++++-------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/src/components/DataEntry/index.tsx b/src/components/DataEntry/index.tsx index 6b37c8e8d0..29aeb962ef 100644 --- a/src/components/DataEntry/index.tsx +++ b/src/components/DataEntry/index.tsx @@ -103,39 +103,42 @@ export default function DataEntry(): ReactElement { }, [analysisLang, dispatch, id]); return ( - - - - + {!open && !!domain.guid && ( + + + + + + 0} + hideQuestions={() => setQuestionsVisible(false)} + isTreeOpen={open} + openTree={() => dispatch(openTreeAction())} + semanticDomain={currentDomain} + showExistingData={() => setDrawerOpen(true)} + updateHeight={updateHeight} + /> + + + - - 0} - hideQuestions={() => setQuestionsVisible(false)} - isTreeOpen={open} - openTree={() => dispatch(openTreeAction())} - semanticDomain={currentDomain} - showExistingData={() => setDrawerOpen(true)} - updateHeight={updateHeight} - /> - - - - - + + )} + - + ); }