diff --git a/.env.example b/.env.example index 7b9637efd41..4e6c72652d6 100644 --- a/.env.example +++ b/.env.example @@ -37,7 +37,7 @@ CLUSTER_MODE= ################# DATABASE ################# -# postgres | msysql | mariadb | sqlite +# postgres | mysql | mariadb | sqlite DB_DIALECT=postgres DB_TABLE_PREFIX= DB_HOST=localhost diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index 19001011aaa..73ad08922d8 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -1,4 +1,4 @@ -name: auto-merge +name: Auto merge main -> next concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -13,17 +13,29 @@ jobs: push-commit: runs-on: ubuntu-latest steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.NOCOBASE_APP_ID }} + private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} + repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }} + skip-token-revoke: true + - name: Get GitHub App User ID + id: get-user-id + run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} - name: Checkout uses: actions/checkout@v4 with: repository: nocobase/nocobase - ssh-key: ${{ secrets.NOCOBASE_DEPLOY_KEY }} + token: ${{ steps.app-token.outputs.token }} persist-credentials: true fetch-depth: 0 - name: main -> next(nocobase) run: | - git config --global user.email "actions@github.com" - git config --global user.name "GitHub Actions Bot" + git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' + git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>' git checkout main git pull origin main git checkout next @@ -33,7 +45,7 @@ jobs: uses: ad-m/github-push-action@master with: branch: next - ssh: true + github_token: ${{ steps.app-token.outputs.token }} repository: nocobase/nocobase tags: true atomic: true diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 9b7f3f544a9..87b3e58a527 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -1,4 +1,4 @@ -name: Build Docker Image +name: Build docker image concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -93,7 +93,7 @@ jobs: file: Dockerfile build-args: | VERDACCIO_URL=http://localhost:4873/ - COMMIT_HASH=${GITHUB_SHA} + COMMIT_HASH=${{ github.sha }} push: true tags: ${{ steps.set-tags.outputs.tags }} diff --git a/.github/workflows/build-pro-image.yml b/.github/workflows/build-pro-image.yml index 38845bddbd9..6fcdd8bfedf 100644 --- a/.github/workflows/build-pro-image.yml +++ b/.github/workflows/build-pro-image.yml @@ -1,4 +1,4 @@ -name: Build Pro Image +name: Build pro image concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -12,16 +12,12 @@ on: - 'next' paths: - 'packages/**' - - 'Dockerfile' + - 'Dockerfile.pro' - '.github/workflows/build-pro-image.yml' jobs: - app-token: - if: github.event.pull_request.head.repo.fork != true - uses: nocobase/nocobase/.github/workflows/get-nocobase-app-token.yml@main - secrets: inherit build-and-push: - needs: app-token + if: github.event.pull_request.head.repo.fork != true runs-on: ubuntu-latest services: verdaccio: @@ -29,14 +25,21 @@ jobs: ports: - 4873:4873 steps: - - name: Decrypt app token - id: app-token - shell: bash + - name: Get pro plugins + id: get-pro-plugins run: | - ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }}; - BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode); - APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}"); - echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT + if [[ "${{ github.head_ref || github.ref_name }}" == "main" ]]; then + echo "proRepos=$(echo '${{ vars.PRO_PLUGIN_REPOS }}')" >> $GITHUB_OUTPUT + else + echo "proRepos=$(echo '${{ vars.NEXT_PRO_PLUGIN_REPOS }}')" >> $GITHUB_OUTPUT + fi + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.NOCOBASE_APP_ID }} + private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} + repositories: nocobase,pro-plugins,${{ join(fromJSON(steps.get-pro-plugins.outputs.proRepos), ',') }} + skip-token-revoke: true - name: Checkout uses: actions/checkout@v3 with: @@ -50,13 +53,6 @@ jobs: path: packages/pro-plugins fetch-depth: 0 token: ${{ steps.app-token.outputs.token }} - - name: Clone pro repos - shell: bash - run: | - for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} - do - git clone -b main https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo - done - run: | cd packages/pro-plugins && if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then @@ -68,8 +64,15 @@ jobs: git checkout main fi fi + - name: Clone pro repos + shell: bash + run: | + for repo in ${{ join(fromJSON(steps.get-pro-plugins.outputs.proRepos), ' ') }} + do + git clone -b main https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo + done - run: | - for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} + for repo in ${{ join(fromJSON(steps.get-pro-plugins.outputs.proRepos), ' ') }} do cd ./packages/pro-plugins/@nocobase/$repo if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then @@ -86,7 +89,7 @@ jobs: - name: rm .git run: | rm -rf packages/pro-plugins/.git - for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} + for repo in ${{ join(fromJSON(steps.get-pro-plugins.outputs.proRepos), ' ') }} do rm -rf packages/pro-plugins/@nocobase/$repo/.git done @@ -137,7 +140,7 @@ jobs: uses: docker/build-push-action@v3 with: context: . - file: Dockerfile + file: Dockerfile.pro build-args: | VERDACCIO_URL=http://localhost:4873/ COMMIT_HASH=${GITHUB_SHA} diff --git a/.github/workflows/changelog-and-release.yml b/.github/workflows/changelog-and-release.yml new file mode 100644 index 00000000000..ed7d78c2315 --- /dev/null +++ b/.github/workflows/changelog-and-release.yml @@ -0,0 +1,82 @@ +name: Write changelog and create release + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + workflow_dispatch: + inputs: + version: + type: choice + description: Please choose a version + options: + - beta + - alpha + default: beta + push: + tags: + - 'v*-beta' + +jobs: + write-changelog-and-release: + runs-on: ubuntu-latest + steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.NOCOBASE_APP_ID }} + private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} + repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }} + skip-token-revoke: true + - name: Get GitHub App User ID + id: get-user-id + run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + - name: Checkout + uses: actions/checkout@v4 + with: + repository: nocobase/nocobase + ref: main + token: ${{ steps.app-token.outputs.token }} + persist-credentials: true + fetch-depth: 0 + - name: Checkout pro-plugins + uses: actions/checkout@v4 + with: + repository: nocobase/pro-plugins + path: packages/pro-plugins + fetch-depth: 0 + token: ${{ steps.app-token.outputs.token }} + persist-credentials: true + - name: Clone pro repos + shell: bash + run: | + for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} + do + git clone -b main https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo + done + - name: Set user + run: | + git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' + git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>' + - name: Set Node.js 18 + uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install dependencies + run: yarn install --frozen-lockfile + - name: Run script + shell: bash + run: | + node scripts/release/changelogAndRelease.js --ver ${{ inputs.version }} --cmsURL ${{ secrets.CMS_URL }} --cmsToken ${{ secrets.CMS_TOKEN }} + env: + PRO_PLUGIN_REPOS: ${{ vars.PRO_PLUGIN_REPOS }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + - name: Commit and push + run: | + git pull origin main + git add . + git commit -m "docs: update changelogs" + git push origin main diff --git a/.github/workflows/deploy-client-docs.yml b/.github/workflows/deploy-client-docs.yml index 8918589bf05..780b48031de 100644 --- a/.github/workflows/deploy-client-docs.yml +++ b/.github/workflows/deploy-client-docs.yml @@ -1,4 +1,4 @@ -name: deploy client docs +name: Deploy client docs concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -25,30 +25,30 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 18 - cache: 'yarn' - - run: yarn install - - name: Build zh-CN - run: yarn doc build core/client --lang=zh-CN - - name: Build en-US - run: yarn doc build core/client --lang=en-US - - name: Set tags - id: set-tags - run: | - if [[ "${{ github.ref_name }}" == "main" ]]; then - echo "::set-output name=tags::${{ github.ref_name }}" - else - echo "::set-output name=tags::pr-${{ github.event.pull_request.number }}" - fi - - name: copy files via ssh - ${{ steps.set-tags.outputs.tags }} - uses: appleboy/scp-action@v0.1.4 - with: - host: ${{ secrets.CN_CLIENT_HOST }} - username: ${{ secrets.CN_CLIENT_USERNAME }} - key: ${{ secrets.CN_CLIENT_KEY }} - port: ${{ secrets.CN_CLIENT_PORT }} - source: "packages/core/client/dist/*" - target: ${{ secrets.CN_CLIENT_TARGET }}/${{ steps.set-tags.outputs.tags }} + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: 'yarn' + - run: yarn install + - name: Build zh-CN + run: yarn doc build core/client --lang=zh-CN + - name: Build en-US + run: yarn doc build core/client --lang=en-US + - name: Set tags + id: set-tags + run: | + if [[ "${{ github.ref_name }}" == "main" ]]; then + echo "::set-output name=tags::${{ github.ref_name }}" + else + echo "::set-output name=tags::pr-${{ github.event.pull_request.number }}" + fi + - name: copy files via ssh - ${{ steps.set-tags.outputs.tags }} + uses: appleboy/scp-action@v0.1.4 + with: + host: ${{ secrets.CN_CLIENT_HOST }} + username: ${{ secrets.CN_CLIENT_USERNAME }} + key: ${{ secrets.CN_CLIENT_KEY }} + port: ${{ secrets.CN_CLIENT_PORT }} + source: 'packages/core/client/dist/*' + target: ${{ secrets.CN_CLIENT_TARGET }}/${{ steps.set-tags.outputs.tags }} diff --git a/.github/workflows/get-nocobase-app-token.yml b/.github/workflows/get-nocobase-app-token.yml deleted file mode 100644 index abfc2b1b2cd..00000000000 --- a/.github/workflows/get-nocobase-app-token.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Get nocobase app github token - -on: - workflow_call: - outputs: - token: - value: ${{ jobs.get-app-token.outputs.token }} - user-id: - value: ${{ jobs.get-app-token.outputs.user-id }} - app-slug: - value: ${{ jobs.get-app-token.outputs.app-slug }} - -jobs: - get-app-token: - runs-on: ubuntu-latest - outputs: - token: ${{ steps.encrypt-token.outputs.token }} - app-slug: ${{ steps.app-token.outputs.app-slug }} - user-id: ${{ steps.get-user-id.outputs.user-id }} - steps: - - uses: actions/create-github-app-token@v1 - id: app-token - with: - app-id: ${{ vars.NOCOBASE_APP_ID }} - private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} - repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }} - skip-token-revoke: true - - name: Encrypt token - id: encrypt-token - shell: bash - run: | - APP_TOKEN=${{ steps.app-token.outputs.token }}; - BINARY_ENCRYPTED_SECRET=$(echo -n "$APP_TOKEN" | openssl enc -aes-256-cbc -pbkdf2 -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}"); - ENCRYPTED_SECRET=$(echo -n "$BINARY_ENCRYPTED_SECRET" | base64 -w 0); - echo "token=$ENCRYPTED_SECRET" >> $GITHUB_OUTPUT - - name: Get GitHub App User ID - id: get-user-id - run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/manual-build-pr-docker-image.yml b/.github/workflows/manual-build-pr-docker-image.yml index 1548b0a6db5..c5bf6ba2714 100644 --- a/.github/workflows/manual-build-pr-docker-image.yml +++ b/.github/workflows/manual-build-pr-docker-image.yml @@ -1,4 +1,4 @@ -name: manual-build-pr-docker-image +name: Manual build pr docker image concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/manual-build-pro-image.yml b/.github/workflows/manual-build-pro-image.yml index 7b598b1ed79..57fe6866e21 100644 --- a/.github/workflows/manual-build-pro-image.yml +++ b/.github/workflows/manual-build-pro-image.yml @@ -1,4 +1,4 @@ -name: manual-build-pro-image +name: Manual build pro image concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -16,12 +16,8 @@ on: required: true jobs: - app-token: - if: github.event.pull_request.head.repo.fork != true - uses: nocobase/nocobase/.github/workflows/get-nocobase-app-token.yml@main - secrets: inherit build-and-push: - needs: app-token + if: github.event.pull_request.head.repo.fork != true runs-on: ubuntu-latest services: verdaccio: @@ -29,14 +25,21 @@ jobs: ports: - 4873:4873 steps: - - name: Decrypt app token - id: app-token - shell: bash + - name: Get pro plugins + id: get-pro-plugins run: | - ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }}; - BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode); - APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}"); - echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT + if [[ "${{ github.head_ref || github.ref_name }}" == "main" ]]; then + echo "proRepos=$(echo '${{ vars.PRO_PLUGIN_REPOS }}')" >> $GITHUB_OUTPUT + else + echo "proRepos=$(echo '${{ vars.NEXT_PRO_PLUGIN_REPOS }}')" >> $GITHUB_OUTPUT + fi + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.NOCOBASE_APP_ID }} + private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} + repositories: nocobase,pro-plugins,${{ join(fromJSON(steps.get-pro-plugins.outputs.proRepos), ',') }} + skip-token-revoke: true - name: Checkout uses: actions/checkout@v4 with: @@ -59,14 +62,14 @@ jobs: - name: Clone pro repos shell: bash run: | - for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} + for repo in ${{ join(fromJSON(steps.get-pro-plugins.outputs.proRepos), ' ') }} do - git clone -b ${{ steps.set_pro_pr_branch.outputs.pr_branch || 'main' }} https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo + git clone -b ${{ github.head_ref || github.ref_name }} https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo done - name: rm .git run: | rm -rf packages/pro-plugins/.git - for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} + for repo in ${{ join(fromJSON(steps.get-pro-plugins.outputs.proRepos), ' ') }} do rm -rf packages/pro-plugins/@nocobase/$repo/.git done diff --git a/.github/workflows/manual-build-pro-plugin-image.yml b/.github/workflows/manual-build-pro-plugin-image.yml new file mode 100644 index 00000000000..3b87bcaf75b --- /dev/null +++ b/.github/workflows/manual-build-pro-plugin-image.yml @@ -0,0 +1,131 @@ +name: Build pro plugin docker image + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + workflow_dispatch: + inputs: + pro_plugin: + description: 'Please enter a pro plugin name' + required: true + pr_number: + description: 'Please enter the pr number of pro plugin repository' + required: false + nocobase_pr_number: + description: 'Please enter the pr number of nocobase/nocobase repository' + required: false +jobs: + build-and-push: + runs-on: ubuntu-latest + services: + verdaccio: + image: verdaccio/verdaccio:latest + ports: + - 4873:4873 + steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.NOCOBASE_APP_ID }} + private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} + repositories: nocobase,pro-plugins,plugin-${{ inputs.pro_plugin }},${{ join(fromJSON(vars.NEXT_PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }} + skip-token-revoke: true + - name: Checkout nocobase/nocobase + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref || github.ref_name }} + fetch-depth: 0 + - name: Checkout nocobase/nocobase pr + if: ${{ inputs.nocobase_pr_number != '' }} + shell: bash + run: | + gh pr checkout ${{ inputs.nocobase_pr_number }} + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + - name: Checkout plugin + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref || github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + repository: nocobase/plugin-${{ inputs.pro_plugin }} + path: packages/pro-plugins/@nocobase/plugin-${{ inputs.pro_plugin }} + - name: Checkout pr + if: ${{ inputs.pr_number != '' }} + shell: bash + run: | + cd ./packages/pro-plugins/@nocobase/plugin-${{ inputs.pro_plugin }} + gh pr checkout ${{ inputs.pr_number }} + cd ../../../../ + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + - name: rm .git + run: rm -rf packages/pro-plugins/@nocobase/plugin-${{ inputs.pro_plugin }}/.git && git config --global user.email "you@example.com" && git config --global user.name "Your Name" && git add -A && git commit -m "tmp commit" + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + driver-opts: network=host + - name: Login to Aliyun Container Registry + uses: docker/login-action@v2 + with: + registry: ${{ secrets.ALI_DOCKER_REGISTRY }} + username: ${{ secrets.ALI_DOCKER_USERNAME }} + password: ${{ secrets.ALI_DOCKER_PASSWORD }} + - name: Get tag + id: get-tag + run: | + if [ "${{ inputs.pr_number }}" != "" ]; then + echo "tag=pr-${{ inputs.pr_number }}" >> "$GITHUB_OUTPUT" + else + echo "tag=${{ github.head_ref || github.ref_name }}" >> "$GITHUB_OUTPUT" + fi + - name: Set tags + id: set-tags + run: | + echo "::set-output name=tags::${{ secrets.ALI_DOCKER_REGISTRY }}/nocobase/nocobase:${{ steps.get-tag.outputs.tag }}-${{ inputs.pro_plugin }}" + - name: IMAGE_TAG + env: + IMAGE_TAG: ${{ steps.get-tag.outputs.tag }} + run: | + echo $IMAGE_TAG + - name: Set variables + run: | + target_directory="./packages/pro-plugins/@nocobase" + subdirectories=$(find "$target_directory" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | tr '\n' ' ') + trimmed_variable=$(echo "$subdirectories" | xargs) + packageNames="@nocobase/${trimmed_variable// / @nocobase/}" + pluginNames="${trimmed_variable//plugin-/}" + BEFORE_PACK_NOCOBASE="yarn add $packageNames -W" + APPEND_PRESET_LOCAL_PLUGINS="${pluginNames// /,}" + echo "var1=$BEFORE_PACK_NOCOBASE" >> $GITHUB_OUTPUT + echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT + id: vars + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + file: Dockerfile + build-args: | + VERDACCIO_URL=http://localhost:4873/ + COMMIT_HASH=${GITHUB_SHA} + PLUGINS_DIRS=pro-plugins + BEFORE_PACK_NOCOBASE=${{ steps.vars.outputs.var1 }} + APPEND_PRESET_LOCAL_PLUGINS=${{ steps.vars.outputs.var2 }} + push: true + tags: ${{ steps.set-tags.outputs.tags }} + - name: Deploy NocoBase + env: + IMAGE_TAG: ${{ steps.get-tag.outputs.tag }} + run: | + echo $IMAGE_TAG + export APP_NAME=$(echo $IMAGE_TAG | cut -d ":" -f 2)-${{ inputs.pro_plugin }} + echo $APP_NAME + curl --retry 2 --location --request POST "${{secrets.NOCOBASE_DEPLOY_HOST}}$APP_NAME" \ + --header 'Content-Type: application/json' \ + -d "{ + \"tag\": \"$APP_NAME\", + \"dialect\": \"postgres\" + }" diff --git a/.github/workflows/manual-e2e.yml b/.github/workflows/manual-e2e.yml index f72aee3db42..61a5bb4d39f 100644 --- a/.github/workflows/manual-e2e.yml +++ b/.github/workflows/manual-e2e.yml @@ -1,4 +1,4 @@ -name: manual-e2e +name: Manual e2e on: workflow_dispatch: diff --git a/.github/workflows/manual-release.yml b/.github/workflows/manual-release.yml index f0ed8a67c4e..218c17f083d 100644 --- a/.github/workflows/manual-release.yml +++ b/.github/workflows/manual-release.yml @@ -1,4 +1,4 @@ -name: manual-release +name: Manual release concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -12,27 +12,28 @@ on: type: boolean jobs: - app-token: - uses: nocobase/nocobase/.github/workflows/get-nocobase-app-token.yml@main - secrets: inherit pre-merge-main-into-next: runs-on: ubuntu-latest - needs: app-token strategy: matrix: repo: - 'nocobase' - 'pro-plugins' - ${{ fromJSON(vars.PRO_PLUGIN_REPOS) }} + - ${{ fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS) }} steps: - - name: Decrypt app token + - uses: actions/create-github-app-token@v1 id: app-token - shell: bash - run: | - ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }}; - BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode); - APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}"); - echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT + with: + app-id: ${{ vars.NOCOBASE_APP_ID }} + private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} + repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }} + skip-token-revoke: true + - name: Get GitHub App User ID + id: get-user-id + run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} - name: Checkout uses: actions/checkout@v4 with: @@ -42,8 +43,8 @@ jobs: token: ${{ steps.app-token.outputs.token }} - name: main -> next (nocobase/${{ matrix.repo }}) run: | - git config --global user.name '${{ needs.app-token.outputs.app-slug }}[bot]' - git config --global user.email '${{ needs.app-token.outputs.user-id }}+${{ needs.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>' + git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' + git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>' git checkout main git pull origin main git checkout next @@ -51,18 +52,21 @@ jobs: git push origin next --tags --atomic update-version: needs: - - app-token - pre-merge-main-into-next runs-on: ubuntu-latest steps: - - name: Decrypt app token + - uses: actions/create-github-app-token@v1 id: app-token - shell: bash - run: | - ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }}; - BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode); - APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}"); - echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT + with: + app-id: ${{ vars.NOCOBASE_APP_ID }} + private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} + repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }} + skip-token-revoke: true + - name: Get GitHub App User ID + id: get-user-id + run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} - name: Checkout uses: actions/checkout@v4 with: @@ -81,7 +85,7 @@ jobs: - name: Clone pro repos shell: bash run: | - for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} + for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} ${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ' ') }} do git clone -b main https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo done @@ -96,23 +100,24 @@ jobs: run: | cd ./packages/pro-plugins git checkout main - for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} + for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} ${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ' ') }} do echo "@nocobase/$repo" >> .git/info/exclude done echo "$(<.git/info/exclude )" cd ./../.. git checkout main - git config --global user.name '${{ needs.app-token.outputs.app-slug }}[bot]' - git config --global user.email '${{ needs.app-token.outputs.user-id }}+${{ needs.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>' + git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' + git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>' echo "packages/pro-plugins" >> .git/info/exclude bash release.sh $IS_FEAT env: IS_FEAT: ${{ inputs.is_feat && '--is-feat' || '' }} PRO_PLUGIN_REPOS: ${{ vars.PRO_PLUGIN_REPOS }} + CUSTOM_PRO_PLUGIN_REPOS: ${{ vars.CUSTOM_PRO_PLUGIN_REPOS }} - name: Push and merge into next run: | - for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} + for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} ${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ' ') }} do cd ./packages/pro-plugins/@nocobase/$repo git push origin main --atomic --tags diff --git a/.github/workflows/nocobase-test-backend.yml b/.github/workflows/nocobase-test-backend.yml index 7bd6f7915e0..d9e1ceee4ee 100644 --- a/.github/workflows/nocobase-test-backend.yml +++ b/.github/workflows/nocobase-test-backend.yml @@ -1,4 +1,4 @@ -name: NocoBase Backend Test +name: NocoBase backend test concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/nocobase-test-frontend.yml b/.github/workflows/nocobase-test-frontend.yml index 0f74eee5964..d08413e0156 100644 --- a/.github/workflows/nocobase-test-frontend.yml +++ b/.github/workflows/nocobase-test-frontend.yml @@ -1,4 +1,4 @@ -name: NocoBase FrontEnd Test +name: NocoBase frontEnd test concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -33,7 +33,7 @@ jobs: frontend-test: strategy: matrix: - node_version: [ '18' ] + node_version: ['18'] runs-on: ubuntu-latest container: node:${{ matrix.node_version }} steps: diff --git a/.github/workflows/release-next.yml b/.github/workflows/release-next.yml index 004f01065fc..240075a57f5 100644 --- a/.github/workflows/release-next.yml +++ b/.github/workflows/release-next.yml @@ -1,4 +1,4 @@ -name: Release Next +name: Release next concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -8,26 +8,27 @@ on: workflow_dispatch: jobs: - app-token: - uses: nocobase/nocobase/.github/workflows/get-nocobase-app-token.yml@main - secrets: inherit publish-npm: runs-on: ubuntu-latest container: node:18 - needs: app-token steps: - - name: Decrypt app token + - uses: actions/create-github-app-token@v1 id: app-token - shell: bash - run: | - ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }}; - BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode); - APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}"); - echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT + with: + app-id: ${{ vars.NOCOBASE_APP_ID }} + private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} + repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.NEXT_PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }} + skip-token-revoke: true + - name: Get GitHub App User ID + id: get-user-id + run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} - name: Checkout uses: actions/checkout@v4 with: ref: next + fetch-depth: 0 - name: Send curl request and parse response env: PKG_USERNAME: ${{ secrets.PKG_USERNAME }} @@ -76,8 +77,8 @@ jobs: - name: publish npmjs.org continue-on-error: true run: | - git config --global user.email "test@mail.com" - git config --global user.name "test" + git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' + git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>' git config --global --add safe.directory /__w/nocobase/nocobase npm config set access public npm config set registry https://registry.npmjs.org/ @@ -99,11 +100,12 @@ jobs: repository: nocobase/pro-plugins path: packages/pro-plugins ref: next + fetch-depth: 0 token: ${{ steps.app-token.outputs.token }} - name: Clone pro repos shell: bash run: | - for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} + for repo in ${{ join(fromJSON(vars.NEXT_PRO_PLUGIN_REPOS), ' ') }} ${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ' ') }} do git clone -b next https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo done @@ -124,3 +126,29 @@ jobs: bash generate-npmignore.sh ignore-src npm config set //pkg-src.nocobase.com/:_authToken=${{ env.PKG_SRC_NOCOBASE_TOKEN }} yarn release:force --no-verify-access --no-git-reset --registry https://pkg-src.nocobase.com --dist-tag=next + - name: Tag + run: | + git reset --hard HEAD~ + git tag v${{ env.NEWVERSION }} + git push origin v${{ env.NEWVERSION }} + cd ./packages/pro-plugins + git reset --hard + git tag v${{ env.NEWVERSION }} + git push origin v${{ env.NEWVERSION }} + cd ../../ + for repo in ${{ join(fromJSON(vars.NEXT_PRO_PLUGIN_REPOS), ' ') }} ${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ' ') }} + do + cd ./packages/pro-plugins/@nocobase/$repo + git reset --hard + git tag v${{ env.NEWVERSION }} + git push origin v${{ env.NEWVERSION }} + cd ../../../../ + done + - name: Run release script + shell: bash + run: | + git fetch + node scripts/release/changelogAndRelease.js --ver alpha --cmsURL ${{ secrets.CMS_URL }} --cmsToken ${{ secrets.CMS_TOKEN }} + env: + PRO_PLUGIN_REPOS: ${{ vars.NEXT_PRO_PLUGIN_REPOS }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9eba8a0b257..7e92a8dcdc2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,22 +10,17 @@ on: - 'v*' jobs: - app-token: - uses: nocobase/nocobase/.github/workflows/get-nocobase-app-token.yml@main - secrets: inherit publish-npm: runs-on: ubuntu-latest container: node:18 - needs: app-token steps: - - name: Decrypt app token + - uses: actions/create-github-app-token@v1 id: app-token - shell: bash - run: | - ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }}; - BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode); - APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}"); - echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT + with: + app-id: ${{ vars.NOCOBASE_APP_ID }} + private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} + repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }} + skip-token-revoke: true - name: Checkout uses: actions/checkout@v3 - name: Send curl request and parse response @@ -76,7 +71,7 @@ jobs: - name: Clone pro repos shell: bash run: | - for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} + for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} ${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ' ') }} do git clone -b main https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo done diff --git a/.gitignore b/.gitignore index e6713802058..a2e9662b727 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ storage/plugins storage/tar storage/tmp storage/app.watch.ts +storage/.upgrading storage/logs-e2e storage/uploads-e2e storage/.pm2-* diff --git a/CHANGELOG.md b/CHANGELOG.md index daaabad2647..572505f6378 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,402 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +## [v1.3.32-beta](https://github.com/nocobase/nocobase/compare/v1.3.31-beta...v1.3.32-beta) - 2024-10-13 + +### 🐛 Bug Fixes + +- **[client]** required relational field still triggers validation error after selecting a value with a variable in data scope ([#5399](https://github.com/nocobase/nocobase/pull/5399)) by @katherinehhh + +## [v1.3.31-beta](https://github.com/nocobase/nocobase/compare/v1.3.30-beta...v1.3.31-beta) - 2024-10-11 + +### 🐛 Bug Fixes + +- **[client]** Fix the issue where using the chinaRegion field in the filter form fails to correctly filter out values ([#5390](https://github.com/nocobase/nocobase/pull/5390)) by @zhangzhonghe + +- **[Action: Import records]** fix import error with wps file ([#5397](https://github.com/nocobase/nocobase/pull/5397)) by @chareice + +## [v1.3.30-beta](https://github.com/nocobase/nocobase/compare/v1.3.29-beta...v1.3.30-beta) - 2024-10-11 + +### 🐛 Bug Fixes + +- **[client]** + - Fix the rendering error that occurs when displaying file collection relationship fields on mobile devices ([#5387](https://github.com/nocobase/nocobase/pull/5387)) by @zhangzhonghe + + - Fix Create Block menu not loading more data collections ([#5388](https://github.com/nocobase/nocobase/pull/5388)) by @zhangzhonghe + +- **[Workflow: Custom action event]** + - Fix custom workflow event did not redirect after successful submission by @katherinehhh + + - Fix custom workflow event did not redirect after successful submission by @katherinehhh + +## [v1.3.29-beta](https://github.com/nocobase/nocobase/compare/v1.3.28-beta...v1.3.29-beta) - 2024-10-10 + +### 🚀 Improvements + +- **[client]** Date variables are also not prohibited in create form ([#5376](https://github.com/nocobase/nocobase/pull/5376)) by @zhangzhonghe + +### 🐛 Bug Fixes + +- **[Workflow: SQL node]** fix error when no result when calling stored procedure in SQL instruction ([#5385](https://github.com/nocobase/nocobase/pull/5385)) by @mytharcher + +- **[Workflow]** fix date field based schedule trigger caused app crash, and also support other data source ([#5364](https://github.com/nocobase/nocobase/pull/5364)) by @mytharcher + +## [v1.3.28-beta](https://github.com/nocobase/nocobase/compare/v1.3.27-beta...v1.3.28-beta) - 2024-10-09 + +### 🚀 Improvements + +- **[client]** Save cdn links as local resources to prevent requesting external resources when deploying on the intranet ([#5375](https://github.com/nocobase/nocobase/pull/5375)) by @zhangzhonghe + +### 🐛 Bug Fixes + +- **[client]** + - Fix the issue where popups opened in the "Users & Permissions" configuration page are obscured by other popups ([#5373](https://github.com/nocobase/nocobase/pull/5373)) by @zhangzhonghe + + - Fix the problem that after deleting a tab in a subpage, it does not take effect after opening it again ([#5362](https://github.com/nocobase/nocobase/pull/5362)) by @zhangzhonghe + + - Fix the issue where inherited collection association fields cannot properly use variables ([#5346](https://github.com/nocobase/nocobase/pull/5346)) by @zhangzhonghe + + - Fix the issue of current and association collection fields affecting each other in configuration ([#5343](https://github.com/nocobase/nocobase/pull/5343)) by @katherinehhh + +- **[Action: Import records]** fixed issue with incorrect results for importing large dates ([#5356](https://github.com/nocobase/nocobase/pull/5356)) by @chareice + +- **[Workflow]** fix switching component of association field in assigned fields caused page crash in create/update node ([#5366](https://github.com/nocobase/nocobase/pull/5366)) by @mytharcher + +- **[Block: Gantt]** Fix the issue where opening a popup in the Gantt block and then closing it causes the subpage to also close ([#5370](https://github.com/nocobase/nocobase/pull/5370)) by @zhangzhonghe + +## [v1.3.27-beta](https://github.com/nocobase/nocobase/compare/v1.3.26-beta...v1.3.27-beta) - 2024-09-30 + +### 🐛 Bug Fixes + +- **[client]** Fix variable "Table selected records" ([#5337](https://github.com/nocobase/nocobase/pull/5337)) by @zhangzhonghe + +- **[Workflow: Custom action event]** fix custom action event not triggers in association block by @mytharcher + +## [v1.3.26-beta](https://github.com/nocobase/nocobase/compare/v1.3.25-beta...v1.3.26-beta) - 2024-09-29 + +### 🚀 Improvements + +- **[client]** Hide scrollbars on mobile ([#5339](https://github.com/nocobase/nocobase/pull/5339)) by @zhangzhonghe + +### 🐛 Bug Fixes + +- **[client]** + - Fix the issue of not being able to open sub-pages in embedded pages ([#5335](https://github.com/nocobase/nocobase/pull/5335)) by @zhangzhonghe + + - Fix the issue of pop-up windows being obscured ([#5338](https://github.com/nocobase/nocobase/pull/5338)) by @zhangzhonghe + + - Fix the issue of abnormal style when creating blocks with data templates in mobile subpages ([#5340](https://github.com/nocobase/nocobase/pull/5340)) by @zhangzhonghe + + - Fix the issue of not refreshing the page block data when closing a subpage via the page menu ([#5331](https://github.com/nocobase/nocobase/pull/5331)) by @zhangzhonghe + +- **[Action: Export records]** fix export format for decimal type fields ([#5316](https://github.com/nocobase/nocobase/pull/5316)) by @chareice + +- **[Block: Kanban]** Fix the issue that the popup window could not be opened after clicking on the Kanban card in the embedded page ([#5326](https://github.com/nocobase/nocobase/pull/5326)) by @zhangzhonghe + +## [v1.3.25-beta](https://github.com/nocobase/nocobase/compare/v1.3.24-beta...v1.3.25-beta) - 2024-09-25 + +### 🚀 Improvements + +- **[client]** update and improve Japanese translations in ja_JP files ([#5292](https://github.com/nocobase/nocobase/pull/5292)) by @Albert-mah + +- **[Workflow]** add error handling for unregistered node type ([#5319](https://github.com/nocobase/nocobase/pull/5319)) by @mytharcher + +### 🐛 Bug Fixes + +- **[client]** Fix for not displaying full fields in variables ([#5310](https://github.com/nocobase/nocobase/pull/5310)) by @zhangzhonghe + +- **[Workflow]** fix non-existed field in collection trigger causes error ([#5318](https://github.com/nocobase/nocobase/pull/5318)) by @mytharcher + +- **[Action: Export records]** Fix fields from assicated tables are not processed by the field interface ([#5296](https://github.com/nocobase/nocobase/pull/5296)) by @gchust + +## [v1.3.24-beta](https://github.com/nocobase/nocobase/compare/v1.3.23-beta...v1.3.24-beta) - 2024-09-23 + +### 🐛 Bug Fixes + +- **[client]** + - markdown report error with using `#each` in handlebars ([#5305](https://github.com/nocobase/nocobase/pull/5305)) by @katherinehhh + + - Fix issue where the collection from external data source does not support sorting on table columns ([#5293](https://github.com/nocobase/nocobase/pull/5293)) by @katherinehhh + +- **[Data visualization]** Fix style issues of chart blocks when using dark mode themes ([#5302](https://github.com/nocobase/nocobase/pull/5302)) by @2013xile + +## [v1.3.23-beta](https://github.com/nocobase/nocobase/compare/v1.3.22-beta...v1.3.23-beta) - 2024-09-19 + +### 🚀 Improvements + +- **[Users]** Optimize performance for rendering the user management table ([#5276](https://github.com/nocobase/nocobase/pull/5276)) by @2013xile + +- **[Departments]** Optimize performance for rendering the user table in department management by @2013xile + +### 🐛 Bug Fixes + +- **[client]** + - Fix incorrect `rowKey` of the `General action permissions table` in Users & Permissions page ([#5287](https://github.com/nocobase/nocobase/pull/5287)) by @gchust + + - Fix the issue where setting a date variable for the date field in the filter form results in incorrect filter results ([#5257](https://github.com/nocobase/nocobase/pull/5257)) by @zhangzhonghe + + - column width issue with scroll.y when table has no data ([#5256](https://github.com/nocobase/nocobase/pull/5256)) by @katherinehhh + + - Fix the problem of blank rows at the beginning of a table block ([#5284](https://github.com/nocobase/nocobase/pull/5284)) by @zhangzhonghe + +- **[create-nocobase-app]** Fix the issue where the popup for configuring Sequence rules lacked a submit button when adding a new Sequence field ([#5281](https://github.com/nocobase/nocobase/pull/5281)) by @zhangzhonghe + +- **[database]** import with checkbox field ([#4992](https://github.com/nocobase/nocobase/pull/4992)) by @chareice + +- **[evaluators]** Fix error caused by `Matrix` type from mathjs ([#5270](https://github.com/nocobase/nocobase/pull/5270)) by @mytharcher + +- **[Calendar]** Cannot select the option to delete the schedule popup ([#5274](https://github.com/nocobase/nocobase/pull/5274)) by @katherinehhh + +- **[Action: Export records]** Fix missing request context when generating data sheet in export action ([#5286](https://github.com/nocobase/nocobase/pull/5286)) by @gchust + +## [v1.3.22-beta](https://github.com/nocobase/nocobase/compare/v1.3.21-beta...v1.3.22-beta) - 2024-09-12 + +### 🎉 New Features + +- **[Action: Custom request]** Support for API token variables in the "Custom Request Button" configuration ([#5263](https://github.com/nocobase/nocobase/pull/5263)) by @zhangzhonghe +Reference: [Custom request](https://docs.nocobase.com/handbook/action-custom-request) +### 🚀 Improvements + +- **[Collection field: Markdown(Vditor)]** Support Vidtor when selecting fields in the UI for external data sources ([#5246](https://github.com/nocobase/nocobase/pull/5246)) by @katherinehhh + +### 🐛 Bug Fixes + +- **[Calendar]** issue where the calendar block cannot display correctly when the end date crosses months ([#5239](https://github.com/nocobase/nocobase/pull/5239)) by @katherinehhh + +## [v1.3.21-beta](https://github.com/nocobase/nocobase/compare/v1.3.20-beta...v1.3.21-beta) - 2024-09-10 + +### 🐛 Bug Fixes + +- **[client]** Fix error when using linkage rules (NocoBase installed via create-nocobase-app) ([#5249](https://github.com/nocobase/nocobase/pull/5249)) by @zhangzhonghe + +## [v1.3.20-beta](https://github.com/nocobase/nocobase/compare/v1.3.19-beta...v1.3.20-beta) - 2024-09-10 + +### 🚀 Improvements + +- **[client]** Support for displaying deeper level association fields in data blocks ([#5243](https://github.com/nocobase/nocobase/pull/5243)) by @zhangzhonghe + +### 🐛 Bug Fixes + +- **[client]** + - Menu modifications do not take effect in real-time ([#5207](https://github.com/nocobase/nocobase/pull/5207)) by @katherinehhh + + - Support association field preloading in Handlebars templates ([#5236](https://github.com/nocobase/nocobase/pull/5236)) by @katherinehhh + +- **[Data visualization]** Fix incorrect data source context when multiple data sources exist ([#5237](https://github.com/nocobase/nocobase/pull/5237)) by @2013xile + +## [v1.3.19-beta](https://github.com/nocobase/nocobase/compare/v1.3.18-beta...v1.3.19-beta) - 2024-09-08 + +### 🐛 Bug Fixes + +- **[client]** Fix URL anomalies caused by using popups together with Link buttons ([#5219](https://github.com/nocobase/nocobase/pull/5219)) by @zhangzhonghe + +## [v1.3.18-beta](https://github.com/nocobase/nocobase/compare/v1.3.17-beta...v1.3.18-beta) - 2024-09-08 + +### 🐛 Bug Fixes + +- **[Collection field: Many to many (array)]** Fix the error when deleting a collection contains m2m array fields ([#5231](https://github.com/nocobase/nocobase/pull/5231)) by @2013xile + +## [v1.3.17-beta](https://github.com/nocobase/nocobase/compare/v1.3.16-beta...v1.3.17-beta) - 2024-09-07 + +### 🎉 New Features + +- **[client]** Supports configuration of linkage rules in sub-forms and sub-forms. ([#5159](https://github.com/nocobase/nocobase/pull/5159)) by @zhangzhonghe + +### 🚀 Improvements + +- **[client]** + - default time for display is 00:00:00 ([#5226](https://github.com/nocobase/nocobase/pull/5226)) by @chenos + + - plugins can also be enabled when the plugin dependency version is inconsistent ([#5225](https://github.com/nocobase/nocobase/pull/5225)) by @chenos + +- **[server]** provide more user-friendly application-level error messages ([#5220](https://github.com/nocobase/nocobase/pull/5220)) by @chenos + +### 🐛 Bug Fixes + +- **[client]** Fix the "Maximum call stack size exceeded" error that occurs in the details block ([#5228](https://github.com/nocobase/nocobase/pull/5228)) by @zhangzhonghe + +- **[Collection field: Many to many (array)]** Fix the error where setting a field of `uid` type as target key for a many to many (array) field ([#5229](https://github.com/nocobase/nocobase/pull/5229)) by @2013xile + +- **[UI schema storage]** Fix the issue that member roles clicking the button reported no permission ([#5206](https://github.com/nocobase/nocobase/pull/5206)) by @zhangzhonghe + +- **[Workflow]** Fix trigger type column showing wrong text after new workflow created ([#5222](https://github.com/nocobase/nocobase/pull/5222)) by @mytharcher + +- **[Users]** Remove phone format validation when editing user phones in user management ([#5221](https://github.com/nocobase/nocobase/pull/5221)) by @2013xile + +## [v1.3.16-beta](https://github.com/nocobase/nocobase/compare/v1.3.15-beta...v1.3.16-beta) - 2024-09-06 + +### 🚀 Improvements + +- **[client]** + - Placeholder added when the user has UI configuration permissions but no view permissions for the collection ([#5208](https://github.com/nocobase/nocobase/pull/5208)) by @katherinehhh + + - Display system title when logo is missing. ([#5175](https://github.com/nocobase/nocobase/pull/5175)) by @maoyutofu + +- **[Authentication]** support line break in system title ([#5211](https://github.com/nocobase/nocobase/pull/5211)) by @chenos + +- **[Workflow: SQL node]** Change result data structure of SQL node to only contains data. ([#5189](https://github.com/nocobase/nocobase/pull/5189)) by @mytharcher +Reference: [SQL Operation](https://docs.nocobase.com/handbook/workflow/nodes/sql) +- **[Access control]** Make the `Permissions` Tab pannel of the `Users & Permissions` configuration page expandable. ([#5216](https://github.com/nocobase/nocobase/pull/5216)) by @zhangzhonghe +Reference: [Development Guide](https://docs.nocobase.com/handbook/acl#development-guide) +- **[Action: Batch edit]** batch updated and batch edit, change 'All' to 'Entire collection" ([#5200](https://github.com/nocobase/nocobase/pull/5200)) by @katherinehhh + +### 🐛 Bug Fixes + +- **[client]** + - component display error when switching assignment types in linkage rules ([#5180](https://github.com/nocobase/nocobase/pull/5180)) by @katherinehhh + + - Fix an issue where using variables in data scope reported an error. ([#5195](https://github.com/nocobase/nocobase/pull/5195)) by @zhangzhonghe + + - issue with custom request refreshDataBlockRequest ([#5188](https://github.com/nocobase/nocobase/pull/5188)) by @katherinehhh + +- **[Data visualization]** Fixed the issue of getting wrong value when aggregating select fields ([#5214](https://github.com/nocobase/nocobase/pull/5214)) by @2013xile + +- **[Data source manager]** Fixed incorrect `rowKey` of the datasource table in `Users & Permissions` page ([#5215](https://github.com/nocobase/nocobase/pull/5215)) by @gchust + +- **[Workflow: HTTP request node]** Fix error when using non-string variable in request parameters. ([#5204](https://github.com/nocobase/nocobase/pull/5204)) by @mytharcher + +- **[Collection field: Formula]** fix formula field serve test ([#5197](https://github.com/nocobase/nocobase/pull/5197)) by @katherinehhh + +- **[App backup & restore (deprecated)]** fix test case errors ([#5201](https://github.com/nocobase/nocobase/pull/5201)) by @chenos + +- **[Data source: REST API]** + - collection name should be disabled in rest-api collection by @katherinehhh + + - Rest api locale improve by @katherinehhh + +## [v1.3.15-beta](https://github.com/nocobase/nocobase/compare/v1.3.14-beta...v1.3.15-beta) - 2024-09-04 + +### 🐛 Bug Fixes + +- **[Workflow]** Fix missed fields in workflow variables. ([#5187](https://github.com/nocobase/nocobase/pull/5187)) by @mytharcher + +- **[Collection field: Markdown(Vditor)]** issue with markdown(Vditor) ([#5176](https://github.com/nocobase/nocobase/pull/5176)) by @katherinehhh + +## [v1.3.14-beta](https://github.com/nocobase/nocobase/compare/v1.3.13-beta...v1.3.14-beta) - 2024-09-03 + +### 🎉 New Features + +- **[client]** Add support for many-to-many association fields. ([#5178](https://github.com/nocobase/nocobase/pull/5178)) by @zhangzhonghe + +### 🚀 Improvements + +- **[Action: Custom request]** remove linkageRule for custom request in create form ([#5179](https://github.com/nocobase/nocobase/pull/5179)) by @katherinehhh + +### 🐛 Bug Fixes + +- **[Collection field: Formula]** formula field adaptation time field ([#5168](https://github.com/nocobase/nocobase/pull/5168)) by @katherinehhh + +## [v1.3.13-beta](https://github.com/nocobase/nocobase/compare/v1.3.12-beta...v1.3.13-beta) - 2024-09-03 + +### 🐛 Bug Fixes + +- **[Action: Export records]** Fixed incorrect export of relational data ([#5170](https://github.com/nocobase/nocobase/pull/5170)) by @chareice + +## [v1.3.12-beta](https://github.com/nocobase/nocobase/compare/v1.3.11-beta...v1.3.12-beta) - 2024-09-01 + +### Merged + +- fix(mobile): fix permission [`#5163`](https://github.com/nocobase/nocobase/pull/5163) + +### Commits + +- chore(versions): 😊 publish v1.3.12-beta [`774c296`](https://github.com/nocobase/nocobase/commit/774c2961d47aa17d1a9da7a595bb070f34aee11b) +- chore: update changelog [`7f9a116`](https://github.com/nocobase/nocobase/commit/7f9a11698f3126257529ce4a91670239900f2ec3) +- chore: update e2e test [`49db3e4`](https://github.com/nocobase/nocobase/commit/49db3e490821cd59aaba2f58ed2bb78051a86ad9) + +## [v1.3.11-beta](https://github.com/nocobase/nocobase/compare/v1.3.10-beta...v1.3.11-beta) - 2024-08-31 + +### Commits + +- chore(versions): 😊 publish v1.3.11-beta [`517e199`](https://github.com/nocobase/nocobase/commit/517e199ed7a8e7dc81a06c50389ef41b6891b133) +- chore: update changelog [`373f517`](https://github.com/nocobase/nocobase/commit/373f51773b772886cc8db3cb50184562113c62eb) + +## [v1.3.10-beta](https://github.com/nocobase/nocobase/compare/v1.3.9-beta...v1.3.10-beta) - 2024-08-31 + +### Merged + +- fix: issue with association select data scope linkage in sub-table [`#5160`](https://github.com/nocobase/nocobase/pull/5160) +- fix: issue in data selector other block should display Markdown, not 'Add Text' [`#5161`](https://github.com/nocobase/nocobase/pull/5161) +- fix(data-vi): issue of parsing variables in filter block [`#5157`](https://github.com/nocobase/nocobase/pull/5157) +- fix(data-vi): transform the values of decimal fields from type string to number [`#5155`](https://github.com/nocobase/nocobase/pull/5155) + +### Commits + +- chore(versions): 😊 publish v1.3.10-beta [`5afac9c`](https://github.com/nocobase/nocobase/commit/5afac9cf82c78db4a7ee8ddb01a60597939ac82d) +- chore: update changelog [`6fceac1`](https://github.com/nocobase/nocobase/commit/6fceac15827a10b6fba65e98314c37f3f9e697ba) +- chore: update comment [`6e45780`](https://github.com/nocobase/nocobase/commit/6e4578056556c1c60ac721ff990a81ed37339074) + +## [v1.3.9-beta](https://github.com/nocobase/nocobase/compare/v1.3.8-beta...v1.3.9-beta) - 2024-08-29 + +### Merged + +- fix(mobile): should not force redirect to mobile page [`#5152`](https://github.com/nocobase/nocobase/pull/5152) +- chore: support year data type in mysql [`#5123`](https://github.com/nocobase/nocobase/pull/5123) + +### Commits + +- chore(versions): 😊 publish v1.3.9-beta [`bf5011f`](https://github.com/nocobase/nocobase/commit/bf5011f75a7a9b26db7fef7aa4be28d7e4e077b4) +- chore: update changelog [`b2fc646`](https://github.com/nocobase/nocobase/commit/b2fc646e5aa64d2ade03ce6fca78753cfddc26ec) + +## [v1.3.8-beta](https://github.com/nocobase/nocobase/compare/v1.3.7-beta...v1.3.8-beta) - 2024-08-29 + +### Commits + +- chore(versions): 😊 publish v1.3.8-beta [`39d021a`](https://github.com/nocobase/nocobase/commit/39d021a9aa29bef9cf15d4af546060fc4b1dbd10) +- chore: update changelog [`9f66c14`](https://github.com/nocobase/nocobase/commit/9f66c14968639d90b399d087eefac7a0c4ea4383) + +## [v1.3.7-beta](https://github.com/nocobase/nocobase/compare/v1.3.6-beta...v1.3.7-beta) - 2024-08-29 + +### Merged + +- fix: add text support handlebars [`#5150`](https://github.com/nocobase/nocobase/pull/5150) + +### Commits + +- chore(versions): 😊 publish v1.3.7-beta [`f429d13`](https://github.com/nocobase/nocobase/commit/f429d1326433e7f290e552ca91548d21b5af92e4) +- chore: update changelog [`b41e477`](https://github.com/nocobase/nocobase/commit/b41e47757ec0d1f7b0af917e25ff5b4a436042aa) + +## [v1.3.6-beta](https://github.com/nocobase/nocobase/compare/v1.3.5-beta...v1.3.6-beta) - 2024-08-29 + +### Merged + +- fix: association select data scope linkage should support edit form [`#5149`](https://github.com/nocobase/nocobase/pull/5149) + +### Commits + +- chore(versions): 😊 publish v1.3.6-beta [`39c7ce4`](https://github.com/nocobase/nocobase/commit/39c7ce4741801819b98970b95c1663915a8c3bff) +- chore: update changelog [`cfbc2a6`](https://github.com/nocobase/nocobase/commit/cfbc2a6c15a5dfb8c0684051df1cf01499ff30ac) + +## [v1.3.5-beta](https://github.com/nocobase/nocobase/compare/v1.3.4-beta...v1.3.5-beta) - 2024-08-28 + +### Merged + +- fix: association select data scope linkage should be supported in sub-form [`#5146`](https://github.com/nocobase/nocobase/pull/5146) +- fix(mobile): resovle redirect issue [`#5145`](https://github.com/nocobase/nocobase/pull/5145) +- feat(plugin-workflow): allow to delete execution in list [`#5135`](https://github.com/nocobase/nocobase/pull/5135) +- fix(defaultValue): ignores variable values that do not match the current field [`#5122`](https://github.com/nocobase/nocobase/pull/5122) +- chore(deps-dev): bump eslint-plugin-jest-dom from 5.1.0 to 5.4.0 [`#5138`](https://github.com/nocobase/nocobase/pull/5138) +- chore(deps): bump @ant-design/pro-layout from 7.17.16 to 7.19.12 [`#5137`](https://github.com/nocobase/nocobase/pull/5137) +- fix(template): fix error on form block submission [`#5133`](https://github.com/nocobase/nocobase/pull/5133) +- feat: add support for opening via URL [`#5098`](https://github.com/nocobase/nocobase/pull/5098) +- fix(release): decrypt token error occasionally [`#5143`](https://github.com/nocobase/nocobase/pull/5143) + +### Commits + +- chore(versions): 😊 publish v1.3.5-beta [`35e8f89`](https://github.com/nocobase/nocobase/commit/35e8f89c75800a612db27485c96196555f922273) +- Revert "chore(deps): bump @ant-design/pro-layout from 7.17.16 to 7.19.12 (#5137)" [`3f461ad`](https://github.com/nocobase/nocobase/commit/3f461ad8f079b4c2cf5975c1e26271f55021e08a) +- fix(release): pro image ci [`e45d450`](https://github.com/nocobase/nocobase/commit/e45d45015792138e7378741bdaf488de714b365d) + +## [v1.3.4-beta](https://github.com/nocobase/nocobase/compare/v1.3.3-beta...v1.3.4-beta) - 2024-08-27 + +### Merged + +- refactor: set remainsTheSame as the default value for field editing in bulk editing action [`#5124`](https://github.com/nocobase/nocobase/pull/5124) + +### Commits + +- chore(versions): 😊 publish v1.3.4-beta [`a011748`](https://github.com/nocobase/nocobase/commit/a0117480e037e48a23f59921110003047a1a174b) +- chore: update changelog [`3403e8d`](https://github.com/nocobase/nocobase/commit/3403e8d76684950d6962a6110a4440eb95856a35) ## [v1.3.3-beta](https://github.com/nocobase/nocobase/compare/v1.3.2-beta...v1.3.3-beta) - 2024-08-27 diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md new file mode 100644 index 00000000000..c2f6095ceeb --- /dev/null +++ b/CHANGELOG.zh-CN.md @@ -0,0 +1,298 @@ +# 更新日志 + +本项目的所有重要更改都将记录在此文件中。 + +格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), +并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。 + +## [v1.3.32-beta](https://github.com/nocobase/nocobase/compare/v1.3.31-beta...v1.3.32-beta) - 2024-10-13 + +### 🐛 修复 + +- **[client]** 关系字段设置必填,数据范围中设置变量后,选中值却报字段必填不通过 ([#5399](https://github.com/nocobase/nocobase/pull/5399)) by @katherinehhh + +## [v1.3.31-beta](https://github.com/nocobase/nocobase/compare/v1.3.30-beta...v1.3.31-beta) - 2024-10-11 + +### 🐛 修复 + +- **[client]** 修复在筛选表单中使用行政区划字段无法正确筛选出值的问题 ([#5390](https://github.com/nocobase/nocobase/pull/5390)) by @zhangzhonghe + +- **[操作:导入记录]** 修复导入 wps 文件报错的问题 ([#5397](https://github.com/nocobase/nocobase/pull/5397)) by @chareice + +## [v1.3.30-beta](https://github.com/nocobase/nocobase/compare/v1.3.29-beta...v1.3.30-beta) - 2024-10-11 + +### 🐛 修复 + +- **[client]** + - 修复在移动端中,显示文件表关系字段时报渲染错误的问题 ([#5387](https://github.com/nocobase/nocobase/pull/5387)) by @zhangzhonghe + + - 修复创建区块菜单无法加载更多数据表的问题 ([#5388](https://github.com/nocobase/nocobase/pull/5388)) by @zhangzhonghe + +- **[工作流:自定义操作事件]** + - 修复 自定义工作流事件提交成功后跳转不生效 by @katherinehhh + + - 自定义工作流事件提交成功后跳转不生效 by @katherinehhh + +## [v1.3.29-beta](https://github.com/nocobase/nocobase/compare/v1.3.28-beta...v1.3.29-beta) - 2024-10-10 + +### 🚀 优化 + +- **[client]** 创建表单中也不禁用日期变量 ([#5376](https://github.com/nocobase/nocobase/pull/5376)) by @zhangzhonghe + +### 🐛 修复 + +- **[工作流:SQL 节点]** 修复在 SQL 节点中调用存储过程没有返回结果时导致错误的问题 ([#5385](https://github.com/nocobase/nocobase/pull/5385)) by @mytharcher + +- **[工作流]** 修复基于时间字段的定时任务导致报错的问题,并支持其他数据库数据源 ([#5364](https://github.com/nocobase/nocobase/pull/5364)) by @mytharcher + +## [v1.3.28-beta](https://github.com/nocobase/nocobase/compare/v1.3.27-beta...v1.3.28-beta) - 2024-10-09 + +### 🚀 优化 + +- **[client]** 将 cdn 链接保存为本地资源,以防止在内网部署时请求外部资源 ([#5375](https://github.com/nocobase/nocobase/pull/5375)) by @zhangzhonghe + +### 🐛 修复 + +- **[client]** + - 修复在“用户和权限”配置页打开的弹窗被其它弹窗遮挡的问题 ([#5373](https://github.com/nocobase/nocobase/pull/5373)) by @zhangzhonghe + + - 修复在子页面中删除 tab 页后,再次打开后未生效的问题 ([#5362](https://github.com/nocobase/nocobase/pull/5362)) by @zhangzhonghe + + - 修复继承表关系字段无法正常使用变量的问题 ([#5346](https://github.com/nocobase/nocobase/pull/5346)) by @zhangzhonghe + + - 修复字段配置中当前数据表字段与关系表字段互相影响缺陷 ([#5343](https://github.com/nocobase/nocobase/pull/5343)) by @katherinehhh + +- **[操作:导入记录]** 修复导入大日期结果不正确的问题 ([#5356](https://github.com/nocobase/nocobase/pull/5356)) by @chareice + +- **[工作流]** 修复新增、更新节点中配置关系字段赋值时切换组件导致的页面崩溃 ([#5366](https://github.com/nocobase/nocobase/pull/5366)) by @mytharcher + +- **[区块:甘特图]** 修复在甘特图中打开弹窗,然后再关闭,导致子页面也被关闭的问题 ([#5370](https://github.com/nocobase/nocobase/pull/5370)) by @zhangzhonghe + +## [v1.3.27-beta](https://github.com/nocobase/nocobase/compare/v1.3.26-beta...v1.3.27-beta) - 2024-09-30 + +### 🐛 修复 + +- **[client]** 修复变量“表格中选中的记录” ([#5337](https://github.com/nocobase/nocobase/pull/5337)) by @zhangzhonghe + +- **[工作流:自定义操作事件]** 修复自定义操作事件在关系区块中不触发的问题 by @mytharcher + +## [v1.3.26-beta](https://github.com/nocobase/nocobase/compare/v1.3.25-beta...v1.3.26-beta) - 2024-09-29 + +### 🚀 优化 + +- **[client]** 隐藏移动端的滚动条 ([#5339](https://github.com/nocobase/nocobase/pull/5339)) by @zhangzhonghe + +### 🐛 修复 + +- **[client]** + - 修复在嵌入页面中无法打开子页面的问题 ([#5335](https://github.com/nocobase/nocobase/pull/5335)) by @zhangzhonghe + + - 修复弹窗被遮挡的问题 ([#5338](https://github.com/nocobase/nocobase/pull/5338)) by @zhangzhonghe + + - 修复移动端子页面中,使用数据模板创建区块时,样式异常的问题 ([#5340](https://github.com/nocobase/nocobase/pull/5340)) by @zhangzhonghe + + - 修复通过页面菜单关闭子页面时,不刷新页面区块数据的问题 ([#5331](https://github.com/nocobase/nocobase/pull/5331)) by @zhangzhonghe + +- **[操作:导出记录]** 修复 decimal 类型字段的导出格式 ([#5316](https://github.com/nocobase/nocobase/pull/5316)) by @chareice + +- **[区块:看板]** 修复在嵌入页面中,点击看板卡片后,无法打开弹窗的问题 ([#5326](https://github.com/nocobase/nocobase/pull/5326)) by @zhangzhonghe + +## [v1.3.25-beta](https://github.com/nocobase/nocobase/compare/v1.3.24-beta...v1.3.25-beta) - 2024-09-25 + +### 🚀 优化 + +- **[client]** 增加日语本地化翻译 ([#5292](https://github.com/nocobase/nocobase/pull/5292)) by @Albert-mah + +- **[工作流]** 增加对未注册的节点类型导致错误的跟踪报错 ([#5319](https://github.com/nocobase/nocobase/pull/5319)) by @mytharcher + +### 🐛 修复 + +- **[client]** 修复变量中没有显示完整字段的问题 ([#5310](https://github.com/nocobase/nocobase/pull/5310)) by @zhangzhonghe + +- **[工作流]** 修复数据表事件中发生改变的字段被删除后报错的问题 ([#5318](https://github.com/nocobase/nocobase/pull/5318)) by @mytharcher + +- **[操作:导出记录]** 修复导出操作时,关联表中的字段未执行interface渲染逻辑 ([#5296](https://github.com/nocobase/nocobase/pull/5296)) by @gchust + +## [v1.3.24-beta](https://github.com/nocobase/nocobase/compare/v1.3.23-beta...v1.3.24-beta) - 2024-09-23 + +### 🐛 修复 + +- **[client]** + - markdown 的handlebars 模板使用#each 渲染数组数据时数据没有正常显示 ([#5305](https://github.com/nocobase/nocobase/pull/5305)) by @katherinehhh + + - 外部数据库数据源表格列头不支持排序的问题 ([#5293](https://github.com/nocobase/nocobase/pull/5293)) by @katherinehhh + +- **[数据可视化]** 修复图表区块在暗黑主题下的样式问题 ([#5302](https://github.com/nocobase/nocobase/pull/5302)) by @2013xile + +## [v1.3.23-beta](https://github.com/nocobase/nocobase/compare/v1.3.22-beta...v1.3.23-beta) - 2024-09-19 + +### 🚀 优化 + +- **[用户]** 优化用户管理表格的渲染速度 ([#5276](https://github.com/nocobase/nocobase/pull/5276)) by @2013xile + +- **[部门]** 优化部门管理中的用户表格的渲染速度 by @2013xile + +### 🐛 修复 + +- **[client]** + - 修复用户和权限设置页面中`通用操作权限表格`的`rowKey`不正确问题 ([#5287](https://github.com/nocobase/nocobase/pull/5287)) by @gchust + + - 修复在筛选表单中,为日期字段设置日期变量后,导致的筛选结果不正确的问题 ([#5257](https://github.com/nocobase/nocobase/pull/5257)) by @zhangzhonghe + + - 表格没有数据时且设置了区块高度时无法设置列宽 ([#5256](https://github.com/nocobase/nocobase/pull/5256)) by @katherinehhh + + - 修复表格区块在一开始出现空白行的问题 ([#5284](https://github.com/nocobase/nocobase/pull/5284)) by @zhangzhonghe + +- **[create-nocobase-app]** 修复在新增自动编码字段时,配置编码规则的弹窗缺少提交按钮的问题 ([#5281](https://github.com/nocobase/nocobase/pull/5281)) by @zhangzhonghe + +- **[database]** 导入支持勾选字段 ([#4992](https://github.com/nocobase/nocobase/pull/4992)) by @chareice + +- **[evaluators]** 修复 Math.js 计算输出矩阵类型导致的问题 ([#5270](https://github.com/nocobase/nocobase/pull/5270)) by @mytharcher + +- **[日历]** 删除日程弹窗选项不能选择 ([#5274](https://github.com/nocobase/nocobase/pull/5274)) by @katherinehhh + +- **[操作:导出记录]** 修复在导出操作中,生成数据表格时,缺少上下文的问题 ([#5286](https://github.com/nocobase/nocobase/pull/5286)) by @gchust + +## [v1.3.22-beta](https://github.com/nocobase/nocobase/compare/v1.3.21-beta...v1.3.22-beta) - 2024-09-12 + +### 🎉 新特性 + +- **[操作:自定义请求]** 自定义请求按钮的配置中,支持使用 API token 变量 ([#5263](https://github.com/nocobase/nocobase/pull/5263)) by @zhangzhonghe +参考文档:[自定义请求-变量](https://docs-cn.nocobase.com/handbook/action-custom-request#%E5%8F%98%E9%87%8F) +### 🚀 优化 + +- **[数据表字段:Markdown(Vditor)]** 在外部数据源中选字段 UI 的时支持 Vidtor ([#5246](https://github.com/nocobase/nocobase/pull/5246)) by @katherinehhh + +### 🐛 修复 + +- **[日历]** 日历区块结束日期跨月时无法正确显示的问题 ([#5239](https://github.com/nocobase/nocobase/pull/5239)) by @katherinehhh + +## [v1.3.21-beta](https://github.com/nocobase/nocobase/compare/v1.3.20-beta...v1.3.21-beta) - 2024-09-10 + +### 🐛 修复 + +- **[client]** 修复在使用联动规则时报错的问题(通过 create-nocobase-app 安装的 NocoBase) ([#5249](https://github.com/nocobase/nocobase/pull/5249)) by @zhangzhonghe + +## [v1.3.20-beta](https://github.com/nocobase/nocobase/compare/v1.3.19-beta...v1.3.20-beta) - 2024-09-10 + +### 🚀 优化 + +- **[client]** 数据区块中支持显示更深层级的关系字段 ([#5243](https://github.com/nocobase/nocobase/pull/5243)) by @zhangzhonghe + +### 🐛 修复 + +- **[client]** + - 修改菜单标题时没有实时生效 ([#5207](https://github.com/nocobase/nocobase/pull/5207)) by @katherinehhh + + - 支持 Handlebars 模板中关系字段的预加载 ([#5236](https://github.com/nocobase/nocobase/pull/5236)) by @katherinehhh + +- **[数据可视化]** 修复存在多个数据源时,图表的数据源上下文不正确的问题 ([#5237](https://github.com/nocobase/nocobase/pull/5237)) by @2013xile + +## [v1.3.19-beta](https://github.com/nocobase/nocobase/compare/v1.3.18-beta...v1.3.19-beta) - 2024-09-08 + +### 🐛 修复 + +- **[client]** 修复因弹窗与 Link 按钮一起使用,所导致的 URL 异常的问题 ([#5219](https://github.com/nocobase/nocobase/pull/5219)) by @zhangzhonghe + +## [v1.3.18-beta](https://github.com/nocobase/nocobase/compare/v1.3.17-beta...v1.3.18-beta) - 2024-09-08 + +### 🐛 修复 + +- **[数据表字段:多对多 (数组)]** 修复删除包含多对多(数组)字段的数据表时出现的错误 ([#5231](https://github.com/nocobase/nocobase/pull/5231)) by @2013xile + +## [v1.3.17-beta](https://github.com/nocobase/nocobase/compare/v1.3.16-beta...v1.3.17-beta) - 2024-09-07 + +### 🎉 新特性 + +- **[client]** 支持在子表单和子表格中配置联动规则。 ([#5159](https://github.com/nocobase/nocobase/pull/5159)) by @zhangzhonghe + +### 🚀 优化 + +- **[client]** + - 显示时间时默认时间为 00:00:00 ([#5226](https://github.com/nocobase/nocobase/pull/5226)) by @chenos + + - 插件依赖版本不一致时也可以激活插件 ([#5225](https://github.com/nocobase/nocobase/pull/5225)) by @chenos + +- **[server]** 提供更友好的应用级错误提示 ([#5220](https://github.com/nocobase/nocobase/pull/5220)) by @chenos + +### 🐛 修复 + +- **[client]** 修复在详情区块中出现的 “Maximum call stack size exceeded” 错误 ([#5228](https://github.com/nocobase/nocobase/pull/5228)) by @zhangzhonghe + +- **[数据表字段:多对多 (数组)]** 修复将 `uid` 类型的字段设置为多对多(数组)字段的目标键时出现的报错 ([#5229](https://github.com/nocobase/nocobase/pull/5229)) by @2013xile + +- **[UI schema 存储服务]** 修复 member 角色点击按钮报无权限的问题 ([#5206](https://github.com/nocobase/nocobase/pull/5206)) by @zhangzhonghe + +- **[工作流]** 修复创建工作流后类型列展示错误文字的问题 ([#5222](https://github.com/nocobase/nocobase/pull/5222)) by @mytharcher + +- **[用户]** 移除在用户管理中编辑用户资料时的手机号格式验证 ([#5221](https://github.com/nocobase/nocobase/pull/5221)) by @2013xile + +## [v1.3.16-beta](https://github.com/nocobase/nocobase/compare/v1.3.15-beta...v1.3.16-beta) - 2024-09-06 + +### 🚀 优化 + +- **[client]** + - 有UI配置权限但没有数据表查看权限时添加占位 ([#5208](https://github.com/nocobase/nocobase/pull/5208)) by @katherinehhh + + - 当缺少 logo 时,显示系统标题。 ([#5175](https://github.com/nocobase/nocobase/pull/5175)) by @maoyutofu + +- **[用户认证]** 系统标题支持换行 ([#5211](https://github.com/nocobase/nocobase/pull/5211)) by @chenos + +- **[工作流:SQL 节点]** 将 SQL 操作节点的结果数据结构调整为仅包含数据部分。 ([#5189](https://github.com/nocobase/nocobase/pull/5189)) by @mytharcher +Reference: [SQL 操作](https://docs-cn.nocobase.com/handbook/workflow/nodes/sql) +- **[权限控制]** 使 `Users & Permissions` 配置页的 `Permissions` Tab 面板可扩展。 ([#5216](https://github.com/nocobase/nocobase/pull/5216)) by @zhangzhonghe +Reference: [开发指南](https://docs-cn.nocobase.com/handbook/acl#%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97) +- **[操作:批量编辑]** 批量更新、批量编辑的 文案 ,“所有” 改成 “全表” ([#5200](https://github.com/nocobase/nocobase/pull/5200)) by @katherinehhh + +### 🐛 修复 + +- **[client]** + - 修复联动规则中切换赋值类型时组件显示错误 ([#5180](https://github.com/nocobase/nocobase/pull/5180)) by @katherinehhh + + - 修复数据范围中使用变量报错的问题。 ([#5195](https://github.com/nocobase/nocobase/pull/5195)) by @zhangzhonghe + + - 自定义请求按钮的请求后刷新数据设置不生效 ([#5188](https://github.com/nocobase/nocobase/pull/5188)) by @katherinehhh + +- **[数据可视化]** 修复聚合选项字段时,获取结果不正确的问题 ([#5214](https://github.com/nocobase/nocobase/pull/5214)) by @2013xile + +- **[数据源管理]** 修复`用户和权限`设置页面中数据源表格`rowKey`不正确问题 ([#5215](https://github.com/nocobase/nocobase/pull/5215)) by @gchust + +- **[工作流:HTTP 请求节点]** 修复请求节点参数使用非字符串变量时的问题。 ([#5204](https://github.com/nocobase/nocobase/pull/5204)) by @mytharcher + +- **[数据表字段:公式]** 修复公式字段时间类型测试用例 ([#5197](https://github.com/nocobase/nocobase/pull/5197)) by @katherinehhh + +- **[应用的备份与还原(废弃)]** 修复测试用例报错 ([#5201](https://github.com/nocobase/nocobase/pull/5201)) by @chenos + +- **[数据源:REST API]** + - rest api 数据表 标识不可编辑 by @katherinehhh + + - Rest api 多语言调整 by @katherinehhh + +## [v1.3.15-beta](https://github.com/nocobase/nocobase/compare/v1.3.14-beta...v1.3.15-beta) - 2024-09-04 + +### 🐛 修复 + +- **[工作流]** 修复工作流变量中缺少部分字段可选的问题。 ([#5187](https://github.com/nocobase/nocobase/pull/5187)) by @mytharcher + +- **[数据表字段:Markdown(Vditor)]** 修复 markdown(Vditor) 字段没有正确显数据(缓存) ([#5176](https://github.com/nocobase/nocobase/pull/5176)) by @katherinehhh + +## [v1.3.14-beta](https://github.com/nocobase/nocobase/compare/v1.3.13-beta...v1.3.14-beta) - 2024-09-03 + +### 🎉 新特性 + +- **[client]** 支持在筛选表单中配置对多关系目标表中的字段。 ([#5178](https://github.com/nocobase/nocobase/pull/5178)) by @zhangzhonghe + +### 🚀 优化 + +- **[操作:自定义请求]** 去掉添加数据表单自定义请求按钮的联动规则 ([#5179](https://github.com/nocobase/nocobase/pull/5179)) by @katherinehhh + +### 🐛 修复 + +- **[数据表字段:公式]** 公式字段使用日期字段时页面报错 ([#5168](https://github.com/nocobase/nocobase/pull/5168)) by @katherinehhh + +## [v1.3.13-beta](https://github.com/nocobase/nocobase/compare/v1.3.12-beta...v1.3.13-beta) - 2024-09-03 + +### 🐛 修复 + +- **[操作:导出记录]** 修复导出关系数据不正确的问题 ([#5170](https://github.com/nocobase/nocobase/pull/5170)) by @chareice + diff --git a/Dockerfile b/Dockerfile index 7b1bc2a9433..6272a59d248 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ ARG BEFORE_PACK_NOCOBASE="ls -l" ARG PLUGINS_DIRS ENV PLUGINS_DIRS=${PLUGINS_DIRS} - +ENV COMMIT_HASH=${COMMIT_HASH} RUN npx npm-cli-adduser --username test --password test -e test@nocobase.com -r $VERDACCIO_URL @@ -40,17 +40,18 @@ RUN cd /app \ && rm -rf nocobase.tar.gz \ && tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app . +RUN echo "${COMMIT_HASH}" > /tmp/commit_hash.txt + FROM node:20.13-bullseye-slim -RUN apt-get update && apt-get install -y nginx +RUN apt-get update && apt-get install -y nginx libaio1 RUN rm -rf /etc/nginx/sites-enabled/default COPY ./docker/nocobase/nocobase.conf /etc/nginx/sites-enabled/nocobase.conf COPY --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz +COPY --from=builder /tmp/commit_hash.txt /app/commit_hash.txt WORKDIR /app/nocobase -RUN mkdir -p /app/nocobase/storage/uploads/ && echo "$COMMIT_HASH" >> /app/nocobase/storage/uploads/COMMIT_HASH - COPY ./docker/nocobase/docker-entrypoint.sh /app/ CMD ["/app/docker-entrypoint.sh"] diff --git a/Dockerfile.pro b/Dockerfile.pro new file mode 100644 index 00000000000..cb4d0352238 --- /dev/null +++ b/Dockerfile.pro @@ -0,0 +1,65 @@ +FROM node:20.13-bullseye as builder +ARG VERDACCIO_URL=http://host.docker.internal:10104/ +ARG COMMIT_HASH +ARG APPEND_PRESET_LOCAL_PLUGINS +ARG BEFORE_PACK_NOCOBASE="ls -l" +ARG PLUGINS_DIRS + +ENV PLUGINS_DIRS=${PLUGINS_DIRS} + + +RUN npx npm-cli-adduser --username test --password test -e test@nocobase.com -r $VERDACCIO_URL + +RUN apt-get update && apt-get install -y jq +WORKDIR /tmp +COPY . /tmp +RUN yarn install && yarn build --no-dts + +RUN cd /tmp && \ + NEWVERSION="$(cat lerna.json | jq '.version' | tr -d '"').$(date +'%Y%m%d%H%M%S')" \ + && git checkout -b release-$(date +'%Y%m%d%H%M%S') \ + && yarn lerna version ${NEWVERSION} -y --no-git-tag-version +RUN git config user.email "test@mail.com" \ + && git config user.name "test" && git add . \ + && git commit -m "chore(versions): test publish packages" +RUN yarn release:force --registry $VERDACCIO_URL + +RUN yarn config set registry $VERDACCIO_URL +WORKDIR /app +RUN cd /app \ + && yarn config set network-timeout 600000 -g \ + && yarn create nocobase-app my-nocobase-app -a -e APP_ENV=production -e APPEND_PRESET_LOCAL_PLUGINS=$APPEND_PRESET_LOCAL_PLUGINS \ + && cd /app/my-nocobase-app \ + && yarn install --production + +WORKDIR /app/my-nocobase-app +RUN $BEFORE_PACK_NOCOBASE + +RUN cd /app \ + && rm -rf my-nocobase-app/packages/app/client/src/.umi \ + && rm -rf nocobase.tar.gz \ + && tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app . + +FROM node:20.13-bullseye-slim +RUN apt-get update && apt-get install -y nginx +RUN rm -rf /etc/nginx/sites-enabled/default + +COPY ./docker/nocobase/nocobase.conf /etc/nginx/sites-enabled/nocobase.conf +COPY --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz + +WORKDIR /app/nocobase + +RUN mkdir -p /app/nocobase/storage/uploads/ && echo "$COMMIT_HASH" >> /app/nocobase/storage/uploads/COMMIT_HASH + +# install postgresql-client and mysql-client +RUN apt update && apt install -y wget postgresql-common gnupg \ + && /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y \ + && apt install -y postgresql-client-16 \ + && wget https://downloads.mysql.com/archives/get/p/23/file/mysql-community-client-core_8.1.0-1debian11_amd64.deb \ + && dpkg -x mysql-community-client-core_8.1.0-1debian11_amd64.deb /tmp/mysql-client \ + && cp /tmp/mysql-client/usr/bin/mysqldump /usr/bin/ \ + && cp /tmp/mysql-client/usr/bin/mysql /usr/bin/ + +COPY ./docker/nocobase/docker-entrypoint.sh /app/ + +CMD ["/app/docker-entrypoint.sh"] diff --git a/LICENSE.txt b/LICENSE.txt index 35554d2963a..5f834534440 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Updated Date: August 15, 2024 +Updated Date: September 17, 2024 NocoBase License Agreement @@ -92,21 +92,23 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr 7.1 It is not allowed to remove or change all intellectual property statements about NocoBase in the code. -7.2 It is not allowed to sell, transfer, lease, share, or give away the Commercial License. +7.2 It is not allowed to sell, transfer, lease, share, or distribute the Commercial License. -7.3 It is not allowed to provide any form of no-code, zero-code, low-code platform SaaS products to the public using the original or modified Software. +7.3 It is not allowed to sell, transfer, lease, share, or distribute any form of no-code, zero-code, low-code platform, or developer tools developed based on Software. -7.4 It is not allowed for Users with a standard license to sell Upper Layer Application to clients without a commercial license. +7.4 It is not allowed to provide any form of no-code, zero-code, low-code platform SaaS products to the public using the original or modified Software. -7.5 It is not allowed to use reverse engineering, decompilation, and other means to try to discover the source code of Commercial Plugins that have not obtained source code license. +7.5 It is not allowed for Users with a standard license to sell Upper Layer Application to clients without a commercial license. -7.6 It is not allowed to disclose the source code of Commercial Plugins to any third party. +7.6 It is not allowed to use reverse engineering, decompilation, and other means to try to discover the source code of Commercial Plugins that have not obtained source code license. -7.7 It is not allowed to publicly sell plugins developed for Software outside of the Marketplace. +7.7 It is not allowed to disclose the source code of Commercial Plugins to any third party. -7.8 If there is a violation of the above obligations or the terms of this Agreement, the rights owned by the User will be immediately terminated, the paid fees will not be refunded, and the Company reserves the right to pursue the User's legal responsibility. +7.8 It is not allowed to publicly sell plugins developed for Software outside of the Marketplace. -7.9 If there are other agreements in the contract for the above obligations, the contract agreement shall prevail. +7.9 If there is a violation of the above obligations or the terms of this Agreement, the rights owned by the User will be immediately terminated, the paid fees will not be refunded, and the Company reserves the right to pursue the User's legal responsibility. + +7.10 If there are other agreements in the contract for the above obligations, the contract agreement shall prevail. ============================================================= 8. Legal Jurisdiction, Interpretation, and Dispute Resolution diff --git a/README.ja-JP.md b/README.ja-JP.md index ed16e72ee09..0e290fc8a3a 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -8,18 +8,20 @@ https://github.com/user-attachments/assets/b11cbb68-76bc-4e8b-a2aa-2a1feed0ab77 NocoBase - Scalability-first, open-source no-code platform | Product Hunt ## 最近の重要なリリース -- [v1.0.1-alpha.1:ブロックの高さ設定をサポート - 2024/06/07](https://docs-cn.nocobase.com/welcome/changelog/20240607) -- [v1.0.0-alpha.15:新しいプラグインの追加、「設定操作」インターフェースの改善 - 2024/05/19](https://docs-cn.nocobase.com/welcome/changelog/20240519) -- [v1.0:新しいマイルストーン - 2024/04/28](https://docs-cn.nocobase.com/welcome/release/v1001-changelog) -- [v0.21:ブロックのパフォーマンスの最適化 - 2024/03/29](https://docs-cn.nocobase.com/welcome/release/v0210-changelog) -- [v0.20:複数のデータソースをサポート - 2024/03/03](https://docs-cn.nocobase.com/welcome/release/v0200-changelog) -- [v0.19:アプリケーションフローの最適化 - 2024/01/08](https://blog-cn.nocobase.com/posts/release-v019/) -- [v0.18:完全なテストシステムの確立 - 2023/12/21](https://blog-cn.nocobase.com/posts/release-v018/) -- [v0.17:新しいSchemaInitializerおよびSchemaSettings - 2023/12/11](https://blog-cn.nocobase.com/posts/release-v017/) -- [v0.16:新しいキャッシュモジュール - 2023/11/20](https://blog-cn.nocobase.com/posts/release-v016/) -- [v0.15:新しいプラグイン設定センター - 2023/11/13](https://blog-cn.nocobase.com/posts/release-v015/) -- [v0.14:新しいプラグインマネージャー、インターフェースを通じたプラグインの追加をサポート - 2023/09/11](https://blog-cn.nocobase.com/posts/release-v014/) -- [v0.13: 新しいアプリケーションステートフロー - 2023/08/24](https://blog-cn.nocobase.com/posts/release-v013/) + +- [v1.3:REST API データソース、モバイル版 V2 などの新機能 - 2024/08/29](https://www.nocobase.com/en/blog/nocobase-1-3) +- [v1.0.1-alpha.1:ブロックの高さ設定をサポート - 2024/06/07](https://www.nocobase.com/en/blog/release-v101-alpha1) +- [v1.0.0-alpha.15:新しいプラグインの追加、「設定操作」インターフェースの改善 - 2024/05/19](https://www.nocobase.com/en/blog/release-v100-alpha15) +- [v1.0:新しいマイルストーン - 2024/04/28](https://www.nocobase.com/en/blog/release-v10) +- [v0.21:ブロックのパフォーマンスの最適化 - 2024/03/29](https://www.nocobase.com/en/blog/release-v021) +- [v0.20:複数のデータソースをサポート - 2024/03/03](https://www.nocobase.com/en/blog/release-v020) +- [v0.19:アプリケーションフローの最適化 - 2024/01/08](https://www.nocobase.com/en/blog/release-v019) +- [v0.18:完全なテストシステムの確立 - 2023/12/21](https://www.nocobase.com/en/blog/release-v018) +- [v0.17:新しいSchemaInitializerおよびSchemaSettings - 2023/12/11](https://www.nocobase.com/en/blog/release-v017) +- [v0.16:新しいキャッシュモジュール - 2023/11/20](https://www.nocobase.com/en/blog/release-v016) +- [v0.15:新しいプラグイン設定センター - 2023/11/13](https://www.nocobase.com/en/blog/release-v015) +- [v0.14:新しいプラグインマネージャー、インターフェースを通じたプラグインの追加をサポート - 2023/09/11](https://www.nocobase.com/en/blog/release-v014) +- [v0.13: 新しいアプリケーションステートフロー - 2023/08/24](https://www.nocobase.com/en/blog/release-v013) ## NocoBaseはなに? diff --git a/README.md b/README.md index 58b7bbf81b5..3937d5e6bb3 100644 --- a/README.md +++ b/README.md @@ -10,18 +10,19 @@ https://github.com/nocobase/nocobase/assets/1267426/1d6a3979-d1eb-4e50-b726-2f90 ## Recent major updates -- [v1.0.1-alpha.1: Blocks support height settings - 2024/06/07](https://docs.nocobase.com/welcome/changelog/20240607) -- [v1.0.0-alpha.15: New Plugins and Improved “Configure actions” Interaction - 2024/05/19](https://docs.nocobase.com/welcome/changelog/20240519) -- [v1.0: Significant Milestone - 2024/04/28](https://docs.nocobase.com/welcome/release/v1001-changelog) -- [v0.21: Block performance optimization - 2024/03/29](https://docs.nocobase.com/welcome/release/v0210-changelog) -- [v0.20: Support for multiple data sources - 2024/03/03](https://docs.nocobase.com/welcome/release/v0200-changelog) -- [v0.19: Application process optimization - 2024/01/08](https://docs.nocobase.com/welcome/release/v0190-changelog) -- [v0.18: Establish a sound testing system - 2023/12/21](https://docs.nocobase.com/welcome/release/v0180-changelog) -- [v0.17: New SchemaInitializer and SchemaSettings - 2023/12/11](https://docs.nocobase.com/welcome/release/v0170-changelog) -- [v0.16: New cache manager - 2023/11/20](https://docs.nocobase.com/welcome/release/v0160-changelog) -- [v0.15: New plugin settings manager - 2023/11/13](https://docs.nocobase.com/welcome/release/v0150-changelog) -- [v0.14: New plugin manager, supports adding plugins through UI - 2023/09/11](https://docs.nocobase.com/welcome/release/v0140-changelog) -- [v0.13: New application status flow - 2023/08/24](https://docs.nocobase.com/welcome/release/v0130-changelog) +- [v1.3: REST API data source, mobile v2, and more features - 2024/08/29](https://www.nocobase.com/en/blog/nocobase-1-3) +- [v1.0.1-alpha.1: Blocks support height settings - 2024/06/07](https://www.nocobase.com/en/blog/release-v101-alpha1) +- [v1.0.0-alpha.15: New Plugins and Improved “Configure actions” Interaction - 2024/05/19](https://www.nocobase.com/en/blog/release-v100-alpha15) +- [v1.0: Significant Milestone - 2024/04/28](https://www.nocobase.com/en/blog/release-v10) +- [v0.21: Block performance optimization - 2024/03/29](https://www.nocobase.com/en/blog/release-v021) +- [v0.20: Support for multiple data sources - 2024/03/03](https://www.nocobase.com/en/blog/release-v020) +- [v0.19: Application process optimization - 2024/01/08](https://www.nocobase.com/en/blog/release-v019) +- [v0.18: Establish a sound testing system - 2023/12/21](https://www.nocobase.com/en/blog/release-v018) +- [v0.17: New SchemaInitializer and SchemaSettings - 2023/12/11](https://www.nocobase.com/en/blog/release-v017) +- [v0.16: New cache manager - 2023/11/20](https://www.nocobase.com/en/blog/release-v016) +- [v0.15: New plugin settings manager - 2023/11/13](https://www.nocobase.com/en/blog/release-v015) +- [v0.14: New plugin manager, supports adding plugins through UI - 2023/09/11](https://www.nocobase.com/en/blog/release-v014) +- [v0.13: New application status flow - 2023/08/24](https://www.nocobase.com/en/blog/release-v013) ## What is NocoBase diff --git a/README.zh-CN.md b/README.zh-CN.md index 20dac826e7a..b75e53ebc4f 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -11,18 +11,19 @@ https://github.com/nocobase/nocobase/assets/1267426/29623e45-9a48-4598-bb9e-9dd1 我们正在招聘远程 **全栈开发工程师** 、 **测试工程师** 、 **技术培训与文档专家**。 欢迎对 NocoBase 有强烈兴趣的伙伴加入。[查看详情](https://www.nocobase.com/cn/recruitment) ## 最近重要更新 -- [v1.0.1-alpha.1:区块支持高度设置 - 2024/06/07](https://docs-cn.nocobase.com/welcome/changelog/20240607) -- [v1.0.0-alpha.15:新增插件、改进「配置操作」交互 - 2024/05/19](https://docs-cn.nocobase.com/welcome/changelog/20240519) -- [v1.0:新的里程碑 - 2024/04/28](https://docs-cn.nocobase.com/welcome/release/v1001-changelog) -- [v0.21:优化区块性能 - 2024/03/29](https://docs-cn.nocobase.com/welcome/release/v0210-changelog) -- [v0.20:支持多数据源 - 2024/03/03](https://docs-cn.nocobase.com/welcome/release/v0200-changelog) -- [v0.19:应用流程优化 - 2024/01/08](https://blog-cn.nocobase.com/posts/release-v019/) -- [v0.18:建立健全的测试体系 - 2023/12/21](https://blog-cn.nocobase.com/posts/release-v018/) -- [v0.17:全新的 SchemaInitializer 和 SchemaSettings - 2023/12/11](https://blog-cn.nocobase.com/posts/release-v017/) -- [v0.16:全新的缓存模块 - 2023/11/20](https://blog-cn.nocobase.com/posts/release-v016/) -- [v0.15:全新的插件设置中心 - 2023/11/13](https://blog-cn.nocobase.com/posts/release-v015/) -- [v0.14:全新的插件管理器,支持通过界面添加插件 - 2023/09/11](https://blog-cn.nocobase.com/posts/release-v014/) -- [v0.13: 全新的应用状态流转 - 2023/08/24](https://blog-cn.nocobase.com/posts/release-v013/) +- [v1.3:REST API 数据源、移动端 V2 和更多功能 - 2024/08/29](https://www.nocobase.com/cn/blog/nocobase-1-3) +- [v1.0.1-alpha.1:区块支持高度设置 - 2024/06/07](https://www.nocobase.com/cn/blog/release-v101-alpha1) +- [v1.0.0-alpha.15:新增插件、改进「配置操作」交互 - 2024/05/19](https://www.nocobase.com/cn/blog/release-v100-alpha15) +- [v1.0:新的里程碑 - 2024/04/28](https://www.nocobase.com/cn/blog/release-v10) +- [v0.21:优化区块性能 - 2024/03/29](https://www.nocobase.com/cn/blog/release-v021) +- [v0.20:支持多数据源 - 2024/03/03](https://www.nocobase.com/cn/blog/release-v020) +- [v0.19:应用流程优化 - 2024/01/08](https://www.nocobase.com/cn/blog/release-v019) +- [v0.18:建立健全的测试体系 - 2023/12/21](https://www.nocobase.com/cn/blog/release-v018) +- [v0.17:全新的 SchemaInitializer 和 SchemaSettings - 2023/12/11](https://www.nocobase.com/cn/blog/release-v017) +- [v0.16:全新的缓存模块 - 2023/11/20](https://www.nocobase.com/cn/blog/release-v016) +- [v0.15:全新的插件设置中心 - 2023/11/13](https://www.nocobase.com/cn/blog/release-v015) +- [v0.14:全新的插件管理器,支持通过界面添加插件 - 2023/09/11](https://www.nocobase.com/cn/blog/release-v014) +- [v0.13: 全新的应用状态流转 - 2023/08/24](https://www.nocobase.com/cn/blog/release-v013) ## NocoBase 是什么 diff --git a/docker/nocobase/docker-entrypoint.sh b/docker/nocobase/docker-entrypoint.sh index 62c7918c62b..7effef05c70 100755 --- a/docker/nocobase/docker-entrypoint.sh +++ b/docker/nocobase/docker-entrypoint.sh @@ -1,6 +1,8 @@ #!/bin/sh set -e +echo "COMMIT_HASH: $(cat /app/commit_hash.txt)" + if [ ! -d "/app/nocobase" ]; then mkdir nocobase fi @@ -18,6 +20,14 @@ ln -s /app/nocobase/storage/nocobase.conf /etc/nginx/sites-enabled/nocobase.conf nginx echo 'nginx started'; +# run scripts in storage/scripts +if [ -d "/app/nocobase/storage/scripts" ]; then + for f in /app/nocobase/storage/scripts/*.sh; do + echo "Running $f" + sh "$f" + done +fi + cd /app/nocobase && yarn start --quickstart # Run command with node if the first argument contains a "-" or is not a system command. The last diff --git a/generate-npmignore.sh b/generate-npmignore.sh index afaf7951029..9b608df7261 100755 --- a/generate-npmignore.sh +++ b/generate-npmignore.sh @@ -3,9 +3,14 @@ FILES=$(find packages/pro-plugins/@nocobase -name .npmignore) if [ "$1" == "ignore-src" ]; then - CONTENT="/node_modules" + CONTENT="/node_modules +/docker +/docs +" else CONTENT="/node_modules +/docker +/docs /src" fi diff --git a/packages/core/actions/src/__tests__/list-action.test.ts b/packages/core/actions/src/__tests__/list-action.test.ts index 5bd8a490954..8533cd650d4 100644 --- a/packages/core/actions/src/__tests__/list-action.test.ts +++ b/packages/core/actions/src/__tests__/list-action.test.ts @@ -146,4 +146,52 @@ describe('list action', () => { expect(response.status).toEqual(200); expect(response.body.count).toEqual(0); }); + + it('should list with simple paginate', async () => { + const Item = app.collection({ + name: 'items', + simplePaginate: true, + fields: [{ type: 'string', name: 'name' }], + }); + + await app.db.sync(); + + await Item.repository.create({ + values: [ + { + name: 'item1', + }, + { + name: 'item2', + }, + { + name: 'item3', + }, + ], + }); + + const response = await app + .agent() + .resource('items') + .list({ + fields: ['id'], + pageSize: 1, + page: 2, + }); + + const body = response.body; + expect(body.hasNext).toBeTruthy(); + + const lastPageResponse = await app + .agent() + .resource('items') + .list({ + fields: ['id'], + pageSize: 1, + page: 3, + }); + + const lastPageBody = lastPageResponse.body; + expect(lastPageBody.hasNext).toBeFalsy(); + }); }); diff --git a/packages/core/actions/src/actions/list.ts b/packages/core/actions/src/actions/list.ts index 0ad05cccec7..0fcf556b14e 100644 --- a/packages/core/actions/src/actions/list.ts +++ b/packages/core/actions/src/actions/list.ts @@ -7,7 +7,6 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { assign, isValidFilter } from '@nocobase/utils'; import { Context } from '..'; import { getRepositoryFromParams, pageArgsToLimitArgs } from '../utils'; import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '../constants'; @@ -49,9 +48,13 @@ async function listWithPagination(ctx: Context) { }); if (simplePaginate) { + options.limit = options.limit + 1; + const rows = await repository.find(options); + ctx.body = { - rows, + rows: rows.slice(0, pageSize), + hasNext: rows.length > pageSize, page: Number(page), pageSize: Number(pageSize), }; diff --git a/packages/core/app/client/.umirc.ts b/packages/core/app/client/.umirc.ts index c1cc4b7a7fa..e0706354816 100644 --- a/packages/core/app/client/.umirc.ts +++ b/packages/core/app/client/.umirc.ts @@ -1,4 +1,4 @@ -import { getUmiConfig, IndexGenerator } from '@nocobase/devtools/umiConfig'; +import { generatePlugins, getUmiConfig } from '@nocobase/devtools/umiConfig'; import path from 'path'; import { defineConfig } from 'umi'; @@ -8,17 +8,11 @@ process.env.MFSU_AD = 'none'; process.env.DID_YOU_KNOW = 'none'; const pluginPrefix = (process.env.PLUGIN_PACKAGE_PREFIX || '').split(',').filter((item) => !item.includes('preset')); // 因为现在 preset 是直接引入的,所以不能忽略,如果以后 preset 也是动态插件的形式引入,那么这里可以去掉 - -const pluginDirs = (process.env.PLUGIN_PATH || 'packages/plugins/,packages/samples/,packages/pro-plugins/') - .split(',').map(item => path.join(process.cwd(), item)); - -const outputPluginPath = path.join(__dirname, 'src', '.plugins'); -const indexGenerator = new IndexGenerator(outputPluginPath, pluginDirs); -indexGenerator.generate(); - const isDevCmd = !!process.env.IS_DEV_CMD; const appPublicPath = isDevCmd ? '/' : '{{env.APP_PUBLIC_PATH}}'; +generatePlugins(); + export default defineConfig({ title: 'Loading...', devtool: process.env.NODE_ENV === 'development' ? 'source-map' : false, diff --git a/packages/core/app/client/public/file-placeholder/ai-200-200.png b/packages/core/app/client/public/file-placeholder/ai-200-200.png new file mode 100644 index 00000000000..8cf26a9af43 Binary files /dev/null and b/packages/core/app/client/public/file-placeholder/ai-200-200.png differ diff --git a/packages/core/app/client/public/file-placeholder/audio-200-200.png b/packages/core/app/client/public/file-placeholder/audio-200-200.png new file mode 100644 index 00000000000..3b20a277233 Binary files /dev/null and b/packages/core/app/client/public/file-placeholder/audio-200-200.png differ diff --git a/packages/core/app/client/public/file-placeholder/docx-200-200.png b/packages/core/app/client/public/file-placeholder/docx-200-200.png new file mode 100644 index 00000000000..1132c959654 Binary files /dev/null and b/packages/core/app/client/public/file-placeholder/docx-200-200.png differ diff --git a/packages/core/app/client/public/file-placeholder/eps-200-200.png b/packages/core/app/client/public/file-placeholder/eps-200-200.png new file mode 100644 index 00000000000..73422d56f87 Binary files /dev/null and b/packages/core/app/client/public/file-placeholder/eps-200-200.png differ diff --git a/packages/core/app/client/public/file-placeholder/gif-200-200.png b/packages/core/app/client/public/file-placeholder/gif-200-200.png new file mode 100644 index 00000000000..750684978ee Binary files /dev/null and b/packages/core/app/client/public/file-placeholder/gif-200-200.png differ diff --git a/packages/core/app/client/public/file-placeholder/jpeg-200-200.png b/packages/core/app/client/public/file-placeholder/jpeg-200-200.png new file mode 100644 index 00000000000..2c2f1827024 Binary files /dev/null and b/packages/core/app/client/public/file-placeholder/jpeg-200-200.png differ diff --git a/packages/core/app/client/public/file-placeholder/pdf-200-200.png b/packages/core/app/client/public/file-placeholder/pdf-200-200.png new file mode 100644 index 00000000000..98459d4bf4e Binary files /dev/null and b/packages/core/app/client/public/file-placeholder/pdf-200-200.png differ diff --git a/packages/core/app/client/public/file-placeholder/png-200-200.png b/packages/core/app/client/public/file-placeholder/png-200-200.png new file mode 100644 index 00000000000..3b42889be19 Binary files /dev/null and b/packages/core/app/client/public/file-placeholder/png-200-200.png differ diff --git a/packages/core/app/client/public/file-placeholder/pptx-200-200.png b/packages/core/app/client/public/file-placeholder/pptx-200-200.png new file mode 100644 index 00000000000..80158b84276 Binary files /dev/null and b/packages/core/app/client/public/file-placeholder/pptx-200-200.png differ diff --git a/packages/core/app/client/public/file-placeholder/psd-200-200.png b/packages/core/app/client/public/file-placeholder/psd-200-200.png new file mode 100644 index 00000000000..3f53acd9913 Binary files /dev/null and b/packages/core/app/client/public/file-placeholder/psd-200-200.png differ diff --git a/packages/core/app/client/public/file-placeholder/svg-200-200.png b/packages/core/app/client/public/file-placeholder/svg-200-200.png new file mode 100644 index 00000000000..746a557ec54 Binary files /dev/null and b/packages/core/app/client/public/file-placeholder/svg-200-200.png differ diff --git a/packages/core/app/client/public/file-placeholder/unknown-200-200.png b/packages/core/app/client/public/file-placeholder/unknown-200-200.png new file mode 100644 index 00000000000..a0c6d976baf Binary files /dev/null and b/packages/core/app/client/public/file-placeholder/unknown-200-200.png differ diff --git a/packages/core/app/client/public/file-placeholder/video-200-200.png b/packages/core/app/client/public/file-placeholder/video-200-200.png new file mode 100644 index 00000000000..982a02e22a1 Binary files /dev/null and b/packages/core/app/client/public/file-placeholder/video-200-200.png differ diff --git a/packages/core/app/client/public/file-placeholder/xlsx-200-200.png b/packages/core/app/client/public/file-placeholder/xlsx-200-200.png new file mode 100644 index 00000000000..ed60bb5b8b9 Binary files /dev/null and b/packages/core/app/client/public/file-placeholder/xlsx-200-200.png differ diff --git a/packages/core/app/client/public/file-placeholder/zip-200-200.png b/packages/core/app/client/public/file-placeholder/zip-200-200.png new file mode 100644 index 00000000000..12ede7f1af1 Binary files /dev/null and b/packages/core/app/client/public/file-placeholder/zip-200-200.png differ diff --git a/packages/core/auth/src/__tests__/middleware.test.ts b/packages/core/auth/src/__tests__/middleware.test.ts index 689a868dbb0..2c58944788e 100644 --- a/packages/core/auth/src/__tests__/middleware.test.ts +++ b/packages/core/auth/src/__tests__/middleware.test.ts @@ -70,7 +70,7 @@ describe('middleware', () => { hasFn.mockImplementation(() => true); const res = await agent.resource('auth').check(); expect(res.status).toBe(401); - expect(res.text).toContain('token is not available'); + expect(res.text).toContain('Token is invalid'); }); }); }); diff --git a/packages/core/auth/src/auth-manager.ts b/packages/core/auth/src/auth-manager.ts index f7bd79d899a..f0c625a1e56 100644 --- a/packages/core/auth/src/auth-manager.ts +++ b/packages/core/auth/src/auth-manager.ts @@ -109,7 +109,10 @@ export class AuthManager { return async (ctx: Context & { auth: Auth }, next: Next) => { const token = ctx.getBearerToken(); if (token && (await ctx.app.authManager.jwt.blacklist?.has(token))) { - return ctx.throw(401, ctx.t('token is not available')); + return ctx.throw(401, { + code: 'TOKEN_INVALID', + message: ctx.t('Token is invalid'), + }); } const name = ctx.get(this.options.authKey) || this.options.default; diff --git a/packages/core/auth/src/base/auth.ts b/packages/core/auth/src/base/auth.ts index 4c8659f63c6..bab49103fe3 100644 --- a/packages/core/auth/src/base/auth.ts +++ b/packages/core/auth/src/base/auth.ts @@ -69,14 +69,14 @@ export class BaseAuth extends Auth { return null; } try { - const { userId, roleName } = await this.jwt.decode(token); + const { userId, roleName, iat, temp } = await this.jwt.decode(token); if (roleName) { this.ctx.headers['x-role'] = roleName; } const cache = this.ctx.cache as Cache; - return await cache.wrap(this.getCacheKey(userId), () => + const user = await cache.wrap(this.getCacheKey(userId), () => this.userRepository.findOne({ filter: { id: userId, @@ -84,6 +84,10 @@ export class BaseAuth extends Auth { raw: true, }), ); + if (temp && user.passwordChangeTz && iat * 1000 < user.passwordChangeTz) { + throw new Error('Token is invalid'); + } + return user; } catch (err) { this.ctx.logger.error(err, { method: 'check' }); return null; @@ -106,6 +110,7 @@ export class BaseAuth extends Auth { } const token = this.jwt.sign({ userId: user.id, + temp: true, }); return { user, @@ -119,7 +124,7 @@ export class BaseAuth extends Auth { return; } const { userId } = await this.jwt.decode(token); - await this.ctx.app.emitAsync('beforeSignOut', { userId }); + await this.ctx.app.emitAsync('cache:del:roles', { userId }); await this.ctx.cache.del(this.getCacheKey(userId)); return await this.jwt.block(token); } diff --git a/packages/core/build/package.json b/packages/core/build/package.json index 4d937611b1e..504c23010ce 100644 --- a/packages/core/build/package.json +++ b/packages/core/build/package.json @@ -11,7 +11,7 @@ "dependencies": { "@babel/core": "7.25.2", "@babel/plugin-transform-modules-amd": "7.24.7", - "@babel/preset-env": "7.25.3", + "@babel/preset-env": "7.25.4", "@hapi/topo": "^6.0.0", "@lerna/project": "4.0.0", "@types/gulp": "^4.0.13", diff --git a/packages/core/cli/package.json b/packages/core/cli/package.json index ebc7b0eb5e5..b04f5280419 100644 --- a/packages/core/cli/package.json +++ b/packages/core/cli/package.json @@ -22,7 +22,7 @@ "portfinder": "^1.0.28", "serve": "^13.0.2", "tree-kill": "^1.2.2", - "tsx": "^4.6.2" + "tsx": "^4.19.0" }, "devDependencies": { "@nocobase/devtools": "1.4.0-alpha" diff --git a/packages/core/cli/src/commands/dev.js b/packages/core/cli/src/commands/dev.js index d830f0b3cf6..8a805adb608 100644 --- a/packages/core/cli/src/commands/dev.js +++ b/packages/core/cli/src/commands/dev.js @@ -9,8 +9,12 @@ const chalk = require('chalk'); const { Command } = require('commander'); -const { runAppCommand, runInstall, run, postCheck, nodeCheck, promptForTs } = require('../util'); +const { generatePlugins, run, postCheck, nodeCheck, promptForTs } = require('../util'); const { getPortPromise } = require('portfinder'); +const chokidar = require('chokidar'); +const { uid } = require('@formily/shared'); +const path = require('path'); +const fs = require('fs'); /** * @@ -27,6 +31,25 @@ module.exports = (cli) => { .option('--inspect [port]') .allowUnknownOption() .action(async (opts) => { + const watcher = chokidar.watch('./storage/plugins/**/*', { + cwd: process.cwd(), + ignored: /(^|[\/\\])\../, // 忽略隐藏文件 + persistent: true, + depth: 1, // 只监听第一层目录 + }); + + watcher + .on('addDir', async (pathname) => { + generatePlugins(); + const file = path.resolve(process.cwd(), 'storage/app.watch.ts'); + await fs.promises.writeFile(file, `export const watchId = '${uid()}';`, 'utf-8'); + }) + .on('unlinkDir', async (pathname) => { + generatePlugins(); + const file = path.resolve(process.cwd(), 'storage/app.watch.ts'); + await fs.promises.writeFile(file, `export const watchId = '${uid()}';`, 'utf-8'); + }); + promptForTs(); const { SERVER_TSCONFIG_PATH } = process.env; process.env.IS_DEV_CMD = true; diff --git a/packages/core/cli/src/commands/global.js b/packages/core/cli/src/commands/global.js index 526c22ecfe5..524ad3cd091 100644 --- a/packages/core/cli/src/commands/global.js +++ b/packages/core/cli/src/commands/global.js @@ -8,7 +8,7 @@ */ const { Command } = require('commander'); -const { run, isDev, isProd, promptForTs } = require('../util'); +const { run, isDev, isProd, promptForTs, downloadPro } = require('../util'); /** * @@ -20,10 +20,14 @@ module.exports = (cli) => { .allowUnknownOption() .option('-h, --help') .option('--ts-node-dev') - .action((options) => { + .action(async (options) => { + const cmd = process.argv.slice(2)?.[0]; + if (cmd === 'install') { + await downloadPro(); + } if (isDev()) { promptForTs(); - run('tsx', [ + await run('tsx', [ '--tsconfig', SERVER_TSCONFIG_PATH, '-r', @@ -32,7 +36,7 @@ module.exports = (cli) => { ...process.argv.slice(2), ]); } else if (isProd()) { - run('node', [`${APP_PACKAGE_ROOT}/lib/index.js`, ...process.argv.slice(2)]); + await run('node', [`${APP_PACKAGE_ROOT}/lib/index.js`, ...process.argv.slice(2)]); } }); }; diff --git a/packages/core/cli/src/commands/index.js b/packages/core/cli/src/commands/index.js index 7f12331d9ae..45daf3c483e 100644 --- a/packages/core/cli/src/commands/index.js +++ b/packages/core/cli/src/commands/index.js @@ -31,6 +31,7 @@ module.exports = (cli) => { require('./umi')(cli); require('./upgrade')(cli); require('./postinstall')(cli); + require('./pkg')(cli); if (isPackageValid('@umijs/utils')) { require('./create-plugin')(cli); } diff --git a/packages/core/cli/src/commands/pkg.js b/packages/core/cli/src/commands/pkg.js new file mode 100644 index 00000000000..1ca981cffad --- /dev/null +++ b/packages/core/cli/src/commands/pkg.js @@ -0,0 +1,218 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +const { Command } = require('commander'); +const axios = require('axios'); +const fs = require('fs-extra'); +const zlib = require('zlib'); +const tar = require('tar'); +const path = require('path'); +const { createStoragePluginsSymlink } = require('@nocobase/utils/plugin-symlink'); +const chalk = require('chalk'); + +class Package { + data; + constructor(packageName, packageManager) { + this.packageName = packageName; + this.packageManager = packageManager; + this.outputDir = path.resolve(process.cwd(), `storage/plugins/${this.packageName}`); + } + + get token() { + return this.packageManager.getToken(); + } + + url(path) { + return this.packageManager.url(path); + } + + async mkdir() { + if (await fs.exists(this.outputDir)) { + await fs.rm(this.outputDir, { recursive: true, force: true }); + } + await fs.mkdirp(this.outputDir); + } + + async getInfo() { + try { + const res = await axios.get(this.url(this.packageName), { + headers: { + Authorization: `Bearer ${this.token}`, + }, + responseType: 'json', + }); + this.data = res.data; + } catch (error) { + return; + } + } + + getTarball(version = 'latest') { + if (this.data.versions[version]) { + return [version, this.data.versions[version].dist.tarball]; + } + + if (version.includes('beta')) { + version = version.split('beta')[0] + 'beta'; + } else if (version.includes('alpha')) { + const prefix = (version = version.split('alpha')[0]); + version = Object.keys(this.data.versions) + .filter((ver) => ver.startsWith(`${prefix}alpha`)) + .sort() + .pop(); + } + + if (version === 'latest') { + version = this.data['dist-tags']['latest']; + } else if (version === 'next') { + version = this.data['dist-tags']['next']; + } + + if (!this.data.versions[version]) { + console.log(chalk.redBright(`Download failed: ${this.packageName}@${version} package does not exist`)); + } + + return [version, this.data.versions[version].dist.tarball]; + } + + async isDevPackage() { + let file = path.resolve(process.cwd(), 'packages/plugins', this.packageName, 'package.json'); + if (await fs.exists(file)) { + return true; + } + file = path.resolve(process.cwd(), 'packages/pro-plugins', this.packageName, 'package.json'); + if (await fs.exists(file)) { + return true; + } + return false; + } + + async download(options = {}) { + if (await this.isDevPackage()) { + console.log(chalk.yellowBright(`Skipped: ${this.packageName} is dev package`)); + return; + } + await this.getInfo(); + if (!this.data) { + console.log(chalk.redBright(`Download failed: ${this.packageName} package does not exist`)); + return; + } + try { + const [version, url] = this.getTarball(options.version); + const response = await axios({ + url, + responseType: 'stream', + method: 'GET', + headers: { + Authorization: `Bearer ${this.token}`, + }, + }); + await this.mkdir(); + await new Promise((resolve, reject) => { + response.data + .pipe(zlib.createGunzip()) // 解压 gzip + .pipe(tar.extract({ cwd: this.outputDir, strip: 1 })) // 解压 tar + .on('finish', resolve) + .on('error', reject); + }); + console.log(chalk.greenBright(`Download success: ${this.packageName}@${version}`)); + } catch (error) { + console.log(chalk.redBright(`Download failed: ${this.packageName}`)); + } + } +} + +class PackageManager { + token; + baseURL; + + constructor({ baseURL }) { + this.baseURL = baseURL; + } + + getToken() { + return this.token; + } + + getBaseURL() { + return this.baseURL; + } + + url(path) { + return this.baseURL + path; + } + + async login(credentials) { + try { + const res1 = await axios.post(`${this.baseURL}-/verdaccio/sec/login`, credentials, { + responseType: 'json', + }); + this.token = res1.data.token; + } catch (error) { + console.error(chalk.redBright(`Login failed: ${this.baseURL}`)); + } + } + + getPackage(packageName) { + return new Package(packageName, this); + } + + async getProPackages() { + const res = await axios.get(this.url('pro-packages'), { + headers: { + Authorization: `Bearer ${this.token}`, + }, + responseType: 'json', + }); + return res.data.data; + } + + async getPackages() { + const pkgs = await this.getProPackages(); + return pkgs; + } + + async download(options = {}) { + const { version } = options; + if (!this.token) { + return; + } + const pkgs = await this.getPackages(); + for (const pkg of pkgs) { + await this.getPackage(pkg).download({ version }); + } + } +} + +/** + * + * @param {Command} cli + */ +module.exports = (cli) => { + const pkg = cli.command('pkg'); + pkg + .command('download-pro') + .option('-V, --version [version]') + .action(async () => { + const { NOCOBASE_PKG_URL, NOCOBASE_PKG_USERNAME, NOCOBASE_PKG_PASSWORD } = process.env; + if (!(NOCOBASE_PKG_URL && NOCOBASE_PKG_USERNAME && NOCOBASE_PKG_PASSWORD)) { + return; + } + const credentials = { username: NOCOBASE_PKG_USERNAME, password: NOCOBASE_PKG_PASSWORD }; + const pm = new PackageManager({ baseURL: NOCOBASE_PKG_URL }); + await pm.login(credentials); + const file = path.resolve(__dirname, '../../package.json'); + const json = await fs.readJson(file); + await pm.download({ version: json.version }); + await createStoragePluginsSymlink(); + }); + pkg.command('export-all').action(async () => { + console.log('Todo...'); + }); +}; diff --git a/packages/core/cli/src/commands/postinstall.js b/packages/core/cli/src/commands/postinstall.js index 539afdf8b7f..cbe7bf48e68 100644 --- a/packages/core/cli/src/commands/postinstall.js +++ b/packages/core/cli/src/commands/postinstall.js @@ -8,7 +8,7 @@ */ const { Command } = require('commander'); -const { run, isDev, isPackageValid, generatePlaywrightPath } = require('../util'); +const { run, isDev, isPackageValid, generatePlaywrightPath, generatePlugins } = require('../util'); const { dirname, resolve } = require('path'); const { existsSync, mkdirSync, readFileSync, appendFileSync } = require('fs'); const { readFile, writeFile } = require('fs').promises; @@ -41,7 +41,7 @@ module.exports = (cli) => { .option('--skip-umi') .action(async (options) => { writeToExclude(); - + generatePlugins(); generatePlaywrightPath(true); await createStoragePluginsSymlink(); if (!isDev()) { diff --git a/packages/core/cli/src/commands/start.js b/packages/core/cli/src/commands/start.js index 2437ea3dfbb..467acd19bf2 100644 --- a/packages/core/cli/src/commands/start.js +++ b/packages/core/cli/src/commands/start.js @@ -8,10 +8,11 @@ */ const { Command } = require('commander'); -const { isDev, run, postCheck, runInstall, promptForTs } = require('../util'); +const { isDev, run, postCheck, downloadPro, promptForTs } = require('../util'); const { existsSync, rmSync } = require('fs'); const { resolve } = require('path'); const chalk = require('chalk'); +const chokidar = require('chokidar'); function deleteSockFiles() { const { SOCKET_PATH, PM2_HOME } = process.env; @@ -38,6 +39,23 @@ module.exports = (cli) => { .option('--quickstart') .allowUnknownOption() .action(async (opts) => { + if (opts.quickstart) { + await downloadPro(); + } + + const watcher = chokidar.watch('./storage/plugins/**/*', { + cwd: process.cwd(), + ignoreInitial: true, + ignored: /(^|[\/\\])\../, // 忽略隐藏文件 + persistent: true, + depth: 1, // 只监听第一层目录 + }); + + watcher.on('addDir', async (pathname) => { + console.log('pathname', pathname); + await run('yarn', ['nocobase', 'pm2-restart']); + }); + if (opts.port) { process.env.APP_PORT = opts.port; } diff --git a/packages/core/cli/src/commands/upgrade.js b/packages/core/cli/src/commands/upgrade.js index a36c0ef1fec..2e0fcb20a7b 100644 --- a/packages/core/cli/src/commands/upgrade.js +++ b/packages/core/cli/src/commands/upgrade.js @@ -10,7 +10,7 @@ const chalk = require('chalk'); const { Command } = require('commander'); const { resolve } = require('path'); -const { run, promptForTs, runAppCommand, hasCorePackages, updateJsonFile, hasTsNode } = require('../util'); +const { run, promptForTs, runAppCommand, hasCorePackages, downloadPro, hasTsNode } = require('../util'); const { existsSync, rmSync } = require('fs'); /** @@ -29,15 +29,18 @@ module.exports = (cli) => { if (hasTsNode()) promptForTs(); if (hasCorePackages()) { // await run('yarn', ['install']); + await downloadPro(); await runAppCommand('upgrade'); return; } if (options.skipCodeUpdate) { + await downloadPro(); await runAppCommand('upgrade'); return; } // await runAppCommand('upgrade'); if (!hasTsNode()) { + await downloadPro(); await runAppCommand('upgrade'); return; } @@ -54,8 +57,9 @@ module.exports = (cli) => { stdio: 'pipe', }); if (pkg.version === stdout) { + await downloadPro(); await runAppCommand('upgrade'); - rmAppDir(); + await rmAppDir(); return; } const currentY = 1 * pkg.version.split('.')[1]; @@ -66,7 +70,8 @@ module.exports = (cli) => { await run('yarn', ['add', '@nocobase/cli', '@nocobase/devtools', '-W']); } await run('yarn', ['install']); + await downloadPro(); await runAppCommand('upgrade'); - rmAppDir(); + await rmAppDir(); }); }; diff --git a/packages/core/cli/src/plugin-generator.js b/packages/core/cli/src/plugin-generator.js index a1d77c28ebe..29c0c979135 100644 --- a/packages/core/cli/src/plugin-generator.js +++ b/packages/core/cli/src/plugin-generator.js @@ -72,7 +72,7 @@ class PluginGenerator extends Generator { }); this.log(''); genTsConfigPaths(); - execa.sync('yarn', ['postinstall', '--skip-umi'], { shell: true, stdio: 'inherit' }); + execa.sync('yarn', ['postinstall'], { shell: true, stdio: 'inherit' }); this.log(`The plugin folder is in ${chalk.green(`packages/plugins/${name}`)}`); } } diff --git a/packages/core/cli/src/util.js b/packages/core/cli/src/util.js index eb3acd54f85..66c834b0474 100644 --- a/packages/core/cli/src/util.js +++ b/packages/core/cli/src/util.js @@ -163,6 +163,10 @@ exports.promptForTs = () => { console.log(chalk.green('WAIT: ') + 'TypeScript compiling...'); }; +exports.downloadPro = async () => { + await exports.run('yarn', ['nocobase', 'pkg', 'download-pro']); +}; + exports.updateJsonFile = async (target, fn) => { const content = await readFile(target, 'utf-8'); const json = JSON.parse(content); @@ -416,3 +420,13 @@ exports.initEnv = function initEnv() { ); } }; + +exports.generatePlugins = function () { + try { + require.resolve('@nocobase/devtools/umiConfig'); + const { generatePlugins } = require('@nocobase/devtools/umiConfig'); + generatePlugins(); + } catch (error) { + return; + } +}; diff --git a/packages/core/client/package.json b/packages/core/client/package.json index c8f6d826e57..82f58fc9c03 100644 --- a/packages/core/client/package.json +++ b/packages/core/client/package.json @@ -11,6 +11,7 @@ "@ant-design/icons": "^5.1.4", "@ant-design/pro-layout": "^7.16.11", "@antv/g2plot": "^2.4.18", + "@budibase/handlebars-helpers": "^0.13.2", "@ctrl/tinycolor": "^3.6.0", "@dnd-kit/core": "^5.0.1", "@dnd-kit/modifiers": "^6.0.0", @@ -30,7 +31,7 @@ "@nocobase/sdk": "1.4.0-alpha", "@nocobase/utils": "1.4.0-alpha", "ahooks": "^3.7.2", - "antd": "^5.12.8", + "antd": "5.12.8", "antd-style": "3.4.5", "axios": "^0.26.1", "bignumber.js": "^9.1.2", @@ -48,6 +49,7 @@ "markdown-it-highlightjs": "3.3.1", "mathjs": "^10.6.0", "mermaid": "9.4.3", + "mime": "^4.0.4", "mime-match": "^1.0.2", "react-beautiful-dnd": "^13.1.0", "react-drag-listview": "^0.1.9", @@ -57,7 +59,7 @@ "react-i18next": "^11.15.1", "react-iframe": "~1.8.5", "react-image-lightbox": "^5.1.4", - "react-intersection-observer": "9.8.1", + "react-intersection-observer": "9.13.0", "react-js-cron": "^3.1.0", "react-quill": "^2.0.0", "react-router-dom": "^6.11.2", diff --git a/packages/core/client/src/acl/ACLProvider.tsx b/packages/core/client/src/acl/ACLProvider.tsx index e0b907a12f2..edba0767ba2 100644 --- a/packages/core/client/src/acl/ACLProvider.tsx +++ b/packages/core/client/src/acl/ACLProvider.tsx @@ -20,6 +20,7 @@ import { useResourceActionContext } from '../collection-manager/ResourceActionPr import { useDataSourceKey } from '../data-source/data-source/DataSourceProvider'; import { useRecord } from '../record-provider'; import { SchemaComponentOptions, useDesignable } from '../schema-component'; +import { CollectionNotAllowViewPlaceholder } from '../data-source'; import { useApp } from '../application'; @@ -102,6 +103,13 @@ export const useACLContext = () => { export const ACLActionParamsContext = createContext({}); ACLActionParamsContext.displayName = 'ACLActionParamsContext'; +export const ACLCustomContext = createContext({}); +ACLCustomContext.displayName = 'ACLCustomContext'; + +const useACLCustomContext = () => { + return useContext(ACLCustomContext); +}; + export const useACLRolesCheck = () => { const ctx = useContext(ACLContext); const dataSourceName = useDataSourceKey(); @@ -204,11 +212,23 @@ export function useACLRoleContext() { }; } +/** + * Used to get whether the current user has permission to configure UI + * @returns {allowConfigUI: boolean} + */ +export function useUIConfigurationPermissions(): { allowConfigUI: boolean } { + const { allowAll, snippets } = useACLRoleContext(); + return { + allowConfigUI: allowAll || snippets.includes('ui.*'), + }; +} + export const ACLCollectionProvider = (props) => { const { allowAll, parseAction } = useACLRoleContext(); + const { allowAll: customAllowAll } = useACLCustomContext(); const app = useApp(); const schema = useFieldSchema(); - if (allowAll || app.disableAcl) { + if (allowAll || app.disableAcl || customAllowAll) { return props.children; } let actionPath = schema?.['x-acl-action'] || props.actionPath; @@ -222,7 +242,7 @@ export const ACLCollectionProvider = (props) => { } const params = parseAction(actionPath, { schema }); if (!params) { - return null; + return ; } const [_, actionName] = actionPath.split(':'); params.actionName = actionName; diff --git a/packages/core/client/src/acl/Configuration/StrategyActions.tsx b/packages/core/client/src/acl/Configuration/StrategyActions.tsx index ace50dfe416..df501db6912 100644 --- a/packages/core/client/src/acl/Configuration/StrategyActions.tsx +++ b/packages/core/client/src/acl/Configuration/StrategyActions.tsx @@ -54,6 +54,7 @@ export const StrategyActions = connect((props) => { { - const errs = error?.response?.data?.errors || [{ message: 'Server error' }]; + const errs = this.toErrMessages(error); // Hard code here temporarily // TODO(yangqia): improve error code and message if (errs.find((error: { code?: string }) => error.code === 'ROLE_NOT_FOUND_ERR')) { this.auth.setRole(null); } + if (errs.find((error: { code?: string }) => error.code === 'TOKEN_INVALID')) { + this.auth.setToken(null); + } throw error; }, ); } + toErrMessages(error) { + if (typeof error?.response?.data === 'string') { + return [{ message: error?.response?.data }]; + } + return ( + error?.response?.data?.errors || + error?.response?.data?.messages || + error?.response?.error || [{ message: error.message || 'Server error' }] + ); + } + useNotificationMiddleware() { this.axios.interceptors.response.use( (response) => { @@ -119,9 +133,11 @@ export class APIClient extends APIClientSDK { } return response; }, - (error) => { + async (error) => { if (this.silence) { - throw error; + console.error(error); + return; + // throw error; } const redirectTo = error?.response?.data?.redirectTo; if (redirectTo) { @@ -143,7 +159,7 @@ export class APIClient extends APIClientSDK { } else if (this.app.error) { this.app.error = null; } - let errs = error?.response?.data?.errors || error?.response?.data?.messages || [{ message: 'Server error' }]; + let errs = this.toErrMessages(error); errs = errs.filter((error) => { const lastTime = errorCache.get(error.message); if (lastTime && new Date().getTime() - lastTime < 500) { diff --git a/packages/core/client/src/application/Application.tsx b/packages/core/client/src/application/Application.tsx index 6b92e390f9d..d190ee577a5 100644 --- a/packages/core/client/src/application/Application.tsx +++ b/packages/core/client/src/application/Application.tsx @@ -34,17 +34,17 @@ import { compose, normalizeContainer } from './utils'; import { defineGlobalDeps } from './utils/globalDeps'; import { getRequireJs } from './utils/requirejs'; +import { CollectionFieldInterfaceComponentOption } from '../data-source/collection-field-interface/CollectionFieldInterface'; import { CollectionField } from '../data-source/collection-field/CollectionField'; import { DataSourceApplicationProvider } from '../data-source/components/DataSourceApplicationProvider'; import { DataBlockProvider } from '../data-source/data-block/DataBlockProvider'; import { DataSourceManager, type DataSourceManagerOptions } from '../data-source/data-source/DataSourceManager'; -import { CollectionFieldInterfaceComponentOption } from '../data-source/collection-field-interface/CollectionFieldInterface'; +import type { CollectionFieldInterfaceFactory } from '../data-source'; import { OpenModeProvider } from '../modules/popup/OpenModeProvider'; import { AppSchemaComponentProvider } from './AppSchemaComponentProvider'; import type { Plugin } from './Plugin'; import type { RequireJS } from './utils/requirejs'; -import type { CollectionFieldInterfaceFactory } from '../data-source'; declare global { interface Window { @@ -282,10 +282,21 @@ export class Application { }); } loadFailed = true; - const others = error?.response?.data?.error || error?.response?.data?.errors?.[0] || { message: error?.message }; + const toError = (error) => { + if (typeof error?.response?.data === 'string') { + return { message: error?.response?.data }; + } + if (error?.response?.data?.error) { + return error?.response?.data?.error; + } + if (error?.response?.data?.errors?.[0]) { + return error?.response?.data?.errors?.[0]; + } + return { message: error?.message }; + }; this.error = { code: 'LOAD_ERROR', - ...others, + ...toError(error), }; console.error(error, this.error); } diff --git a/packages/core/client/src/application/__tests__/utils/remotePlugins.test.ts b/packages/core/client/src/application/__tests__/utils/remotePlugins.test.ts index 5466d6ce025..d629b45f96f 100644 --- a/packages/core/client/src/application/__tests__/utils/remotePlugins.test.ts +++ b/packages/core/client/src/application/__tests__/utils/remotePlugins.test.ts @@ -267,7 +267,7 @@ describe('remotePlugins', () => { expect(requirejs.requirejs.config).toBeCalledTimes(0); }); - it('If there is devDynamicImport and devDynamicImport returns partial, remote API will be requested', async () => { + it.skip('If there is devDynamicImport and devDynamicImport returns partial, remote API will be requested', async () => { const remoteFn = vi.fn(); const mockPluginsModules = (pluginData, resolve) => { remoteFn(); diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSubMenu.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSubMenu.tsx index 76faaf2db6f..fbd32324b51 100644 --- a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSubMenu.tsx +++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSubMenu.tsx @@ -48,7 +48,11 @@ export const SchemaInitializerMenu: FC = (props) => { const { items, ...others } = props; const { token } = theme.useToken(); const itemsWithPopupClass = useMemo( - () => items.map((item) => ({ ...item, popupClassName: `${hashId} ${componentCls}-menu-sub` })), + () => + items.map((item) => ({ + ...item, + popupClassName: `${hashId} ${componentCls}-menu-sub`, + })), [componentCls, hashId, items], ); // selectedKeys 为了不让有选中效果 @@ -62,9 +66,23 @@ export const SchemaInitializerMenu: FC = (props) => { border-inline-end: 0 !important; .ant-menu-sub { max-height: 50vh !important; + padding: ${token.paddingXXS}px !important; } .ant-menu-item { - margin-block: 0; + margin-inline: ${token.marginXXS}px !important; + margin-block: 0 !important; + width: auto !important; + padding: 0 ${token.paddingSM}px 0 ${token.padding}px !important; + } + .ant-menu-item-group-title { + padding: 0 ${token.padding}px; + margin-inline: 0; + line-height: 32px; + } + .ant-menu-submenu-title { + margin: 0 ${token.marginXXS}px !important; + padding-left: ${token.padding}px !important; + width: auto !important; } .ant-menu-root { margin: 0 -${token.margin}px; diff --git a/packages/core/client/src/application/schema-initializer/components/style.ts b/packages/core/client/src/application/schema-initializer/components/style.ts index 9d8e20811f5..deb6ade3d62 100644 --- a/packages/core/client/src/application/schema-initializer/components/style.ts +++ b/packages/core/client/src/application/schema-initializer/components/style.ts @@ -51,11 +51,6 @@ export const useSchemaInitializerStyles = genStyleHook('nb-schema-initializer', }, }, }, - [`${componentCls}-menu-sub`]: { - ul: { - maxHeight: '50vh !important', - }, - }, [`${componentCls}-item-content`]: { marginLeft: token.marginXS, }, diff --git a/packages/core/client/src/application/schema-initializer/hooks/useSchemaInitializerRender.tsx b/packages/core/client/src/application/schema-initializer/hooks/useSchemaInitializerRender.tsx index 968aa3f01d7..ea5bf8af728 100644 --- a/packages/core/client/src/application/schema-initializer/hooks/useSchemaInitializerRender.tsx +++ b/packages/core/client/src/application/schema-initializer/hooks/useSchemaInitializerRender.tsx @@ -12,10 +12,10 @@ import React, { FC, useMemo } from 'react'; import { useApp } from '../../hooks'; import { SchemaInitializerItems } from '../components'; import { SchemaInitializerButton } from '../components/SchemaInitializerButton'; -import { withInitializer } from '../withInitializer'; -import { SchemaInitializerOptions } from '../types'; import { SchemaInitializer } from '../SchemaInitializer'; - +import { SchemaInitializerOptions } from '../types'; +import { withInitializer } from '../withInitializer'; +import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider'; const InitializerComponent: FC> = React.memo((options) => { const Component: any = options.Component || SchemaInitializerButton; @@ -38,6 +38,18 @@ export function useSchemaInitializerRender( options?: Omit, 'name'>, ) { const app = useApp(); + const { isMobile } = useOpenModeContext() || {}; + + // compatible with mobile + // TODO: delete this code + if ( + name === 'popup:common:addBlock' && + app.schemaInitializerManager.has('mobile:popup:common:addBlock') && + isMobile + ) { + name = 'mobile:popup:common:addBlock'; + } + const initializer = useMemo( () => (typeof name === 'object' ? name : app.schemaInitializerManager.get(name)), [app.schemaInitializerManager, name], diff --git a/packages/core/client/src/application/utils/remotePlugins.ts b/packages/core/client/src/application/utils/remotePlugins.ts index 593162a7375..704224dbcfc 100644 --- a/packages/core/client/src/application/utils/remotePlugins.ts +++ b/packages/core/client/src/application/utils/remotePlugins.ts @@ -7,10 +7,10 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import type { DevDynamicImport } from '../Application'; import type { Plugin } from '../Plugin'; import type { PluginData } from '../PluginManager'; import type { RequireJS } from './requirejs'; -import type { DevDynamicImport } from '../Application'; /** * @internal @@ -131,7 +131,10 @@ export async function getPlugins(options: GetPluginsOption): Promise { const association = getCollectionField(assoc); return recordData?.[association.targetKey || 'id']; } - return recordData?.[collection.filterTargetKey || 'id']; + if (isArray(collection.filterTargetKey)) { + const filterByTk = {}; + for (const key of collection.filterTargetKey) { + filterByTk[key] = recordData?.[key]; + } + return filterByTk; + } else { + return recordData?.[collection.filterTargetKey || 'id']; + } }; /** diff --git a/packages/core/client/src/block-provider/DetailsBlockProvider.tsx b/packages/core/client/src/block-provider/DetailsBlockProvider.tsx index 42450701b04..cb050435cb7 100644 --- a/packages/core/client/src/block-provider/DetailsBlockProvider.tsx +++ b/packages/core/client/src/block-provider/DetailsBlockProvider.tsx @@ -141,7 +141,8 @@ export const useDetailsBlockProps = () => { ctx.form .reset() .then(() => { - ctx.form.setValues(data || {}); + ctx.form.setInitialValues(data || {}); + // Using `ctx.form.setValues(data || {});` here may cause an internal infinite loop in Formily }) .catch(console.error); } diff --git a/packages/core/client/src/block-provider/FormBlockProvider.tsx b/packages/core/client/src/block-provider/FormBlockProvider.tsx index 6ec74f7ccb8..d2abb742760 100644 --- a/packages/core/client/src/block-provider/FormBlockProvider.tsx +++ b/packages/core/client/src/block-provider/FormBlockProvider.tsx @@ -87,7 +87,7 @@ const InternalFormBlockProvider = (props) => { updateAssociationValues, ]); - if (service.loading && Object.keys(form?.initialValues)?.length === 0 && action) { + if (service.loading && Object.keys(form?.initialValues || {})?.length === 0 && action) { return ; } @@ -151,9 +151,10 @@ export const useFormBlockContext = () => { /** * @internal */ -export const useFormBlockProps = (shouldClearInitialValues = false) => { +export const useFormBlockProps = () => { const ctx = useFormBlockContext(); const treeParentRecord = useTreeParentRecord(); + useEffect(() => { if (treeParentRecord) { ctx.form?.query('parent').take((field) => { @@ -164,19 +165,14 @@ export const useFormBlockProps = (shouldClearInitialValues = false) => { }); useEffect(() => { - if (!ctx?.service?.loading) { - const form: Form = ctx.form; - if (form) { - // form 字段中可能一开始就存在一些默认值(比如设置了字段默认值的模板区块)。在编辑表单中, - // 这些默认值是不需要的,需要清除掉,不然会导致一些问题。比如:https://github.com/nocobase/nocobase/issues/4868 - if (shouldClearInitialValues === true) { - form.initialValues = {}; - form.reset(); - } - form.setInitialValues(ctx.service?.data?.data); - } + const form: Form = ctx.form; + + if (!form || ctx.service?.loading) { + return; } - }, [ctx?.service?.loading]); + + form.setInitialValues(ctx.service?.data?.data); + }, [ctx.form, ctx.service?.data?.data, ctx.service?.loading]); return { form: ctx.form, }; diff --git a/packages/core/client/src/block-provider/TableBlockProvider.tsx b/packages/core/client/src/block-provider/TableBlockProvider.tsx index 0dc55069633..6e7154aacd9 100644 --- a/packages/core/client/src/block-provider/TableBlockProvider.tsx +++ b/packages/core/client/src/block-provider/TableBlockProvider.tsx @@ -12,7 +12,7 @@ import { FormContext, useField, useFieldSchema } from '@formily/react'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { useCollectionManager_deprecated } from '../collection-manager'; import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps'; -import { useTableBlockParams } from '../modules/blocks/data-blocks/table'; +import { useTableBlockParams } from '../modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps'; import { FixedBlockWrapper, SchemaComponentOptions } from '../schema-component'; import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { useBlockHeightProps } from './hooks'; diff --git a/packages/core/client/src/block-provider/TableFieldProvider.tsx b/packages/core/client/src/block-provider/TableFieldProvider.tsx index 2fab38ae735..6fa1dcce1d5 100644 --- a/packages/core/client/src/block-provider/TableFieldProvider.tsx +++ b/packages/core/client/src/block-provider/TableFieldProvider.tsx @@ -183,9 +183,10 @@ export const useTableFieldProps = () => { rowKey: (record: any) => { return field.value?.indexOf?.(record); }, - onRowSelectionChange(selectedRowKeys) { + onRowSelectionChange(selectedRowKeys, selectedRowData) { ctx.field.data = ctx?.field?.data || {}; ctx.field.data.selectedRowKeys = selectedRowKeys; + ctx.field.data.selectedRowData = selectedRowData; }, onChange({ current, pageSize }) { ctx.service.run({ page: current, pageSize }); diff --git a/packages/core/client/src/block-provider/TableSelectorProvider.tsx b/packages/core/client/src/block-provider/TableSelectorProvider.tsx index 3ddcc01d4a0..9c8e9b72e1d 100644 --- a/packages/core/client/src/block-provider/TableSelectorProvider.tsx +++ b/packages/core/client/src/block-provider/TableSelectorProvider.tsx @@ -319,9 +319,10 @@ export const useTableSelectorProps = () => { dragSort: false, rowKey: ctx.rowKey || 'id', pagination: fieldSchema?.['x-component-props']?.pagination === false ? false : field.componentProps.pagination, - onRowSelectionChange(selectedRowKeys, selectedRows) { + onRowSelectionChange(selectedRowKeys, selectedRowData) { ctx.field.data = ctx?.field?.data || {}; ctx.field.data.selectedRowKeys = selectedRowKeys; + ctx.field.data.selectedRowData = selectedRowData; }, async onRowDragEnd({ from, to }) { await ctx.resource.move({ diff --git a/packages/core/client/src/block-provider/__tests__/hooks/index.test.ts b/packages/core/client/src/block-provider/__tests__/hooks/index.test.ts index 7500de347d3..fda48baea60 100644 --- a/packages/core/client/src/block-provider/__tests__/hooks/index.test.ts +++ b/packages/core/client/src/block-provider/__tests__/hooks/index.test.ts @@ -24,7 +24,7 @@ describe('parseVariablesAndChangeParamsToQueryString', () => { { name: 'param3', value: 'value3' }, ]; const variables: any = { - parseVariable: vi.fn().mockResolvedValue('parsedValue'), + parseVariable: vi.fn().mockResolvedValue({ value: 'parsedValue' }), }; const localVariables: any = [ { name: '$var1', ctx: { value: 'localValue1' } }, diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index e66d10fcb0b..cc15e0fe2ec 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -28,6 +28,7 @@ import { useCollectionRecord, useDataSourceHeaders, useFormActiveFields, + useParsedFilter, useRouterBasename, useTableBlockContext, } from '../..'; @@ -41,7 +42,7 @@ import { useTreeParentRecord } from '../../modules/blocks/data-blocks/table/Tree import { useRecord } from '../../record-provider'; import { removeNullCondition, useActionContext, useCompile } from '../../schema-component'; import { isSubMode } from '../../schema-component/antd/association-field/util'; -import { replaceVariables } from '../../schema-component/antd/form-v2/utils'; +import { replaceVariables } from '../../schema-settings/LinkageRules/bindLinkageRulesToFiled'; import { useCurrentUserContext } from '../../user'; import { useLocalVariables, useVariables } from '../../variables'; import { VariableOption, VariablesContextType } from '../../variables/types'; @@ -163,9 +164,9 @@ export function useCollectValuesToSubmit() { } if (isVariable(value)) { - const result = await variables?.parseVariable(value, localVariables); - if (result) { - assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField }); + const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {}; + if (parsedValue) { + assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField }); } } else if (value != null && value !== '') { assignedValues[key] = value; @@ -223,7 +224,7 @@ export const useCreateActionProps = () => { return { async onClick() { const { onSuccess, skipValidator, triggerWorkflows } = actionSchema?.['x-action-settings'] ?? {}; - + const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {}; if (!skipValidator) { await form.submit(); } @@ -241,39 +242,49 @@ export const useCreateActionProps = () => { : undefined, updateAssociationValues, }); - setVisible?.(false); + if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) { + setVisible?.(false); + } setSubmitted?.(true); setFormValueChanged?.(false); actionField.data.loading = false; actionField.data.data = data; // __parent?.service?.refresh?.(); - if (!onSuccess?.successMessage) { + if (!successMessage) { message.success(t('Saved successfully')); await resetFormCorrectly(form); + if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) { + if (isURL(redirectTo)) { + window.location.href = redirectTo; + } else { + navigate(redirectTo); + } + } + return; } - if (onSuccess?.manualClose) { + if (manualClose) { modal.success({ - title: compile(onSuccess?.successMessage), + title: compile(successMessage), onOk: async () => { await resetFormCorrectly(form); - if (onSuccess?.redirecting && onSuccess?.redirectTo) { - if (isURL(onSuccess.redirectTo)) { - window.location.href = onSuccess.redirectTo; + if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) { + if (isURL(redirectTo)) { + window.location.href = redirectTo; } else { - navigate(onSuccess.redirectTo); + navigate(redirectTo); } } }, }); } else { - message.success(compile(onSuccess?.successMessage)); + message.success(compile(successMessage)); await resetFormCorrectly(form); - if (onSuccess?.redirecting && onSuccess?.redirectTo) { - if (isURL(onSuccess.redirectTo)) { - window.location.href = onSuccess.redirectTo; + if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) { + if (isURL(redirectTo)) { + window.location.href = redirectTo; } else { - navigate(onSuccess.redirectTo); + navigate(redirectTo); } } } @@ -311,7 +322,7 @@ export const useAssociationCreateActionProps = () => { triggerWorkflows, } = actionSchema?.['x-action-settings'] ?? {}; const addChild = fieldSchema?.['x-component-props']?.addChild; - + const { successMessage } = onSuccess || {}; const assignedValues = {}; const waitList = Object.keys(originalAssignedValues).map(async (key) => { const value = originalAssignedValues[key]; @@ -324,9 +335,9 @@ export const useAssociationCreateActionProps = () => { } if (isVariable(value)) { - const result = await variables?.parseVariable(value, localVariables); - if (result) { - assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField }); + const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {}; + if (parsedValue) { + assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField }); } } else if (value != null && value !== '') { assignedValues[key] = value; @@ -371,10 +382,10 @@ export const useAssociationCreateActionProps = () => { __parent?.service?.refresh?.(); setVisible?.(false); setSubmitted?.(true); - if (!onSuccess?.successMessage) { + if (!successMessage) { return; } - message.success(compile(onSuccess?.successMessage)); + message.success(compile(successMessage)); } catch (error) { actionField.data.data = null; actionField.data.loading = false; @@ -560,6 +571,7 @@ export const useCustomizeUpdateActionProps = () => { const variables = useVariables(); const localVariables = useLocalVariables({ currentForm: form }); const { name, getField } = useCollection_deprecated(); + const { setVisible } = useActionContext(); return { async onClick(e?, callBack?) { @@ -569,7 +581,7 @@ export const useCustomizeUpdateActionProps = () => { skipValidator, triggerWorkflows, } = actionSchema?.['x-action-settings'] ?? {}; - + const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {}; const assignedValues = {}; const waitList = Object.keys(originalAssignedValues).map(async (key) => { const value = originalAssignedValues[key]; @@ -582,9 +594,9 @@ export const useCustomizeUpdateActionProps = () => { } if (isVariable(value)) { - const result = await variables?.parseVariable(value, localVariables); - if (result) { - assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField }); + const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {}; + if (parsedValue) { + assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField }); } } else if (value != null && value !== '') { assignedValues[key] = value; @@ -603,6 +615,9 @@ export const useCustomizeUpdateActionProps = () => { ? triggerWorkflows.map((row) => [row.workflowKey, row.context].filter(Boolean).join('!')).join(',') : undefined, }); + if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) { + setVisible?.(false); + } // service?.refresh?.(); if (callBack) { callBack?.(); @@ -610,29 +625,29 @@ export const useCustomizeUpdateActionProps = () => { if (!(resource instanceof TableFieldResource)) { __parent?.service?.refresh?.(); } - if (!onSuccess?.successMessage) { + if (!successMessage) { return; } - if (onSuccess?.manualClose) { + if (manualClose) { modal.success({ - title: compile(onSuccess?.successMessage), + title: compile(successMessage), onOk: async () => { - if (onSuccess?.redirecting && onSuccess?.redirectTo) { - if (isURL(onSuccess.redirectTo)) { - window.location.href = onSuccess.redirectTo; + if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) { + if (isURL(redirectTo)) { + window.location.href = redirectTo; } else { - navigate(onSuccess.redirectTo); + navigate(redirectTo); } } }, }); } else { - message.success(compile(onSuccess?.successMessage)); - if (onSuccess?.redirecting && onSuccess?.redirectTo) { - if (isURL(onSuccess.redirectTo)) { - window.location.href = onSuccess.redirectTo; + message.success(compile(successMessage)); + if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) { + if (isURL(redirectTo)) { + window.location.href = redirectTo; } else { - navigate(onSuccess.redirectTo); + navigate(redirectTo); } } } @@ -657,6 +672,7 @@ export const useCustomizeBulkUpdateActionProps = () => { const record = useRecord(); const { name, getField } = useCollection_deprecated(); const localVariables = useLocalVariables(); + const { setVisible } = useActionContext(); return { async onClick() { @@ -665,6 +681,7 @@ export const useCustomizeBulkUpdateActionProps = () => { onSuccess, updateMode, } = actionSchema?.['x-action-settings'] ?? {}; + const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {}; actionField.data = field.data || {}; actionField.data.loading = true; @@ -680,16 +697,18 @@ export const useCustomizeBulkUpdateActionProps = () => { } if (isVariable(value)) { - const result = await variables?.parseVariable(value, localVariables); - if (result) { - assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField }); + const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {}; + if (parsedValue) { + assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField }); } } else if (value != null && value !== '') { assignedValues[key] = value; } }); await Promise.all(waitList); - + if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) { + setVisible?.(false); + } modal.confirm({ title: t('Bulk update'), content: updateMode === 'selected' ? t('Update selected data?') : t('Update all data?'), @@ -722,29 +741,29 @@ export const useCustomizeBulkUpdateActionProps = () => { if (!(resource instanceof TableFieldResource)) { __parent?.service?.refresh?.(); } - if (!onSuccess?.successMessage) { + if (!successMessage) { return; } - if (onSuccess?.manualClose) { + if (manualClose) { modal.success({ - title: compile(onSuccess?.successMessage), + title: compile(successMessage), onOk: async () => { - if (onSuccess?.redirecting && onSuccess?.redirectTo) { - if (isURL(onSuccess.redirectTo)) { - window.location.href = onSuccess.redirectTo; + if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) { + if (isURL(redirectTo)) { + window.location.href = redirectTo; } else { - navigate(onSuccess.redirectTo); + navigate(redirectTo); } } }, }); } else { - message.success(compile(onSuccess?.successMessage)); - if (onSuccess?.redirecting && onSuccess?.redirectTo) { - if (isURL(onSuccess.redirectTo)) { - window.location.href = onSuccess.redirectTo; + message.success(compile(successMessage)); + if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) { + if (isURL(redirectTo)) { + window.location.href = redirectTo; } else { - navigate(onSuccess.redirectTo); + navigate(redirectTo); } } } @@ -770,6 +789,7 @@ export const useCustomizeRequestActionProps = () => { const currentUserContext = useCurrentUserContext(); const currentUser = currentUserContext?.data?.data; const actionField = useField(); + const { t } = useTranslation(); const { setVisible } = useActionContext(); const { modal } = App.useApp(); const { getActiveFieldsName } = useFormActiveFields() || {}; @@ -777,6 +797,7 @@ export const useCustomizeRequestActionProps = () => { return { async onClick() { const { skipValidator, onSuccess, requestSettings } = actionSchema?.['x-action-settings'] ?? {}; + const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {}; const xAction = actionSchema?.['x-action']; if (!requestSettings['url']) { return; @@ -821,26 +842,36 @@ export const useCustomizeRequestActionProps = () => { } service?.refresh?.(); if (xAction === 'customize:form:request') { - setVisible?.(false); + if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) { + setVisible?.(false); + } } - if (!onSuccess?.successMessage) { - return; + if (!successMessage) { + message.success(t('Saved successfully')); + await resetFormCorrectly(form); + if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) { + if (isURL(redirectTo)) { + window.location.href = redirectTo; + } else { + navigate(redirectTo); + } + } } - if (onSuccess?.manualClose) { + if (manualClose) { modal.success({ - title: compile(onSuccess?.successMessage), + title: compile(successMessage), onOk: async () => { - if (onSuccess?.redirecting && onSuccess?.redirectTo) { - if (isURL(onSuccess.redirectTo)) { - window.location.href = onSuccess.redirectTo; + if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) { + if (isURL(redirectTo)) { + window.location.href = redirectTo; } else { - navigate(onSuccess.redirectTo); + navigate(redirectTo); } } }, }); } else { - message.success(compile(onSuccess?.successMessage)); + message.success(compile(successMessage)); } } finally { actionField.data.loading = false; @@ -853,7 +884,7 @@ export const useUpdateActionProps = () => { const form = useForm(); const filterByTk = useFilterByTk(); const { field, resource, __parent } = useBlockRequestContext(); - const { setVisible, setSubmitted, setFormValueChanged } = useActionContext(); + const { setVisible, setFormValueChanged } = useActionContext(); const actionSchema = useFieldSchema(); const navigate = useNavigateNoUpdate(); const { fields, getField, name } = useCollection_deprecated(); @@ -867,7 +898,7 @@ export const useUpdateActionProps = () => { const { getActiveFieldsName } = useFormActiveFields() || {}; return { - async onClick() { + async onClick(e?, callBack?) { const { assignedValues: originalAssignedValues = {}, onSuccess, @@ -875,7 +906,7 @@ export const useUpdateActionProps = () => { skipValidator, triggerWorkflows, } = actionSchema?.['x-action-settings'] ?? {}; - + const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {}; const assignedValues = {}; const waitList = Object.keys(originalAssignedValues).map(async (key) => { const value = originalAssignedValues[key]; @@ -888,9 +919,9 @@ export const useUpdateActionProps = () => { } if (isVariable(value)) { - const result = await variables?.parseVariable(value, localVariables); - if (result) { - assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField }); + const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {}; + if (parsedValue) { + assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField }); } } else if (value != null && value !== '') { assignedValues[key] = value; @@ -930,33 +961,42 @@ export const useUpdateActionProps = () => { }); actionField.data.loading = false; // __parent?.service?.refresh?.(); - setVisible?.(false); - setSubmitted?.(true); + if (callBack) { + callBack?.(); + } + if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) { + setVisible?.(false); + } setFormValueChanged?.(false); - if (!onSuccess?.successMessage) { + if (!successMessage) { return; } - if (onSuccess?.manualClose) { + if (manualClose) { modal.success({ - title: compile(onSuccess?.successMessage), + title: compile(successMessage), onOk: async () => { await form.reset(); - if (onSuccess?.redirecting && onSuccess?.redirectTo) { - if (isURL(onSuccess.redirectTo)) { - window.location.href = onSuccess.redirectTo; + if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) { + if (isURL(redirectTo)) { + window.location.href = redirectTo; } else { - navigate(onSuccess.redirectTo); + navigate(redirectTo); } } }, }); } else { - message.success(compile(onSuccess?.successMessage)); - if (onSuccess?.redirecting && onSuccess?.redirectTo) { - if (isURL(onSuccess.redirectTo)) { - window.location.href = onSuccess.redirectTo; + message.success(compile(successMessage)); + if ( + ((redirecting && !actionAfterSuccess) || + actionAfterSuccess === 'redirect' || + actionAfterSuccess === 'redirect') && + redirectTo + ) { + if (isURL(redirectTo)) { + window.location.href = redirectTo; } else { - navigate(onSuccess.redirectTo); + navigate(redirectTo); } } } @@ -1077,13 +1117,25 @@ export const useBulkDestroyActionProps = () => { const { field } = useBlockRequestContext(); const { resource, service } = useBlockRequestContext(); const { setSubmitted } = useActionContext(); + const collection = useCollection_deprecated(); + const { filterTargetKey } = collection; return { async onClick(e?, callBack?) { + let filterByTk = field.data?.selectedRowKeys; + if (Array.isArray(filterTargetKey)) { + filterByTk = field.data.selectedRowData.map((v) => { + const obj = {}; + filterTargetKey.map((j) => { + obj[j] = v[j]; + }); + return obj; + }); + } if (!field?.data?.selectedRowKeys?.length) { return; } await resource.destroy({ - filterByTk: field.data?.selectedRowKeys, + filterByTk, }); field.data.selectedRowKeys = []; const currentPage = service.params[0]?.page; @@ -1095,7 +1147,7 @@ export const useBulkDestroyActionProps = () => { callBack?.(); } setSubmitted?.(true); - // service?.refresh?.(); + service?.refresh?.(); }, }; }; @@ -1245,6 +1297,7 @@ export const useAssociationFilterBlockProps = () => { const { props: blockProps } = useBlockRequestContext(); const headers = useDataSourceHeaders(blockProps?.dataSource); const cm = useCollectionManager_deprecated(); + const { filter, parseVariableLoading } = useParsedFilter({ filterOption: field.componentProps?.params?.filter }); let list, handleSearchInput, params, run, data, valueKey, labelKey, filterKey; @@ -1264,6 +1317,7 @@ export const useAssociationFilterBlockProps = () => { pageSize: 200, page: 1, ...field.componentProps?.params, + filter, }, }, { @@ -1275,12 +1329,64 @@ export const useAssociationFilterBlockProps = () => { useEffect(() => { // 由于选项字段不需要触发当前请求,所以请求单独在关系字段的时候触发 - if (!isOptionalField(collectionField)) { + if (!isOptionalField(collectionField) && parseVariableLoading === false) { run(); } // do not format the dependencies - }, [collectionField, labelKey, run, valueKey, field.componentProps?.params, field.componentProps?.params?.sort]); + }, [ + collectionField, + labelKey, + run, + valueKey, + field.componentProps?.params, + field.componentProps?.params?.sort, + parseVariableLoading, + ]); + + const onSelected = useCallback( + (value) => { + const { targets, uid } = findFilterTargets(fieldSchema); + + getDataBlocks().forEach((block) => { + const target = targets.find((target) => target.uid === block.uid); + if (!target) return; + + const key = `${uid}${fieldSchema.name}`; + const param = block.service.params?.[0] || {}; + + if (!block.service.params?.[1]?.filters) { + _.set(block.service.params, '[1].filters', {}); + } + + // 保留原有的 filter + const storedFilter = block.service.params[1].filters; + + if (value.length) { + storedFilter[key] = { + [filterKey]: value, + }; + } else { + if (block.dataLoadingMode === 'manual') { + return block.clearData(); + } + delete storedFilter[key]; + } + + const mergedFilter = mergeFilter([...Object.values(storedFilter), block.defaultFilter]); + + return block.doFilter( + { + ...param, + page: 1, + filter: mergedFilter, + }, + { filters: storedFilter }, + ); + }); + }, + [fieldSchema, filterKey, getDataBlocks], + ); if (!collectionField) { return {}; @@ -1323,41 +1429,6 @@ export const useAssociationFilterBlockProps = () => { }; } - const onSelected = (value) => { - const { targets, uid } = findFilterTargets(fieldSchema); - - getDataBlocks().forEach((block) => { - const target = targets.find((target) => target.uid === block.uid); - if (!target) return; - - const key = `${uid}${fieldSchema.name}`; - const param = block.service.params?.[0] || {}; - // 保留原有的 filter - const storedFilter = block.service.params?.[1]?.filters || {}; - if (value.length) { - storedFilter[key] = { - [filterKey]: value, - }; - } else { - if (block.dataLoadingMode === 'manual') { - return block.clearData(); - } - delete storedFilter[key]; - } - - const mergedFilter = mergeFilter([...Object.values(storedFilter), block.defaultFilter]); - - return block.doFilter( - { - ...param, - page: 1, - filter: mergedFilter, - }, - { filters: storedFilter }, - ); - }); - }; - return { /** 渲染 Collapse 的列表数据 */ list, @@ -1438,7 +1509,8 @@ export const useAssociationNames = (dataSource?: string) => { ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'belongsToArray'].includes(collectionField.type); // 根据联动规则中条件的字段获取一些 appends - if (s['x-linkage-rules']) { + // 需要排除掉子表格和子表单中的联动规则 + if (s['x-linkage-rules'] && !isSubMode(s)) { const collectAppends = (obj) => { const type = Object.keys(obj)[0] || '$and'; const list = obj[type]; @@ -1661,8 +1733,8 @@ export async function parseVariablesAndChangeParamsToQueryString({ searchParams.map(async ({ name, value }) => { if (typeof value === 'string') { if (isVariable(value)) { - const result = await variables.parseVariable(value, localVariables); - return { name, value: result }; + const { value: parsedValue } = (await variables.parseVariable(value, localVariables)) || {}; + return { name, value: parsedValue }; } const result = await replaceVariableValue(value, variables, localVariables); return { name, value: result }; diff --git a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx index 622e5d830b2..0fb0ad29652 100644 --- a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx @@ -16,64 +16,23 @@ import { cloneDeep } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useRequest } from '../../api-client'; +import { CollectionFieldInterface } from '../../data-source'; import { RecordProvider, useRecord } from '../../record-provider'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; import { useCancelAction } from '../action-hooks'; import { useCollectionManager_deprecated } from '../hooks'; import useDialect from '../hooks/useDialect'; -import { IField } from '../interfaces/types'; import * as components from './components'; import { useFieldInterfaceOptions } from './interfaces'; -const getSchema = (schema: IField, record: any, compile) => { +const getSchema = (schema: CollectionFieldInterface, record: any, compile) => { if (!schema) { return; } - const properties = cloneDeep(schema.properties) as any; + const properties = schema.getConfigureFormProperties(); - if (schema.hasDefaultValue === true) { - properties['defaultValue'] = cloneDeep(schema?.default?.uiSchema); - properties.defaultValue.required = false; - properties['defaultValue']['title'] = compile('{{ t("Default value") }}'); - properties['defaultValue']['x-decorator'] = 'FormItem'; - properties['defaultValue']['x-reactions'] = [ - { - dependencies: [ - 'uiSchema.x-component-props.gmt', - 'uiSchema.x-component-props.showTime', - 'uiSchema.x-component-props.dateFormat', - 'uiSchema.x-component-props.timeFormat', - ], - fulfill: { - state: { - componentProps: { - gmt: '{{$deps[0]}}', - showTime: '{{$deps[1]}}', - dateFormat: '{{$deps[2]}}', - timeFormat: '{{$deps[3]}}', - }, - }, - }, - }, - { - dependencies: ['primaryKey', 'unique', 'autoIncrement'], - when: '{{$deps[0]||$deps[1]||$deps[2]}}', - fulfill: { - state: { - hidden: true, - value: null, - }, - }, - otherwise: { - state: { - hidden: false, - }, - }, - }, - ]; - } const initialValue: any = { name: `f_${uid()}`, ...cloneDeep(schema.default), diff --git a/packages/core/client/src/collection-manager/Configuration/EditCollectionAction.tsx b/packages/core/client/src/collection-manager/Configuration/EditCollectionAction.tsx index 6bf545b4d80..dc314f823eb 100644 --- a/packages/core/client/src/collection-manager/Configuration/EditCollectionAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/EditCollectionAction.tsx @@ -64,6 +64,9 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`, 'x-decorator': 'FormItem', 'x-component': 'Select', + 'x-component-props': { + multiple: true, + }, 'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'], }, footer: { diff --git a/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx index db509e36b63..3ca1486ac10 100644 --- a/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx @@ -16,6 +16,7 @@ import set from 'lodash/set'; import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAPIClient, useRequest } from '../../api-client'; +import { CollectionFieldInterface } from '../../data-source'; import { useCollectionParentRecordData } from '../../data-source/collection-record/CollectionRecordProvider'; import { RecordProvider, useRecord } from '../../record-provider'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; @@ -23,60 +24,23 @@ import { useResourceActionContext, useResourceContext } from '../ResourceActionP import { useCancelAction } from '../action-hooks'; import { useCollectionManager_deprecated } from '../hooks'; import useDialect from '../hooks/useDialect'; -import { IField } from '../interfaces/types'; import * as components from './components'; -const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => { +const getSchema = ( + schema: CollectionFieldInterface, + defaultValues: any, + record: any, + compile, + getContainer, +): ISchema => { if (!schema) { return; } - const properties = cloneDeep(schema.properties) as any; + const properties = schema.getConfigureFormProperties(); if (properties?.name) { properties.name['x-disabled'] = true; } - if (schema.hasDefaultValue === true) { - properties['defaultValue'] = cloneDeep(schema.default.uiSchema) || {}; - properties.defaultValue.required = false; - properties['defaultValue']['title'] = compile('{{ t("Default value") }}'); - properties['defaultValue']['x-decorator'] = 'FormItem'; - properties['defaultValue']['x-reactions'] = [ - { - dependencies: [ - 'uiSchema.x-component-props.gmt', - 'uiSchema.x-component-props.showTime', - 'uiSchema.x-component-props.dateFormat', - 'uiSchema.x-component-props.timeFormat', - ], - fulfill: { - state: { - componentProps: { - gmt: '{{$deps[0]}}', - showTime: '{{$deps[1]}}', - dateFormat: '{{$deps[2]}}', - timeFormat: '{{$deps[3]}}', - }, - }, - }, - }, - { - dependencies: ['primaryKey', 'unique', 'autoIncrement'], - when: '{{$deps[0]||$deps[1]||$deps[2]}}', - fulfill: { - state: { - hidden: true, - value: undefined, - }, - }, - otherwise: { - state: { - hidden: false, - }, - }, - }, - ]; - } - return { type: 'object', properties: { @@ -92,7 +56,7 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema return useRequest( () => Promise.resolve({ - data: cloneDeep(omit(schema.default, ['uiSchema.rawTitle'])), + data: cloneDeep(omit(defaultValues, ['uiSchema.rawTitle'])), }), options, ); @@ -230,15 +194,7 @@ export const EditFieldAction = (props) => { set(defaultValues.reverseField, 'name', `f_${uid()}`); set(defaultValues.reverseField, 'uiSchema.title', record.__parent?.title); } - const schema = getSchema( - { - ...interfaceConf, - default: defaultValues, - }, - record, - compile, - getContainer, - ); + const schema = getSchema(interfaceConf, defaultValues, record, compile, getContainer); setSchema(schema); setVisible(true); }} diff --git a/packages/core/client/src/collection-manager/ResourceActionProvider.tsx b/packages/core/client/src/collection-manager/ResourceActionProvider.tsx index eabab5b4a11..77cee09cb51 100644 --- a/packages/core/client/src/collection-manager/ResourceActionProvider.tsx +++ b/packages/core/client/src/collection-manager/ResourceActionProvider.tsx @@ -136,6 +136,6 @@ export const useResourceContext = () => { resource, collection, association, - targetKey: association?.targetKey || collection?.targetKey || 'id', + targetKey: association?.targetKey || collection?.filterTargetKey || collection?.targetKey || 'id', }; }; diff --git a/packages/core/client/src/collection-manager/collectionPlugin.ts b/packages/core/client/src/collection-manager/collectionPlugin.ts index ae5d0140a74..eedf07ced5c 100644 --- a/packages/core/client/src/collection-manager/collectionPlugin.ts +++ b/packages/core/client/src/collection-manager/collectionPlugin.ts @@ -51,6 +51,8 @@ import { UUIDFieldInterface, NanoidFieldInterface, UnixTimestampFieldInterface, + DateFieldInterface, + DatetimeNoTzFieldInterface, } from './interfaces'; import { GeneralCollectionTemplate, @@ -171,6 +173,8 @@ export class CollectionPlugin extends Plugin { UUIDFieldInterface, NanoidFieldInterface, UnixTimestampFieldInterface, + DateFieldInterface, + DatetimeNoTzFieldInterface, ]); } diff --git a/packages/core/client/src/collection-manager/interfaces/createdAt.ts b/packages/core/client/src/collection-manager/interfaces/createdAt.ts index 97c0a6da241..c6c18713a80 100644 --- a/packages/core/client/src/collection-manager/interfaces/createdAt.ts +++ b/packages/core/client/src/collection-manager/interfaces/createdAt.ts @@ -28,7 +28,7 @@ export class CreatedAtFieldInterface extends CollectionFieldInterface { 'x-read-pretty': true, }, }; - availableTypes = ['date']; + availableTypes = []; properties = { ...defaultProps, ...dateTimeProps, diff --git a/packages/core/client/src/collection-manager/interfaces/dateOnly.ts b/packages/core/client/src/collection-manager/interfaces/dateOnly.ts new file mode 100644 index 00000000000..7bc57cbb604 --- /dev/null +++ b/packages/core/client/src/collection-manager/interfaces/dateOnly.ts @@ -0,0 +1,46 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; +import { dateTimeProps, defaultProps, operators } from './properties'; + +export class DateFieldInterface extends CollectionFieldInterface { + name = 'date'; + type = 'object'; + group = 'datetime'; + order = 3; + title = '{{t("DateOnly")}}'; + sortable = true; + default = { + type: 'dateOnly', + uiSchema: { + type: 'string', + 'x-component': 'DatePicker', + 'x-component-props': { + dateOnly: true, + }, + }, + }; + availableTypes = ['dateOnly']; + hasDefaultValue = true; + properties = { + ...defaultProps, + ...dateTimeProps, + 'uiSchema.x-component-props.showTime': { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-visible': false, + }, + }; + filterable = { + operators: operators.datetime, + }; + titleUsable = true; +} diff --git a/packages/core/client/src/collection-manager/interfaces/datetime.ts b/packages/core/client/src/collection-manager/interfaces/datetime.ts index 615d11e9eab..68fabfb78c1 100644 --- a/packages/core/client/src/collection-manager/interfaces/datetime.ts +++ b/packages/core/client/src/collection-manager/interfaces/datetime.ts @@ -15,23 +15,39 @@ export class DatetimeFieldInterface extends CollectionFieldInterface { type = 'object'; group = 'datetime'; order = 1; - title = '{{t("Datetime")}}'; + title = '{{t("Datetime (with time zone)")}}'; sortable = true; default = { type: 'date', + defaultToCurrentTime: false, + onUpdateToCurrentTime: false, + timezone: true, uiSchema: { type: 'string', 'x-component': 'DatePicker', 'x-component-props': { showTime: false, + utc: true, }, }, }; - availableTypes = ['date', 'dateOnly', 'string']; + availableTypes = ['date', 'string', 'datetime', 'datetimeTz']; hasDefaultValue = true; properties = { ...defaultProps, ...dateTimeProps, + defaultToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Default value to current time")}}', + }, + onUpdateToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Automatically update timestamp on update")}}', + }, 'uiSchema.x-component-props.gmt': { type: 'boolean', title: '{{t("GMT")}}', diff --git a/packages/core/client/src/collection-manager/interfaces/datetimeNoTz.ts b/packages/core/client/src/collection-manager/interfaces/datetimeNoTz.ts new file mode 100644 index 00000000000..6f56b10bddb --- /dev/null +++ b/packages/core/client/src/collection-manager/interfaces/datetimeNoTz.ts @@ -0,0 +1,65 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; +import { dateTimeProps, defaultProps, operators } from './properties'; + +export class DatetimeNoTzFieldInterface extends CollectionFieldInterface { + name = 'datetimeNoTz'; + type = 'object'; + group = 'datetime'; + order = 2; + title = '{{t("Datetime (without time zone)")}}'; + sortable = true; + default = { + type: 'datetimeNoTz', + defaultToCurrentTime: false, + onUpdateToCurrentTime: false, + timezone: false, + uiSchema: { + type: 'string', + 'x-component': 'DatePicker', + 'x-component-props': { + showTime: false, + utc: false, + }, + }, + }; + availableTypes = ['string', 'datetimeNoTz']; + hasDefaultValue = true; + properties = { + ...defaultProps, + ...dateTimeProps, + defaultToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Default value to current server time")}}', + }, + onUpdateToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Automatically update timestamp to the current server time on update")}}', + }, + 'uiSchema.x-component-props.gmt': { + type: 'boolean', + title: '{{t("GMT")}}', + 'x-hidden': true, + 'x-component': 'Checkbox', + 'x-content': '{{t("Use the same time zone (GMT) for all users")}}', + 'x-decorator': 'FormItem', + default: false, + }, + }; + filterable = { + operators: operators.datetime, + }; + titleUsable = true; +} diff --git a/packages/core/client/src/collection-manager/interfaces/index.ts b/packages/core/client/src/collection-manager/interfaces/index.ts index 7d45ec1b200..c453aa4b0e2 100644 --- a/packages/core/client/src/collection-manager/interfaces/index.ts +++ b/packages/core/client/src/collection-manager/interfaces/index.ts @@ -45,3 +45,5 @@ export * from './url'; export * from './uuid'; export * from './nanoid'; export * from './unixTimestamp'; +export * from './dateOnly'; +export * from './datetimeNoTz'; diff --git a/packages/core/client/src/collection-manager/interfaces/m2m.tsx b/packages/core/client/src/collection-manager/interfaces/m2m.tsx index cd00ad9045a..bbec128358d 100644 --- a/packages/core/client/src/collection-manager/interfaces/m2m.tsx +++ b/packages/core/client/src/collection-manager/interfaces/m2m.tsx @@ -10,7 +10,7 @@ import { ISchema } from '@formily/react'; import { uid } from '@formily/shared'; import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; -import { getUniqueKeyFromCollection } from './o2m'; +import { getUniqueKeyFromCollection } from './utils'; import { defaultProps, relationshipType, reverseFieldProperties } from './properties'; export class M2MFieldInterface extends CollectionFieldInterface { diff --git a/packages/core/client/src/collection-manager/interfaces/m2o.tsx b/packages/core/client/src/collection-manager/interfaces/m2o.tsx index b6ad7c82671..8cf58654c7d 100644 --- a/packages/core/client/src/collection-manager/interfaces/m2o.tsx +++ b/packages/core/client/src/collection-manager/interfaces/m2o.tsx @@ -9,7 +9,7 @@ import { ISchema } from '@formily/react'; import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; -import { getUniqueKeyFromCollection } from './o2m'; +import { getUniqueKeyFromCollection } from './utils'; import { constraintsProps, relationshipType, reverseFieldProperties } from './properties'; export class M2OFieldInterface extends CollectionFieldInterface { diff --git a/packages/core/client/src/collection-manager/interfaces/o2m.tsx b/packages/core/client/src/collection-manager/interfaces/o2m.tsx index 2b55318b5bf..bfd664a03d3 100644 --- a/packages/core/client/src/collection-manager/interfaces/o2m.tsx +++ b/packages/core/client/src/collection-manager/interfaces/o2m.tsx @@ -8,10 +8,9 @@ */ import { ISchema } from '@formily/react'; -import { Collection } from '../../data-source'; import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; import { constraintsProps, relationshipType, reverseFieldProperties } from './properties'; - +import { getUniqueKeyFromCollection } from './utils'; export class O2MFieldInterface extends CollectionFieldInterface { name = 'o2m'; type = 'object'; @@ -215,7 +214,3 @@ export class O2MFieldInterface extends CollectionFieldInterface { ], }; } - -export function getUniqueKeyFromCollection(collection: Collection) { - return collection?.filterTargetKey || collection?.getPrimaryKey() || 'id'; -} diff --git a/packages/core/client/src/collection-manager/interfaces/o2o.tsx b/packages/core/client/src/collection-manager/interfaces/o2o.tsx index f47de65b75f..16f497ed17f 100644 --- a/packages/core/client/src/collection-manager/interfaces/o2o.tsx +++ b/packages/core/client/src/collection-manager/interfaces/o2o.tsx @@ -9,7 +9,7 @@ import { ISchema } from '@formily/react'; import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; -import { getUniqueKeyFromCollection } from './o2m'; +import { getUniqueKeyFromCollection } from './utils'; import { constraintsProps, relationshipType, reverseFieldProperties } from './properties'; export class O2OFieldInterface extends CollectionFieldInterface { diff --git a/packages/core/client/src/collection-manager/interfaces/properties/index.ts b/packages/core/client/src/collection-manager/interfaces/properties/index.ts index 1ef36e4b61b..82d08c2ad9d 100644 --- a/packages/core/client/src/collection-manager/interfaces/properties/index.ts +++ b/packages/core/client/src/collection-manager/interfaces/properties/index.ts @@ -10,6 +10,8 @@ import { Field } from '@formily/core'; import { ISchema } from '@formily/react'; import { uid } from '@formily/shared'; +import { css } from '@emotion/css'; +import { DateFormatCom } from '../../../schema-component/antd/expiresRadio'; export * as operators from './operators'; export const type: ISchema = { @@ -225,26 +227,88 @@ export const reverseFieldProperties: Record = { }; export const dateTimeProps: { [key: string]: ISchema } = { + 'uiSchema.x-component-props.picker': { + type: 'string', + title: '{{t("Picker")}}', + 'x-decorator': 'FormItem', + 'x-component': 'Radio.Group', + default: 'date', + enum: [ + { + label: '{{t("Date")}}', + value: 'date', + }, + // { + // label: '{{t("Week")}}', + // value: 'week', + // }, + { + label: '{{t("Month")}}', + value: 'month', + }, + { + label: '{{t("Quarter")}}', + value: 'quarter', + }, + { + label: '{{t("Year")}}', + value: 'year', + }, + ], + }, + 'uiSchema.x-component-props.dateFormat': { type: 'string', title: '{{t("Date format")}}', - 'x-component': 'Radio.Group', 'x-decorator': 'FormItem', + 'x-component': 'ExpiresRadio', + 'x-decorator-props': {}, + 'x-component-props': { + className: css` + .ant-radio-wrapper { + display: flex; + margin: 5px 0px; + } + `, + defaultValue: 'dddd', + formats: ['MMMM Do YYYY', 'YYYY-MM-DD', 'MM/DD/YY', 'YYYY/MM/DD', 'DD/MM/YYYY'], + }, default: 'YYYY-MM-DD', enum: [ { - label: '{{t("Year/Month/Day")}}', - value: 'YYYY/MM/DD', + label: DateFormatCom({ format: 'MMMM Do YYYY' }), + value: 'MMMM Do YYYY', }, { - label: '{{t("Year-Month-Day")}}', + label: DateFormatCom({ format: 'YYYY-MM-DD' }), value: 'YYYY-MM-DD', }, { - label: '{{t("Day/Month/Year")}}', + label: DateFormatCom({ format: 'MM/DD/YY' }), + value: 'MM/DD/YY', + }, + { + label: DateFormatCom({ format: 'YYYY/MM/DD' }), + value: 'YYYY/MM/DD', + }, + { + label: DateFormatCom({ format: 'DD/MM/YYYY' }), value: 'DD/MM/YYYY', }, + { + label: 'custom', + value: 'custom', + }, ], + 'x-reactions': { + dependencies: ['uiSchema.x-component-props.picker'], + fulfill: { + state: { + value: `{{ getPickerFormat($deps[0])}}`, + componentProps: { picker: `{{$deps[0]}}` }, + }, + }, + }, }, 'uiSchema.x-component-props.showTime': { type: 'boolean', @@ -253,10 +317,26 @@ export const dateTimeProps: { [key: string]: ISchema } = { 'x-content': '{{t("Show time")}}', 'x-reactions': [ `{{(field) => { - field.query('..[].timeFormat').take(f => { - f.display = field.value ? 'visible' : 'none'; - }); - }}}`, + field.query('..[].timeFormat').take(f => { + f.display = field.value ? 'visible' : 'none'; + f.value='HH:mm:ss' + }); + }}}`, + { + dependencies: ['uiSchema.x-component-props.picker'], + when: '{{$deps[0]==="date"}}', + fulfill: { + state: { + hidden: false, + }, + }, + otherwise: { + state: { + hidden: true, + value: false, + }, + }, + }, ], }, 'uiSchema.x-component-props.timeFormat': { @@ -275,6 +355,14 @@ export const dateTimeProps: { [key: string]: ISchema } = { value: 'HH:mm:ss', }, ], + 'x-reactions': { + dependencies: ['uiSchema.x-component-props.showTime'], + fulfill: { + state: { + hidden: `{{ !$deps[0] }}`, + }, + }, + }, }, }; diff --git a/packages/core/client/src/collection-manager/interfaces/properties/operators.ts b/packages/core/client/src/collection-manager/interfaces/properties/operators.ts index 5e3530d845f..a79128316bc 100644 --- a/packages/core/client/src/collection-manager/interfaces/properties/operators.ts +++ b/packages/core/client/src/collection-manager/interfaces/properties/operators.ts @@ -60,13 +60,48 @@ export const object = [ ]; export const datetime = [ - { label: "{{ t('is') }}", value: '$dateOn', selected: true }, - { label: "{{ t('is not') }}", value: '$dateNotOn' }, - { label: "{{ t('is before') }}", value: '$dateBefore' }, - { label: "{{ t('is after') }}", value: '$dateAfter' }, - { label: "{{ t('is on or after') }}", value: '$dateNotBefore' }, - { label: "{{ t('is on or before') }}", value: '$dateNotAfter' }, - { label: "{{ t('is between') }}", value: '$dateBetween', schema: { 'x-component': 'DatePicker.RangePicker' } }, + { + label: "{{ t('is') }}", + value: '$dateOn', + selected: true, + schema: { 'x-component': 'DatePicker.FilterWithPicker' }, + onlyFilterAction: true, //schema 仅在Filter.Action生效,筛选表单中不生效 + }, + { + label: "{{ t('is not') }}", + value: '$dateNotOn', + schema: { 'x-component': 'DatePicker.FilterWithPicker' }, + onlyFilterAction: true, + }, + { + label: "{{ t('is before') }}", + value: '$dateBefore', + schema: { 'x-component': 'DatePicker.FilterWithPicker' }, + onlyFilterAction: true, + }, + { + label: "{{ t('is after') }}", + value: '$dateAfter', + schema: { 'x-component': 'DatePicker.FilterWithPicker' }, + onlyFilterAction: true, + }, + { + label: "{{ t('is on or after') }}", + value: '$dateNotBefore', + schema: { 'x-component': 'DatePicker.FilterWithPicker' }, + onlyFilterAction: true, + }, + { + label: "{{ t('is on or before') }}", + value: '$dateNotAfter', + schema: { 'x-component': 'DatePicker.FilterWithPicker' }, + onlyFilterAction: true, + }, + { + label: "{{ t('is between') }}", + value: '$dateBetween', + schema: { 'x-component': 'DatePicker.RangePicker' }, + }, { label: "{{ t('is empty') }}", value: '$empty', noValue: true }, { label: "{{ t('is not empty') }}", value: '$notEmpty', noValue: true }, ]; @@ -157,18 +192,18 @@ export const collection = [ label: '{{t("is")}}', value: '$eq', selected: true, - schema: { 'x-component': 'CollectionSelect' }, + schema: { 'x-component': 'DataSourceCollectionCascader' }, }, { label: '{{t("is not")}}', value: '$ne', - schema: { 'x-component': 'CollectionSelect' }, + schema: { 'x-component': 'DataSourceCollectionCascader' }, }, { label: '{{t("is any of")}}', value: '$in', schema: { - 'x-component': 'CollectionSelect', + 'x-component': 'DataSourceCollectionCascader', 'x-component-props': { mode: 'tags' }, }, }, @@ -176,7 +211,7 @@ export const collection = [ label: '{{t("is none of")}}', value: '$notIn', schema: { - 'x-component': 'CollectionSelect', + 'x-component': 'DataSourceCollectionCascader', 'x-component-props': { mode: 'tags' }, }, }, diff --git a/packages/core/client/src/collection-manager/interfaces/time.ts b/packages/core/client/src/collection-manager/interfaces/time.ts index dc47c417468..2432a10bcb4 100644 --- a/packages/core/client/src/collection-manager/interfaces/time.ts +++ b/packages/core/client/src/collection-manager/interfaces/time.ts @@ -14,7 +14,7 @@ export class TimeFieldInterface extends CollectionFieldInterface { name = 'time'; type = 'object'; group = 'datetime'; - order = 2; + order = 4; title = '{{t("Time")}}'; sortable = true; default = { diff --git a/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx b/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx index 47b3ebc2cf2..8166174c487 100644 --- a/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx +++ b/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx @@ -8,31 +8,34 @@ */ import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; -import { dateTimeProps, defaultProps, operators } from './properties'; - +import { defaultProps, operators, dateTimeProps } from './properties'; export class UnixTimestampFieldInterface extends CollectionFieldInterface { name = 'unixTimestamp'; type = 'object'; group = 'datetime'; - order = 1; + order = 4; title = '{{t("Unix Timestamp")}}'; sortable = true; default = { - type: 'bigInt', + type: 'unixTimestamp', + accuracy: 'second', + timezone: true, + defaultToCurrentTime: false, + onUpdateToCurrentTime: false, uiSchema: { type: 'number', 'x-component': 'UnixTimestamp', 'x-component-props': { - accuracy: 'second', showTime: true, }, }, }; - availableTypes = ['integer', 'bigInt']; - hasDefaultValue = true; + availableTypes = ['unixTimestamp']; + hasDefaultValue = false; properties = { ...defaultProps, - 'uiSchema.x-component-props.accuracy': { + ...dateTimeProps, + accuracy: { type: 'string', title: '{{t("Accuracy")}}', 'x-component': 'Radio.Group', @@ -43,9 +46,21 @@ export class UnixTimestampFieldInterface extends CollectionFieldInterface { { value: 'second', label: '{{t("Second")}}' }, ], }, + defaultToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Default value to current time")}}', + }, + onUpdateToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Automatically update timestamp on update")}}', + }, }; filterable = { - operators: operators.number, + operators: operators.datetime, }; titleUsable = true; } diff --git a/packages/core/client/src/collection-manager/interfaces/updatedAt.ts b/packages/core/client/src/collection-manager/interfaces/updatedAt.ts index ad969338e7f..533c008d7fd 100644 --- a/packages/core/client/src/collection-manager/interfaces/updatedAt.ts +++ b/packages/core/client/src/collection-manager/interfaces/updatedAt.ts @@ -28,7 +28,7 @@ export class UpdatedAtFieldInterface extends CollectionFieldInterface { 'x-read-pretty': true, }, }; - availableTypes = ['date']; + availableTypes = []; properties = { ...defaultProps, ...dateTimeProps, diff --git a/packages/core/client/src/collection-manager/interfaces/utils/index.ts b/packages/core/client/src/collection-manager/interfaces/utils/index.ts new file mode 100644 index 00000000000..3984230cc47 --- /dev/null +++ b/packages/core/client/src/collection-manager/interfaces/utils/index.ts @@ -0,0 +1,20 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Collection } from '../../../data-source'; + +export function getUniqueKeyFromCollection(collection: Collection) { + if (collection?.filterTargetKey) { + if (Array.isArray(collection.filterTargetKey)) { + return collection?.filterTargetKey?.[0]; + } + return collection?.filterTargetKey; + } + return collection?.getPrimaryKey() || 'id'; +} diff --git a/packages/core/client/src/collection-manager/interfaces/uuid.ts b/packages/core/client/src/collection-manager/interfaces/uuid.ts index 2b15120eee7..ee233de5c37 100644 --- a/packages/core/client/src/collection-manager/interfaces/uuid.ts +++ b/packages/core/client/src/collection-manager/interfaces/uuid.ts @@ -26,7 +26,7 @@ export class UUIDFieldInterface extends CollectionFieldInterface { 'x-validator': 'uuid', }, }; - availableTypes = ['string', 'uid', 'uuid']; + availableTypes = ['string', 'uuid']; properties = { 'uiSchema.title': { type: 'string', diff --git a/packages/core/client/src/collection-manager/templates/sql.tsx b/packages/core/client/src/collection-manager/templates/sql.tsx index 0ca28e4727a..405a20547ea 100644 --- a/packages/core/client/src/collection-manager/templates/sql.tsx +++ b/packages/core/client/src/collection-manager/templates/sql.tsx @@ -90,6 +90,9 @@ export class SqlCollectionTemplate extends CollectionTemplate { description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`, 'x-decorator': 'FormItem', 'x-component': 'Select', + 'x-component-props': { + multiple: true, + }, 'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'], }, }; diff --git a/packages/core/client/src/collection-manager/templates/view.tsx b/packages/core/client/src/collection-manager/templates/view.tsx index 82f2d6ac9b3..197fbda053a 100644 --- a/packages/core/client/src/collection-manager/templates/view.tsx +++ b/packages/core/client/src/collection-manager/templates/view.tsx @@ -153,6 +153,9 @@ export class ViewCollectionTemplate extends CollectionTemplate { description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`, 'x-decorator': 'FormItem', 'x-component': 'Select', + 'x-component-props': { + multiple: true, + }, 'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'], }, ...getConfigurableProperties('category', 'description'), diff --git a/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts index c063a2a577a..befd3def02e 100644 --- a/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts +++ b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts @@ -8,9 +8,10 @@ */ import type { ISchema } from '@formily/react'; +import { cloneDeep } from 'lodash'; import type { CollectionFieldOptions } from '../collection'; import { CollectionFieldInterfaceManager } from './CollectionFieldInterfaceManager'; - +import { defaultProps } from '../../collection-manager/interfaces/properties'; export type CollectionFieldInterfaceFactory = new ( collectionFieldInterfaceManager: CollectionFieldInterfaceManager, ) => CollectionFieldInterface; @@ -42,6 +43,7 @@ export abstract class CollectionFieldInterface { componentOptions?: CollectionFieldInterfaceComponentOption[]; isAssociation?: boolean; operators?: any[]; + properties?: any; /** * - 如果该值为空,则在 Filter 组件中该字段会被过滤掉 * - 如果该值为空,则不会在变量列表中看到该字段 @@ -82,4 +84,76 @@ export abstract class CollectionFieldInterface { } this.componentOptions.push(componentOption); } + getConfigureFormProperties() { + const defaultValueProps = this.hasDefaultValue ? this.getDefaultValueProperty() : {}; + return { + ...cloneDeep({ ...defaultProps, ...this?.properties }), + ...defaultValueProps, + }; + } + getDefaultValueProperty() { + return { + defaultValue: { + ...cloneDeep(this?.default?.uiSchema), + ...this?.properties?.uiSchema, + required: false, + title: '{{ t("Default value") }}', + 'x-decorator': 'FormItem', + 'x-reactions': [ + { + dependencies: [ + 'uiSchema.x-component-props.gmt', + 'uiSchema.x-component-props.showTime', + 'uiSchema.x-component-props.dateFormat', + 'uiSchema.x-component-props.timeFormat', + 'uiSchema.x-component-props.picker', + ], + fulfill: { + state: { + componentProps: { + gmt: '{{$deps[0]}}', + showTime: '{{$deps[1]}}', + dateFormat: '{{$deps[2]}}', + timeFormat: '{{$deps[3]}}', + picker: '{{$deps[4]}}', + }, + }, + }, + }, + { + // 当 picker 改变时,清空 defaultValue + dependencies: ['uiSchema.x-component-props.picker'], + fulfill: { + state: { + value: null, + }, + }, + }, + { + dependencies: ['primaryKey', 'unique', 'autoIncrement', 'defaultToCurrentTime'], + when: '{{$deps[0]||$deps[1]||$deps[2]||$deps[3]}}', + fulfill: { + state: { + hidden: true, + value: null, + }, + }, + otherwise: { + state: { + hidden: false, + }, + }, + }, + { + dependencies: ['uiSchema.enum'], + fulfill: { + state: { + dataSource: '{{$deps[0]}}', + }, + }, + }, + ], + }, + }; + } } diff --git a/packages/core/client/src/data-source/collection-record/CollectionRecord.ts b/packages/core/client/src/data-source/collection-record/CollectionRecord.ts index 2f564b41481..aba9229f7f4 100644 --- a/packages/core/client/src/data-source/collection-record/CollectionRecord.ts +++ b/packages/core/client/src/data-source/collection-record/CollectionRecord.ts @@ -13,7 +13,7 @@ export interface CollectionRecordOptions { parentRecord?: CollectionRecord; } -export class CollectionRecord { +export class CollectionRecord { public data?: DataType; public parentRecord?: CollectionRecord; public isNew?: boolean; diff --git a/packages/core/client/src/data-source/collection-record/isNewRecord.ts b/packages/core/client/src/data-source/collection-record/isNewRecord.ts index cda652d907f..90b10922bae 100644 --- a/packages/core/client/src/data-source/collection-record/isNewRecord.ts +++ b/packages/core/client/src/data-source/collection-record/isNewRecord.ts @@ -7,10 +7,10 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { untracked } from '@formily/reactive'; import { isObject } from 'lodash'; -// 使用 Symbol 作为 key,避免与其他属性冲突,同时也可以防止通过表单提交到后端 -const key = Symbol('isNewRecord'); +const key = '__isNewRecord__'; /** * 判断一个记录对象是否是新记录,可通过 markRecordAsNew 标记 @@ -18,7 +18,7 @@ const key = Symbol('isNewRecord'); * @returns */ export const isNewRecord = (record: object) => { - return !!record?.[key]; + return untracked(() => !!record?.[key]); }; /** diff --git a/packages/core/client/src/data-source/collection/CollectionManager.ts b/packages/core/client/src/data-source/collection/CollectionManager.ts index 0d9265973f3..6f8bda79666 100644 --- a/packages/core/client/src/data-source/collection/CollectionManager.ts +++ b/packages/core/client/src/data-source/collection/CollectionManager.ts @@ -8,6 +8,7 @@ */ import type { SchemaKey } from '@formily/json-schema'; +import qs from 'qs'; import type { DataSource } from '../data-source'; import type { CollectionFieldOptions, CollectionOptions, GetCollectionFieldPredicate } from './Collection'; @@ -159,10 +160,23 @@ export class CollectionManager { ); return; } + const getTargetKey = (collection: Collection) => collection.filterTargetKey || collection.getPrimaryKey() || 'id'; + + const buildFilterByTk = (targetKey: string | string[], record: Record) => { + if (Array.isArray(targetKey)) { + const filterByTk = {}; + targetKey.forEach((key) => { + filterByTk[key] = record[key]; + }); + return qs.stringify(filterByTk); + } else { + return record[targetKey]; + } + }; if (collectionOrAssociation instanceof Collection) { - const key = collectionOrAssociation.filterTargetKey || collectionOrAssociation.getPrimaryKey() || 'id'; - return collectionRecordOrAssociationRecord[key]; + const targetKey = getTargetKey(collectionOrAssociation); + return buildFilterByTk(targetKey, collectionRecordOrAssociationRecord); } if (collectionOrAssociation.includes('.')) { @@ -186,9 +200,8 @@ export class CollectionManager { ); return; } - - const key = targetCollection?.filterTargetKey || targetCollection?.getPrimaryKey() || 'id'; - return collectionRecordOrAssociationRecord[key]; + const targetKey = getTargetKey(targetCollection); + return buildFilterByTk(targetKey, collectionRecordOrAssociationRecord); } getSourceKeyByAssociation(associationName: string) { diff --git a/packages/core/client/src/data-source/components/CollectionDeletedPlaceholder.tsx b/packages/core/client/src/data-source/components/CollectionDeletedPlaceholder.tsx index fdf2aff5622..024b938f381 100644 --- a/packages/core/client/src/data-source/components/CollectionDeletedPlaceholder.tsx +++ b/packages/core/client/src/data-source/components/CollectionDeletedPlaceholder.tsx @@ -16,6 +16,7 @@ import { useDataSourceManager } from '../data-source'; import { DEFAULT_DATA_SOURCE_KEY } from '../../data-source/data-source/DataSourceManager'; import { useCollection } from '../collection'; import { BlockItemCard } from '../../schema-component/antd/block-item/BlockItemCard'; +import { AnyKindOfDictionary } from 'lodash'; export interface CollectionDeletedPlaceholderProps { type: 'Collection' | 'Field' | 'Data Source' | 'Block template'; @@ -112,3 +113,51 @@ export const CollectionDeletedPlaceholder: FC return null; }; + +const CollectionNotAllowView = () => { + const { t } = useTranslation(); + const dataSource = useDataSource(); + const compile = useCompile(); + const collection = useCollection(); + const dataSourceManager = useDataSourceManager(); + const nameValue = useMemo(() => { + const dataSourcePrefix = + dataSourceManager?.getDataSources().length >= 1 && dataSource && dataSource.key !== DEFAULT_DATA_SOURCE_KEY + ? `${compile(dataSource.displayName || dataSource.key)} > ` + : ''; + if (collection) { + return `${dataSourcePrefix}${collection.name}`; + } + const collectionPrefix = collection + ? `${compile(collection.title) || collection.name || collection.tableName} > ` + : ''; + return `${dataSourcePrefix}${collectionPrefix}${collection.name}`; + }, []); + + const messageValue = useMemo(() => { + return t( + `The current user only has the UI configuration permission, but don't have view permission for collection "{{name}}"`, + { + name: nameValue, + }, + ).replaceAll('>', '>'); + }, [nameValue, t]); + return ( + + + + ); +}; + +/** + * @internal + */ +export const CollectionNotAllowViewPlaceholder: FC = () => { + const { designable } = useDesignable(); + + if (designable) { + return ; + } + + return null; +}; diff --git a/packages/core/client/src/data-source/data-block/DataBlockProvider.tsx b/packages/core/client/src/data-source/data-block/DataBlockProvider.tsx index 71964c2058a..11df389b3e2 100644 --- a/packages/core/client/src/data-source/data-block/DataBlockProvider.tsx +++ b/packages/core/client/src/data-source/data-block/DataBlockProvider.tsx @@ -121,6 +121,27 @@ export interface DataBlockContextValue { export const DataBlockContext = createContext>({} as any); DataBlockContext.displayName = 'DataBlockContext'; +const DataBlockResourceContext = createContext<{ rerenderDataBlock: () => void }>(null); +export const RerenderDataBlockProvider: FC = ({ children }) => { + const [hidden, setHidden] = React.useState(false); + const value = useMemo(() => { + return { + rerenderDataBlock: () => { + setHidden(true); + setTimeout(() => { + setHidden(false); + }); + }, + }; + }, []); + + if (hidden) { + return null; + } + + return {children}; +}; + /** * @internal */ @@ -170,7 +191,9 @@ export const DataBlockProvider: FC> = withDynamicSche - {children} + + {children} + @@ -195,3 +218,11 @@ export const useDataBlockProps = (): DataBlockContextValue['pro const context = useDataBlock(); return context.props; }; + +export const useRerenderDataBlock = () => { + const context = useContext(DataBlockResourceContext); + if (!context) { + throw new Error('useRerenderDataBlock() must be used within a DataBlockProvider'); + } + return context; +}; diff --git a/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx b/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx index 6625dc9277a..9bb4d8df08b 100644 --- a/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx +++ b/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx @@ -9,7 +9,7 @@ import { IResource } from '@nocobase/sdk'; import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react'; - +import { isArray } from 'lodash'; import { useAPIClient } from '../../api-client'; import { useCollectionManager } from '../collection'; import { CollectionRecord } from '../collection-record'; @@ -34,7 +34,16 @@ export const DataBlockResourceProvider: FC<{ children?: ReactNode }> = ({ childr if (association && parentRecord) { const sourceKey = cm.getSourceKeyByAssociation(association); const parentRecordData = parentRecord instanceof CollectionRecord ? parentRecord.data : parentRecord; - return parentRecordData[sourceKey]; + if (isArray(sourceKey)) { + const filterByTk = {}; + for (const key of sourceKey) { + filterByTk[key] = parentRecordData?.[key]; + } + + return encodeURIComponent(JSON.stringify(filterByTk)); + } else { + return parentRecordData[sourceKey]; + } } }, [association, sourceId, parentRecord]); diff --git a/packages/core/client/src/filter-provider/__tests__/utiles.test.ts b/packages/core/client/src/filter-provider/__tests__/utiles.test.ts index 1d6c886f9b4..be25dce192f 100644 --- a/packages/core/client/src/filter-provider/__tests__/utiles.test.ts +++ b/packages/core/client/src/filter-provider/__tests__/utiles.test.ts @@ -166,7 +166,9 @@ describe('transformToFilter', () => { const getCollectionJoinField = vi.fn((name: string) => { if (name === `${collectionName}.field1`) return {}; if (name === `${collectionName}.field2`) return {}; - if (name === `${collectionName}.field3`) return { target: 'targetCollection', targetKey: 'id' }; + if (name === `${collectionName}.field3`) return { target: 'targetCollection', targetKey: 'id', type: 'belongsTo' }; + if (name === `${collectionName}.chinaRegion`) + return { target: 'chinaRegions', targetKey: 'code', interface: 'chinaRegion', type: 'belongsToMany' }; return {}; }); @@ -242,4 +244,27 @@ describe('transformToFilter', () => { expect(filter).toEqual(expectedFilter); }); + + it('should handle chinaRegion', () => { + const values = { + chinaRegion: [ + { + code: '1', + }, + { + code: '2', + }, + { + code: '3', + }, + ], + }; + const expectedFilter = { + $and: [{ 'chinaRegion.code': { $eq: '3' } }], + }; + + const filter = transformToFilter(values, operators, getCollectionJoinField, collectionName); + + expect(filter).toEqual(expectedFilter); + }); }); diff --git a/packages/core/client/src/filter-provider/utils.ts b/packages/core/client/src/filter-provider/utils.ts index 627635367c6..e59fbf16af7 100644 --- a/packages/core/client/src/filter-provider/utils.ts +++ b/packages/core/client/src/filter-provider/utils.ts @@ -139,9 +139,16 @@ export const transformToFilter = ( let value = _.get(values, key); const collectionField = getCollectionJoinField(`${collectionName}.${key}`); - if (collectionField?.target) { + if ( + collectionField?.target && + ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'belongsToArray'].includes(collectionField.type) + ) { value = getValuesByPath(value, collectionField.targetKey || 'id'); key = `${key}.${collectionField.targetKey || 'id'}`; + + if (collectionField?.interface === 'chinaRegion') { + value = _.last(value); + } } if (!value && value !== 0 && value !== false) { diff --git a/packages/core/client/src/global.less b/packages/core/client/src/global.less index 4b93bb91a70..0a7b20ed1e8 100644 --- a/packages/core/client/src/global.less +++ b/packages/core/client/src/global.less @@ -1,5 +1,3 @@ -// the custom style for desktop -@media (min-width: 768px) { /* width */ ::-webkit-scrollbar { width: 8px; @@ -35,12 +33,3 @@ .rc-virtual-list-scrollbar-thumb:active { background: var(--colorBgScrollBarActive) !important; } -} - -// the custom style for mobile -@media (max-width: 767px) { - // ensure smooth scrolling on iOS devices - .smooth-scroll { - -webkit-overflow-scrolling: touch; - } -} diff --git a/packages/core/client/src/i18n/SwitchLanguage.tsx b/packages/core/client/src/i18n/SwitchLanguage.tsx new file mode 100644 index 00000000000..f204a6dd3f6 --- /dev/null +++ b/packages/core/client/src/i18n/SwitchLanguage.tsx @@ -0,0 +1,42 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { TranslationOutlined } from '@ant-design/icons'; +import { Dropdown } from 'antd'; +import React from 'react'; +import { useAPIClient } from '../api-client'; +import languageCodes from '../locale'; +import { useSystemSettings } from '../system-settings'; + +export function SwitchLanguage() { + const { data } = useSystemSettings(); + const api = useAPIClient(); + return ( + data?.data?.enabledLanguages.length > 1 && ( + { + return { + key: code, + label: languageCodes[code].label, + }; + }), + }} + > + + + ) + ); +} diff --git a/packages/core/client/src/index.ts b/packages/core/client/src/index.ts index 0a03e4b6576..b412d5a407c 100644 --- a/packages/core/client/src/index.ts +++ b/packages/core/client/src/index.ts @@ -74,4 +74,9 @@ export { OpenModeProvider, useOpenModeContext } from './modules/popup/OpenModePr export { PopupContextProvider } from './modules/popup/PopupContextProvider'; export { usePopupUtils } from './modules/popup/usePopupUtils'; +export { SwitchLanguage } from './i18n/SwitchLanguage'; export { VariablePopupRecordProvider } from './modules/variable/variablesProvider/VariablePopupRecordProvider'; + +export { useCurrentPopupRecord } from './modules/variable/variablesProvider/VariablePopupRecordProvider'; + +export { languageCodes } from './locale'; diff --git a/packages/core/client/src/locale/en_US.json b/packages/core/client/src/locale/en_US.json index cbfdf6fdb19..57e7f4ecf02 100644 --- a/packages/core/client/src/locale/en_US.json +++ b/packages/core/client/src/locale/en_US.json @@ -427,6 +427,7 @@ "Option label": "Option label", "Color": "Color", "Background Color": "Background Color", + "Text Align": "Text Align", "Add option": "Add option", "Related collection": "Related collection", "Allow linking to multiple records": "Allow linking to multiple records", @@ -639,7 +640,7 @@ "Current time": "Current time", "System variables": "System variables", "Date variables": "Date variables", - "Popup close method": "Popup close method", + "Message popup close method": "Message popup close method", "Automatic close": "Automatic close", "Manually close": "Manually close", "After successful update": "After successful update", @@ -838,5 +839,11 @@ "Open in new window": "Open in new window", "Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.", "is none of": "is none of", - "is any of": "is any of" + "is any of": "is any of", + "Plugin dependency version mismatch": "Plugin dependency version mismatch", + "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?", + "Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data", + "Enable secondary confirmation": "Enable secondary confirmation", + "Notification": "Notification", + "Ellipsis overflow content": "Ellipsis overflow content" } diff --git a/packages/core/client/src/locale/es_ES.json b/packages/core/client/src/locale/es_ES.json index d0c8ccab344..7e26cf7a99d 100644 --- a/packages/core/client/src/locale/es_ES.json +++ b/packages/core/client/src/locale/es_ES.json @@ -765,5 +765,6 @@ "Expand All": "Expandir todo", "Clear default value": "Borrar valor por defecto", "Open in new window": "Abrir en una nueva ventana", - "Sorry, the page you visited does not exist.": "Lo siento, la página que visitaste no existe." + "Sorry, the page you visited does not exist.": "Lo siento, la página que visitaste no existe.", + "Ellipsis overflow content": "Contenido de desbordamiento de elipsis" } diff --git a/packages/core/client/src/locale/fr_FR.json b/packages/core/client/src/locale/fr_FR.json index 4a9cca811ad..6f3af0f5637 100644 --- a/packages/core/client/src/locale/fr_FR.json +++ b/packages/core/client/src/locale/fr_FR.json @@ -785,5 +785,6 @@ "Expand All": "Tout déplier", "Clear default value": "Effacer la valeur par défaut", "Open in new window": "Ouvrir dans une nouvelle fenêtre", - "Sorry, the page you visited does not exist.": "Désolé, la page que vous avez visitée n'existe pas." + "Sorry, the page you visited does not exist.": "Désolé, la page que vous avez visitée n'existe pas.", + "Ellipsis overflow content": "Contenu de débordement avec ellipse" } diff --git a/packages/core/client/src/locale/index.ts b/packages/core/client/src/locale/index.ts index 8723e05f3fa..6719d551167 100644 --- a/packages/core/client/src/locale/index.ts +++ b/packages/core/client/src/locale/index.ts @@ -7,6 +7,15 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + export type LocaleOptions = { label: string; }; @@ -81,7 +90,7 @@ export const dayjsLocale = { 'zh-TW': 'zh-tw', }; -export default { +export const languageCodes = { 'ar-EG': { label: 'العربية' }, 'az-AZ': { label: 'Azərbaycan dili' }, 'bg-BG': { label: 'Български' }, @@ -150,3 +159,5 @@ export default { 'zh-HK': { label: '繁體中文(香港)' }, 'zh-TW': { label: '繁體中文(台湾)' }, }; + +export default languageCodes; diff --git a/packages/core/client/src/locale/ja_JP.json b/packages/core/client/src/locale/ja_JP.json index d1b4fb90126..d99cede3915 100644 --- a/packages/core/client/src/locale/ja_JP.json +++ b/packages/core/client/src/locale/ja_JP.json @@ -178,7 +178,7 @@ "Before change": "変更前", "After change": "変更後", "Delete record": "レコード削除", - "Delete collection":"データテーブルの削除", + "Delete collection": "データテーブルの削除", "Create collection": "コレクションの作成", "Collection display name": "コレクション名", "Collection name": "コレクション識別子", @@ -680,14 +680,14 @@ "Plugin starting...": "プラグインを起動しています...", "Plugin stopping...": "プラグインを停止しています...", "Are you sure to delete this plugin?": "このプラグインを削除してもよろしいですか?", - "Are you sure to delete this plugin":"本当にこのプラグインを無効にしますか?", + "Are you sure to delete this plugin": "本当にこのプラグインを無効にしますか?", "re-download file": "ファイルを再ダウンロード", "Not enabled": "有効になっていません", "Search plugin": "プラグインを検索", "Author": "著者", "Plugin loading failed. Please check the server logs.": "プラグインのロードに失敗しました。サーバーログを確認してください。", "Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects": "テーブルに依存するオブジェクト、およびそれらに依存するオブジェクトを自動的に削除する", - "Allow add new":"新規作成を許可", + "Allow add new": "新規作成を許可", "Allow selection of existing records": "既存のデータの選択を許可", "loading": "ロード中", "name is required": "名前は必須です", @@ -704,5 +704,305 @@ "Expand All": "すべて展開", "Clear default value": "デフォルト値をクリア", "Open in new window": "新しいウィンドウで開く", - "Sorry, the page you visited does not exist.": "申し訳ありませんが、お探しのページは存在しません。" + "Sorry, the page you visited does not exist.": "申し訳ありませんが、お探しのページは存在しません。", + "Ellipsis overflow content": "省略記号で内容を省略", + "NaN": "なし", + "Settings": "設定", + "Collection selector": "コレクションセレクタ", + "Providing certain collections as options for users, typically used in polymorphic or inheritance scenarios": "ユーザーに提供する特定のコレクションは多態性や継承シナリオで使用されます。", + "Problematic": "問題あり", + "Setting": "設定", + "List": "リスト", + "Grid Card": "グリッドカード", + "pixels": "ピクセル", + "Screen size": "スクリーンサイズ", + "Display title": "タイトルを表示", + "Set the count of columns displayed in a row": "列の数を設定", + "Column": "カラム", + "Phone device": "携帯デバイス", + "Tablet device": "タブレットデバイス", + "Desktop device": "デスクトップデバイス", + "Large screen device": "大画面デバイス", + "Delete events": "イベントを削除", + "This event": "このイベント", + "This and following events": "このイベントおよび後続のイベント", + "All events": "すべてのイベント", + "Delete this event?": "このイベントを削除しますか?", + "Delete Event": "イベントを削除", + "Default collapse": "デフォルト閉じる", + "Insert if not exists": "存在しない場合に挿入", + "Insert if not exists, or update": "存在しない場合に挿入、さもなくば更新", + "Determine whether a record exists by the following fields": "次のフィールドでレコードが存在するかどうかを判断", + "Update record": "レコードを更新", + "Primary key, unique identifier, self growth": "主キー、ユニークID、自動増加", + "Store the creation user of each record": "各レコードの作成者を保存", + "Store the last update user of each record": "各レコードの最終更新者を保存", + "Store the creation time of each record": "各レコードの作成日時を保存", + "Store the last update time of each record": "各レコードの最終更新日時を保存", + "More options": "その他のオプション", + "Sync from database": "データベースから同期", + "Association fields filter": "関連フィールドフィルター", + "PK & FK fields": "PK & FKフィールド", + "Association fields": "関連フィールド", + "System fields": "システムフィールド", + "General fields": "一般フィールド", + "Inherited fields": "継承フィールド", + "Parent collection fields": "親コレクションフィールド", + "Inverse relationship type": "逆関連付けタイプ", + "Generated automatically if left blank": "空欄のままにすると自動的に生成されます", + "Display field title": "フィールドタイトルを表示", + "Toggles the subfield mode": "サブフィールドモードを切り替える", + "Selector mode": "セレクターモード", + "Subtable mode": "サブテーブルモード", + "Subform mode": "サブフォームモード", + "Pattern": "パターン", + "Operator": "演算子", + "Import": "インポート", + "Custom": "カスタム", + "Primary": "主キー", + "Auto increment": "自動インクリメント", + "Max length": "最大長さ", + "Min length": "最小長さ", + "Length": "長さ", + "The field value cannot be greater than ": "数値は...より大きくてはならない", + "The field value cannot be less than ": "数値は...より小さくてはならない", + "The field value is not an integer number": "数字は整数ではありません", + "Background Color": "背景色", + "Allow uploading multiple files": "複数のファイルのアップロードを許可する", + "Custom title": "カスタムタイトル", + "Daily": "毎日", + "Weekly": "毎週", + "Monthly": "毎月", + "Yearly": "毎年", + "Repeats": "繰り返し", + "Show lunar": "旧暦を表示", + "Importable fields": "インポート可能なフィールド", + "Sign in via account": "アカウントでサインイン", + "Sign in via phone": "電話番号でサインイン", + "Verification code": "認証コード", + "Send code": "認証コードを送信", + "Retry after {{count}} seconds": "{{count}} 秒後に再試行", + "Popup size": "ポップアップサイズ", + "Small": "スモール", + "Middle": "ミドル", + "Large": "ラージ", + "Identifier for program usage. Support letters, numbers and underscores, must start with an letter.": "プログラム使用のための識別子。文字、数字、...", + "Style": "スタイル", + "Allows to configure interface": "インターフェースの設定を許可", + "Allows to install, activate, disable plugins": "プラグインのインストール、アクティブ化、無効化を許可", + "Allows to configure plugins": "プラグインの設定を許可", + "Parent record": "親レコード", + "Current time": "現在の時刻", + "System variables": "システム変数", + "Date variables": "日付変数", + "Select": "選択", + "Done": "完了", + "File manager": "ファイルマネージャー", + "ACL": "アクセス制御", + "Collection manager": "コレクション管理", + "Plugin manager": "プラグイン管理", + "Local": "ローカル", + "Built-in": "内蔵", + "Marketplace": "マーケットプレイス", + "Add plugin": "プラグインを追加", + "Plugin source": "プラグインソース", + "Plugin dependencies check failed": "プラグインの依存関係のチェックに失敗しました", + "Plugin dependencies check failed, you should change the dependent version to meet the version requirements.": "プラグインの依存関係チェックに失敗しました。依存バージョンを変更して要件を満たしてください。", + "Are you sure to disable this plugin?": "このプラグインを無効にしてもよろしいですか?", + "Coming soon...": "近日公開予定です...", + "All plugin settings": "すべてのプラグイン設定", + "Bookmark": "ブックマーク", + "Manage all settings": "すべての設定を管理", + "Create inverse field in the target collection": "ターゲットコレクションに逆フィールドを作成", + "Inverse field name": "逆フィールド名", + "Inverse field display name": "逆フィールド表示名", + "Bulk update": "一括更新", + "After successful bulk update": "一括更新が成功した後", + "Bulk edit": "一括編集", + "Data will be updated": "データが更新されます", + "Selected": "選択済み", + "All": "すべて", + "Update selected data?": "選択したデータを更新しますか?", + "Update all data?": "すべてのデータを更新しますか?", + "Remains the same": "変更なし", + "Changed to": "に変更", + "Clear": "クリア", + "Add attach": "添付を追加", + "Please select the records to be updated": "更新するレコードを選択してください", + "Selector": "セレクタ", + "Inner": "内部", + "Please fill in the iframe URL": "iframe URLを入力してください", + "Fix block": "ブロックを固定", + "Plugin name": "プラグイン名", + "Plugin tab name": "プラグインタブ名", + "Column width": "列幅", + "Sortable": "ソート可能", + "Enable link": "リンクを有効にする", + "App error": "アプリエラー", + "Download logs": "ログをダウンロード", + "Assign data scope for the template": "テンプレートのデータ範囲を指定", + "Table selected records": "選択されたレコード", + "Restart": "再起動", + "Restart application": "アプリケーションを再起動", + "Execute": "実行", + "Please use a valid SELECT or WITH AS statement": "有効な SELECT または WITH AS 文を使用してください", + "Please confirm the SQL statement first": "SQL 文を確認してください", + "Sign in with another account": "他のアカウントでサインイン", + "Return to the main application": "メインアプリケーションに戻る", + "Permission deined": "アクセス拒否", + "Preset fields": "プリセットフィールド", + "Generic properties": "汎用プロパティ", + "Specific properties": "特定のプロパティ", + "Used for drag and drop sorting scenarios, supporting grouping sorting": "ドラッグ&ドロップによる並べ替えで使用され、グループ分けソートをサポート", + "Grouped sorting": "グループ分けソート", + "When a field is selected for grouping, it will be grouped first before sorting.": "フィールドをグループに選択すると、最初にグループ化され、その後ソートされます", + "Departments": "部門", + "Main department": "主要部門", + "Department name": "部門名", + "Superior department": "上位部門", + "Owners": "責任者", + "Plugin settings": "プラグイン設定", + "Menu": "メニュー", + "Drag and drop sorting field": "ドラッグ&ドロップソートフィールド", + "Search": "検索", + "is none of": "どれにも一致しない", + "is any of": "いずれかに一致する", + "Plugin dependency version mismatch": "プラグインの依存バージョンが一致しません", + "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "現在のプラグインの依存バージョンがアプリのバージョンと異なるため、正常に動作しない可能性があります。それでもプラグインを有効化しますか?", + "Page number": "ページ番号", + "Page size": "ページサイズ", + "Enable": "有効化", + "Disable": "無効化", + "Calculation engine": "計算エンジン", + "Expression collection": "式コレクション", + "Tree collection": "ツリーコレクション", + "Parent ID": "親ID", + "Parent": "親", + "Children": "子", + "Confirm": "確認", + "Block": "ブロック", + "Unnamed": "無名", + "SQL collection": "SQLコレクション", + "Configure field": "フィールドを設定", + "Username": "ユーザー名", + "Null": "ヌル", + "Boolean": "ブール値", + "String": "文字列", + "Syntax references": "構文リファレンス", + "Math.js comes with a large set of built-in functions and constants, and offers an integrated solution to work with different data types.": "Math.jsには多くの組み込み関数と定数があり、異なるデータ型を扱うための統合ソリューションを提供します。", + "Formula.js supports most Microsoft Excel formula functions.": "Formula.jsはMicrosoft Excelの大半の関数をサポートしています。", + "String template": "文字列テンプレート", + "Simple string replacement, can be used to interpolate variables in a string.": "シンプルな文字列置換で、文字列に変数を埋め込むことができます。", + "Display when unchecked": "未チェック時に を表示", + "Allow dissociate": "関連解除を許可", + "Must be 1-50 characters in length (excluding @.<>\"'/)": "1〜50文字で、@.<>\"'/は使用できません", + "Data source permissions": "データソースの権限", + "Now": "現在", + "Access control": "アクセス制御", + "Remove": "削除", + "Docs": "ドキュメント", + "Enable page header": "ページヘッダーを有効にする", + "Display page title": "ページタイトルを表示", + "Edit page title": "ページタイトルを編集", + "Enable page tabs": "ページタブを有効にする", + "Constant": "定数", + "Select a variable": "変数を選択", + "Double click to choose entire object": "ダブルクリックして全体オブジェクトを選択", + "true": "真", + "false": "偽", + "Prettify": "整形", + "Theme": "テーマ", + "Default theme": "デフォルトテーマ", + "Compact theme": "コンパクトテーマ", + "Download": "ダウンロード", + "Support for a single or bulk upload.": "単一または一括アップロードに対応します。", + "File size should not exceed {{size}}.": "ファイルサイズは{{size}}を超えてはいけません。", + "File size exceeds the limit": "ファイルサイズが制限を超えています", + "File type is not allowed": "ファイルタイプが許可されていません", + "Incomplete uploading files need to be resolved": "未完了のアップロードファイルを処理する必要があります", + "Enable form data template": "フォームデータテンプレートを有効にする", + "No configuration available.": "設定可能な項目がありません。", + "Reload application": "アプリケーションを再読み込み", + "The application is reloading, please do not close the page.": "アプリケーションが再読み込み中です。ページを閉じないでください。", + "Application reloading": "アプリケーションを再読み込み中", + "Allows to clear cache, reboot application": "キャッシュのクリアとアプリケーションの再起動が可能", + "The will interrupt service, it may take a few seconds to restart. Are you sure to continue?": "再起動はサービスを中断します。再起動には数秒かかる場合があります。続行しますか?", + "Clear cache": "キャッシュをクリア", + "Quick create": "クイック作成", + "Dropdown": "ドロップダウン", + "Pop-up": "ポップアップ", + "Direct duplicate": "直接複製", + "Copy into the form and continue to fill in": "フォームにコピーして入力を続ける", + "Failed to load plugin": "プラグインの読み込みに失敗しました", + "Filter out a single piece or a group of records as a template": "単一または複数のレコードをテンプレートとしてフィルタリング", + "The title field is used to identify the template record": "タイトルフィールドはテンプレートレコードを識別するために使用されます", + "Template fields": "テンプレートフィールド", + "The selected fields will automatically populate the form": "選択されたフィールドがフォームに自動入力されます", + "UnSelect all": "全ての選択を解除", + "Secondary confirmation": "二次確認", + "Perform the {{title}}": "{{title}}を実行", + "Are you sure you want to perform the {{title}} action?": "{{title}}操作を実行してもよろしいですか?", + "Permission denied": "権限が拒否されました", + "Data model": "データモデル", + "Security": "セキュリティ", + "Action": "アクション", + "System": "システム", + "Other": "その他", + "Data Model": "データモデル", + "Blocks": "ブロック", + "Users & permissions": "ユーザーと権限", + "System management": "システム管理", + "System & security": "システムとセキュリティ", + "Workflow": "ワークフロー", + "Third party services": "サードパーティサービス", + "Data model tools": "データモデルツール", + "Data sources": "データソース", + "Collections": "コレクション", + "Collection fields": "コレクションフィールド", + "Authentication": "認証", + "Logging and monitoring": "ログおよび監視", + "Main": "メイン", + "Index": "インデックス", + "Field values must be unique.": "フィールド値は一意でなければなりません", + "Alphabet": "アルファベット", + "Accuracy": "精度", + "Millisecond": "ミリ秒", + "Second": "秒", + "Unix Timestamp": "Unixタイムスタンプ", + "Field value do not meet the requirements": "フィールド値が要件を満たしていません", + "Field value size is": "フィールド値のサイズは", + "Unit conversion": "単位変換", + "Separator": "区切り", + "Prefix": "接頭辞", + "Suffix": "接尾辞", + "Record unique key": "レコードのユニークキー", + "Filter target key": "フィルター対象キー", + "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.": "コレクションにプライマリキーがない場合、行レコードを特定するためのユニークキーを設定する必要があります。設定しないと、コレクションのデータブロックが作成できません。", + "Filter data based on the specific field, with the requirement that the field value must be unique.": "特定フィールドに基づいてデータをフィルタリングしますが、そのフィールド値は一意である必要があります。", + "Multiply by": "掛ける", + "Divide by": "割る", + "Scientifix notation": "科学的表記", + "Normal": "通常", + "Automatically generate default values": "自動でデフォルト値を生成", + "Refresh data on close": "閉じるとデータを更新", + "Refresh data on action": "アクション時にデータを更新", + "Unknown field type": "不明なフィールドタイプ", + "The following field types are not compatible and do not support output and display": "次のフィールドタイプは互換性がなく、出力および表示をサポートしていません", + "Not fixed": "固定されていない", + "Left fixed": "左に固定", + "Right fixed": "右に固定", + "Fixed": "固定", + "Set block height": "ブロックの高さを設定", + "Specify height": "高さを指定", + "Full height": "全画面表示", + "Please configure the URL": "URLを設定してください", + "URL": "URL", + "Search parameters": "検索パラメーター", + "Do not concatenate search params in the URL": "URLに検索パラメーターを連結しない", + "Edit link": "リンクを編集", + "Add parameter": "パラメーターを追加", + "Use simple pagination mode": "シンプルなページネーションモードを使用", + "Set Template Engine": "テンプレートエンジンを設定", + "Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "ページング時にテーブルレコードの総数取得をスキップして、読み込み速度を向上させます。データ量が多い場合にこのオプションの使用をお勧めします。", + "The current user only has the UI configuration permission, but don't have view permission for collection \"{{name}}\"": "現在のユーザーにはUI設定の権限しかなく、コレクション「{{name}}」を閲覧する権限はありません。" } diff --git a/packages/core/client/src/locale/ko_KR.json b/packages/core/client/src/locale/ko_KR.json index 1576dc4aad9..25e3bc0ae03 100644 --- a/packages/core/client/src/locale/ko_KR.json +++ b/packages/core/client/src/locale/ko_KR.json @@ -876,5 +876,6 @@ "Expand All": "모두 펼치기", "Clear default value": "기본값 지우기", "Open in new window": "새 창에서 열기", -"Sorry, the page you visited does not exist.": "죄송합니다. 방문한 페이지가 존재하지 않습니다." +"Sorry, the page you visited does not exist.": "죄송합니다. 방문한 페이지가 존재하지 않습니다.", +"Ellipsis overflow content": "생략 부호로 내용 줄임" } diff --git a/packages/core/client/src/locale/pt_BR.json b/packages/core/client/src/locale/pt_BR.json index a36d710e068..b62239943ff 100644 --- a/packages/core/client/src/locale/pt_BR.json +++ b/packages/core/client/src/locale/pt_BR.json @@ -742,5 +742,6 @@ "Current popup record": "Registro pop-up atual", "Clear default value": "Limpar valor padrão", "Open in new window": "Abrir em nova janela", - "Sorry, the page you visited does not exist.": "Desculpe, a página que você visitou não existe." + "Sorry, the page you visited does not exist.": "Desculpe, a página que você visitou não existe.", + "Ellipsis overflow content": "Conteúdo de transbordamento com reticências" } diff --git a/packages/core/client/src/locale/ru_RU.json b/packages/core/client/src/locale/ru_RU.json index c61e7b9817b..04fcd165f35 100644 --- a/packages/core/client/src/locale/ru_RU.json +++ b/packages/core/client/src/locale/ru_RU.json @@ -579,5 +579,6 @@ "Expand All": "Развернуть все", "Clear default value": "Очистить значение по умолчанию", "Open in new window": "Открыть в новом окне", - "Sorry, the page you visited does not exist.": "Извините, посещенной вами страницы не существует." + "Sorry, the page you visited does not exist.": "Извините, посещенной вами страницы не существует.", + "Ellipsis overflow content": "Содержимое с многоточием при переполнении" } diff --git a/packages/core/client/src/locale/tr_TR.json b/packages/core/client/src/locale/tr_TR.json index 5ba68ab3f09..60e54f36163 100644 --- a/packages/core/client/src/locale/tr_TR.json +++ b/packages/core/client/src/locale/tr_TR.json @@ -577,5 +577,6 @@ "Expand All": "Tümünü genişlet", "Clear default value": "Varsayılan değeri temizle", "Open in new window": "Yeni pencerede aç", - "Sorry, the page you visited does not exist.": "Üzgünüz, ziyaret ettiğiniz sayfa mevcut değil." + "Sorry, the page you visited does not exist.": "Üzgünüz, ziyaret ettiğiniz sayfa mevcut değil.", + "Ellipsis overflow content": "Üç nokta ile taşan içerik" } diff --git a/packages/core/client/src/locale/uk_UA.json b/packages/core/client/src/locale/uk_UA.json index 5464b0af51b..2f622e7cda3 100644 --- a/packages/core/client/src/locale/uk_UA.json +++ b/packages/core/client/src/locale/uk_UA.json @@ -785,5 +785,6 @@ "Expand All": "Розгорнути все", "Clear default value": "Очистити значення за замовчуванням", "Open in new window": "Відкрити в новому вікні", - "Sorry, the page you visited does not exist.": "Вибачте, сторінка, яку ви відвідали, не існує." + "Sorry, the page you visited does not exist.": "Вибачте, сторінка, яку ви відвідали, не існує.", + "Ellipsis overflow content": "Вміст з багатокрапкою при переповненні" } diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json index c3645dfdd8f..707af0105d1 100644 --- a/packages/core/client/src/locale/zh-CN.json +++ b/packages/core/client/src/locale/zh-CN.json @@ -283,7 +283,7 @@ "Checkbox group": "复选框", "China region": "中国行政区", "Date & Time": "日期 & 时间", - "Datetime": "日期", + "Datetime": "日期时间", "Relation": "关系类型", "Link to": "关联", "Link to description": "用于快速创建表关系,可兼容大多数普通场景。适合非开发人员使用。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。创建后,将同时在目标数据表中生成当前数据表的关联字段。", @@ -448,6 +448,7 @@ "Option label": "选项标签", "Color": "颜色", "Background Color": "背景颜色", + "Text Align": "文本对齐", "Add option": "添加选项", "Related collection": "关系表", "Allow linking to multiple records": "允许关联多条记录", @@ -655,7 +656,7 @@ "Parent record": "上级记录", "Current time": "当前时间", "Now": "现在", - "Popup close method": "弹窗关闭方式", + "Message popup close method": "消息弹窗关闭方式", "Automatic close": "自动关闭", "Manually close": "手动关闭", "After successful update": "更新成功后", @@ -799,6 +800,7 @@ "Try again": "重试一下", "Download logs": "下载日志", "Download": "下载", + "File type is not supported for previewing, please download it to preview.": "不支持预览该文件类型,请下载后查看。", "Click or drag file to this area to upload": "点击或拖拽文件到此区域上传", "Support for a single or bulk upload.": "支持单个或批量上传", "File size should not exceed {{size}}.": "文件大小不能超过 {{size}}", @@ -901,6 +903,7 @@ "Collections": "数据表", "Collection fields": "数据表字段", "Authentication": "用户认证", + "Notification": "通知", "Logging and monitoring": "日志与监控", "Home page": "主页", "Handbook": "用户手册", @@ -971,5 +974,35 @@ "Use simple pagination mode": "使用简单分页模式", "Sorry, the page you visited does not exist.": "抱歉,你访问的页面不存在。", "Set Template Engine": "设置模板引擎", - "Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "在分页时跳过获取表记录总数,以加快加载速度,建议对有大量数据的数据表开启此选项" + "Template engine": "模板引擎", + "Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "在分页时跳过获取表记录总数,以加快加载速度,建议对有大量数据的数据表开启此选项", + "The current user only has the UI configuration permission, but don't have view permission for collection \"{{name}}\"": "当前用户只有 UI 配置权限,但没有数据表 \"{{name}}\" 查看权限。", + "Plugin dependency version mismatch": "插件依赖版本不一致", + "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "当前插件的依赖版本与应用的版本不一致,可能无法正常工作。您确定要继续激活插件吗?", + "Default value to current time": "设置字段默认值为当前时间", + "Automatically update timestamp on update": "当记录更新时自动设置字段值为当前时间", + "Default value to current server time": "设置字段默认值为当前服务端时间", + "Automatically update timestamp to the current server time on update": "当记录更新时自动设置字段值为当前服务端时间", + "Datetime (with time zone)": "日期时间(含时区)", + "Datetime (without time zone)": "日期时间(不含时区)", + "DateOnly": "仅日期", + "Enable secondary confirmation": "启用二次确认", + "Content": "内容", + "Perform the Update record": "执行更新数据", + "Are you sure you want to perform the Update record action?": "你确定执行更新数据操作吗?", + "Perform the Custom request": "执行自定义请求", + "Are you sure you want to perform the Custom request action": "你确定执行自定义请求操作吗?", + "Perform the Refresh": "执行刷新", + "Are you sure you want to perform the Refresh action?": "你确定执行刷新操作吗?", + "Perform the Submit": "执行提交", + "Are you sure you want to perform the Submit action?": "你确定执行提交操作吗?", + "Perform the Trigger workflow": "执行触发工作流", + "Are you sure you want to perform the Trigger workflow action?": "你确定执行触发工作流吗?", + "Ellipsis overflow content": "省略超出长度的内容", + "Picker": "选择器", + "Quarter":"季度", + "Switching the picker, the value and default value will be cleared":"切换选择器时,字段的值和默认值将会被清空", + "Stay on the current popup or page":"停留在当前弹窗或页面", + "Return to the previous popup or page":"返回上一层弹窗或页面", + "Action after successful submission":"提交成功后动作" } diff --git a/packages/core/client/src/locale/zh-TW.json b/packages/core/client/src/locale/zh-TW.json index 99d34adfe2d..b9f1c087b5e 100644 --- a/packages/core/client/src/locale/zh-TW.json +++ b/packages/core/client/src/locale/zh-TW.json @@ -444,6 +444,7 @@ "Option value": "選項值", "Option label": "選項標籤", "Color": "顏色", + "Text Align": "文本對齊", "Add option": "新增選項", "Related collection": "關聯表", "Allow linking to multiple records": "允許關聯多條記錄", @@ -874,5 +875,6 @@ "Expand All": "展開全部", "Clear default value": "清除預設值", "Open in new window": "新窗口打開", - "Sorry, the page you visited does not exist.": "抱歉,你訪問的頁面不存在。" + "Sorry, the page you visited does not exist.": "抱歉,你訪問的頁面不存在。", + "Ellipsis overflow content": "省略超出長度的內容" } diff --git a/packages/core/client/src/modules/actions/__e2e__/bulk-destroy/basic.test.ts b/packages/core/client/src/modules/actions/__e2e__/bulk-destroy/basic.test.ts index 25886da626b..777b9bb5c16 100644 --- a/packages/core/client/src/modules/actions/__e2e__/bulk-destroy/basic.test.ts +++ b/packages/core/client/src/modules/actions/__e2e__/bulk-destroy/basic.test.ts @@ -44,6 +44,8 @@ test.describe('bulk-destroy', () => { await page.getByLabel('action-Action-Delete-destroy-').hover(); await page.getByLabel('designer-schema-settings-Action-actionSettings:bulkDelete-general').hover(); await page.getByRole('menuitem', { name: 'Secondary confirmation' }).click(); + await page.getByLabel('Enable secondary confirmation').uncheck(); + await page.getByRole('button', { name: 'OK' }).click(); await page.mouse.move(500, 0); // 2. 选中所有行 diff --git a/packages/core/client/src/modules/actions/__e2e__/submit/refreshData.test.ts b/packages/core/client/src/modules/actions/__e2e__/submit/refreshData.test.ts index 5b58bdb3057..89b7c5ce7cf 100644 --- a/packages/core/client/src/modules/actions/__e2e__/submit/refreshData.test.ts +++ b/packages/core/client/src/modules/actions/__e2e__/submit/refreshData.test.ts @@ -8,7 +8,11 @@ */ import { expect, test } from '@nocobase/test/e2e'; -import { submitInReferenceTemplateBlock } from './templates'; +import { + shouldRefreshDataWhenSubpageIsClosedByPageMenu, + submitInReferenceTemplateBlock, + createFormSubmit, +} from './templates'; test.describe('Submit: should refresh data after submit', () => { test('submit in reference template block', async ({ page, mockPage, clearBlockTemplates, mockRecord }) => { @@ -83,4 +87,59 @@ test.describe('Submit: should refresh data after submit', () => { page.getByLabel('block-item-CardItem-collection-table').getByRole('button', { name: 'abc456' }), ).toBeVisible(); }); + + test('should refresh data when subpage is closed by page menu', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(shouldRefreshDataWhenSubpageIsClosedByPageMenu).waitForInit(); + await mockRecord('testRefresh', { name: 'abcdefg' }); + await nocoPage.goto(); + const pageUid = await nocoPage.getUid(); + + // 1. Initially, there should be a row with "abcdefg" on the page + await expect(page.getByRole('button', { name: 'abcdefg', exact: true })).toBeVisible(); + + // 2. Click the "Edit" button to open the subpage + await page.getByLabel('action-Action.Link-Edit-').click(); + + // 3. On the subpage, open a popup, fill out the form, and submit + await page.getByLabel('Edit', { exact: true }).getByLabel('action-Action.Link-Edit-').click(); + await page.getByLabel('block-item-CollectionField-').getByRole('textbox').fill('1234567890'); + await page.getByLabel('action-Action-Submit-submit-').click(); + + // 4. Close the subpage by clicking on the menu at the top of the page + await page.getByLabel(pageUid).click(); + + // 5. The data in the block on the page should be up-to-date + await page.getByRole('button', { name: '1234567890', exact: true }).click(); + }); +}); + +test.describe('Submit: After successful submission', () => { + test('return to the previous popup or page as default value', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(createFormSubmit).waitForInit(); + await nocoPage.goto(); + + await page.getByLabel('action-Action-Add new-create-').click(); + await page.getByLabel('action-Action-Submit-submit-').click(); + await expect(page.getByTestId('drawer-Action.Container-users-Add record')).not.toBeVisible(); + }); + + test('return to the previous popup or page change to stay on the current popup or page', async ({ + page, + mockPage, + mockRecord, + }) => { + const nocoPage = await mockPage(createFormSubmit).waitForInit(); + await nocoPage.goto(); + + await page.getByLabel('action-Action-Add new-create-').click(); + + await page.getByLabel('action-Action-Submit-submit-').hover(); + await page.getByLabel('designer-schema-settings-Action-actionSettings:createSubmit-users').hover(); + + await page.getByText('After successful submission').click(); + await page.getByLabel('Stay on the current popup or').check(); + await page.getByRole('button', { name: 'OK' }).click(); + await page.getByLabel('action-Action-Submit-submit-').click(); + await expect(page.getByTestId('drawer-Action.Container-users-Add record')).toBeVisible(); + }); }); diff --git a/packages/core/client/src/modules/actions/__e2e__/submit/templates.ts b/packages/core/client/src/modules/actions/__e2e__/submit/templates.ts index 0b12f0ff77b..004d6a42b1a 100644 --- a/packages/core/client/src/modules/actions/__e2e__/submit/templates.ts +++ b/packages/core/client/src/modules/actions/__e2e__/submit/templates.ts @@ -863,3 +863,1099 @@ export const submitInReferenceTemplateBlock: PageConfig = { 'x-index': 1, }, }; +export const shouldRefreshDataWhenSubpageIsClosedByPageMenu = { + collections: [ + { + name: 'testRefresh', + fields: [ + { + name: 'name', + interface: 'input', + }, + ], + }, + ], + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + f60xwf8ufc1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: { + c383hu4oxba: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.25-beta', + properties: { + igrzb8z8p47: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.25-beta', + properties: { + czd5rlqiiug: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'testRefresh:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'testRefresh', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.3.25-beta', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.3.25-beta', + 'x-uid': 'u883ena2rat', + 'x-async': false, + 'x-index': 1, + }, + '0s7l6sopxd1': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.3.25-beta', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.3.25-beta', + properties: { + jxbi8tufxur: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.3.25-beta', + properties: { + '5ip8j17sa1q': { + 'x-uid': 'tmlwp0igqn3', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Edit") }}', + 'x-action': 'update', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:edit', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'page', + icon: 'EditOutlined', + }, + 'x-action-context': { + dataSource: 'main', + collection: 'testRefresh', + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Edit record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Edit")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:common:addBlock', + properties: { + a5o0u6ic4zi: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.25-beta', + properties: { + o94fslh753r: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.25-beta', + properties: { + bxmjip1l3zo: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'testRefresh:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'testRefresh', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.3.25-beta', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.3.25-beta', + 'x-uid': 'cbina6hzuul', + 'x-async': false, + 'x-index': 1, + }, + nol8niatans: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.3.25-beta', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.3.25-beta', + properties: { + l3z75xol1mi: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.3.25-beta', + properties: { + ls8c2cy2m89: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Edit") }}', + 'x-action': 'update', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:edit', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'drawer', + icon: 'EditOutlined', + }, + 'x-action-context': { + dataSource: 'main', + collection: 'testRefresh', + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Edit record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Edit")}}', + 'x-component': + 'Tabs.TabPane', + 'x-designer': + 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': + 'popup:common:addBlock', + properties: { + c8inwkebx1a: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': + 'Grid.Row', + 'x-app-version': + '1.3.25-beta', + properties: { + a5gbhpfx4ay: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': + 'Grid.Col', + 'x-app-version': + '1.3.25-beta', + properties: { + hekz785yebu: { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-acl-action-props': + { + skipScopeCheck: + false, + }, + 'x-acl-action': + 'testRefresh:update', + 'x-decorator': + 'FormBlockProvider', + 'x-use-decorator-props': + 'useEditFormBlockDecoratorProps', + 'x-decorator-props': + { + action: + 'get', + dataSource: + 'main', + collection: + 'testRefresh', + }, + 'x-toolbar': + 'BlockSchemaToolbar', + 'x-settings': + 'blockSettings:editForm', + 'x-component': + 'CardItem', + 'x-app-version': + '1.3.25-beta', + properties: + { + '1r799t325wo': + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'FormV2', + 'x-use-component-props': + 'useEditFormBlockProps', + 'x-app-version': + '1.3.25-beta', + properties: + { + grid: { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid', + 'x-initializer': + 'form:configureFields', + 'x-app-version': + '1.3.25-beta', + properties: + { + w7mpjt5r510: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Row', + 'x-app-version': + '1.3.25-beta', + properties: + { + '1tncur8pho6': + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Col', + 'x-app-version': + '1.3.25-beta', + properties: + { + name: { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'string', + 'x-toolbar': + 'FormItemSchemaToolbar', + 'x-settings': + 'fieldSettings:FormItem', + 'x-component': + 'CollectionField', + 'x-decorator': + 'FormItem', + 'x-collection-field': + 'testRefresh.name', + 'x-component-props': + {}, + 'x-app-version': + '1.3.25-beta', + 'x-uid': + 'w2b29tftqa5', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'vmqxwcj1sah', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'jx174p4aj50', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'nnf7799bh1o', + 'x-async': + false, + 'x-index': 1, + }, + w12hok3wqww: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-initializer': + 'editForm:configureActions', + 'x-component': + 'ActionBar', + 'x-component-props': + { + layout: + 'one-column', + }, + 'x-app-version': + '1.3.25-beta', + properties: + { + tpy6isposk0: + { + _isJSONSchemaObject: + true, + version: + '2.0', + title: + '{{ t("Submit") }}', + 'x-action': + 'submit', + 'x-component': + 'Action', + 'x-use-component-props': + 'useUpdateActionProps', + 'x-toolbar': + 'ActionSchemaToolbar', + 'x-settings': + 'actionSettings:updateSubmit', + 'x-component-props': + { + type: 'primary', + htmlType: + 'submit', + }, + 'x-action-settings': + { + triggerWorkflows: + [], + }, + type: 'void', + 'x-app-version': + '1.3.25-beta', + 'x-uid': + 'witfr2ignds', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + '95h3woyegid', + 'x-async': + false, + 'x-index': 2, + }, + }, + 'x-uid': + 'jz4pvx7vdyn', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'w9r4yyxzwo2', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'n94upy7yd47', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'bvg41neww0r', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'z459apnn13m', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '2t3sx3nedi1', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '88zf1r5n5ej', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 't3xqeq01rdj', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'c1i3hb0bu0f', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'fbgaaj9td5t', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'dhdum7zv2wv', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'vl8ub53ikv4', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'rzmt46s0nfk', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '7ywukcy6ce7', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'u1914umcr4j', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'cvv09ko8h3e', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'o06ez3hp081', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'dhdv3671m57', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'u1ero8piyk0', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'nonwnta1tpk', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '46nco16r81v', + 'x-async': false, + 'x-index': 1, + }, + bpfpuc2t2tw: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-app-version': '1.3.25-beta', + properties: { + name: { + _isJSONSchemaObject: true, + version: '2.0', + 'x-collection-field': 'testRefresh.name', + 'x-component': 'CollectionField', + 'x-component-props': { + ellipsis: true, + }, + 'x-read-pretty': true, + 'x-decorator': null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': '1.3.25-beta', + 'x-uid': '0dq29e4kbly', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'l1df5slauzp', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '6ydl9ber22f', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'xczcehxr2m5', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'hwa7l2ppe6l', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '54nsmh38l7k', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'tcwxtcalzkm', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '5g1v6hmn04q', + 'x-async': true, + 'x-index': 1, + }, +}; + +export const createFormSubmit = { + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + 'x-app-version': '1.4.0-alpha', + properties: { + c9ar217262w: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + 'x-app-version': '1.4.0-alpha', + properties: { + '943651fwz8t': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + g9fq98c2s4n: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + '4d3afoagc5g': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.4.0-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.4.0-alpha', + properties: { + afp94e3l423: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-action': 'create', + 'x-acl-action': 'create', + title: "{{t('Add new')}}", + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:addNew', + 'x-component': 'Action', + 'x-decorator': 'ACLActionProvider', + 'x-component-props': { + openMode: 'drawer', + type: 'primary', + component: 'CreateRecordAction', + icon: 'PlusOutlined', + }, + 'x-action-context': { + dataSource: 'main', + collection: 'users', + }, + 'x-align': 'right', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-app-version': '1.4.0-alpha', + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Add record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + 'x-app-version': '1.4.0-alpha', + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + 'x-initializer-props': { + gridInitializer: 'popup:addNew:addBlock', + }, + 'x-app-version': '1.4.0-alpha', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Add new")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:addNew:addBlock', + 'x-app-version': '1.4.0-alpha', + properties: { + umjudxvb3ri: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + to6yrk48a1e: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + zfuiyepfpo8: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-acl-action': 'users:create', + 'x-decorator': 'FormBlockProvider', + 'x-use-decorator-props': 'useCreateFormBlockDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + collection: 'users', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:createForm', + 'x-component': 'CardItem', + 'x-app-version': '1.4.0-alpha', + properties: { + '67zq3yb6rg0': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useCreateFormBlockProps', + 'x-app-version': '1.4.0-alpha', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'tyztr48d01m', + 'x-async': false, + 'x-index': 1, + }, + '1cslc0bzwan': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'createForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + }, + 'x-app-version': '1.4.0-alpha', + properties: { + rjfxzva092d: { + _isJSONSchemaObject: true, + version: '2.0', + title: '{{ t("Submit") }}', + 'x-action': 'submit', + 'x-component': 'Action', + 'x-use-component-props': 'useCreateActionProps', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:createSubmit', + 'x-component-props': { + type: 'primary', + htmlType: 'submit', + }, + 'x-action-settings': { + triggerWorkflows: [], + }, + type: 'void', + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'jpbq9zyexb9', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'xsv37mjii6o', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '2j67lyyth1f', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'zcli14m52d6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '63hxohd7td3', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'm6uu6mtlopn', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'x5nqkk6whbz', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'kaxuorp0kwz', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'qka0657zo5s', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '857bxboz38t', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'qxh2sf5itxw', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '3nsvi44j20t', + 'x-async': false, + 'x-index': 1, + }, + bk1pbg52rcs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.4.0-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.4.0-alpha', + properties: { + '9tql6edotky': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'zrlg6n04kd0', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'uv7q2tnh9mc', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'g76idw88ik8', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'h0mnhu6ihsd', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '3xdo6vt2c07', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'et8gznfo7r7', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '18w03j8qcv6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'qvl1ar53zpz', + 'x-async': true, + 'x-index': 1, + }, +}; diff --git a/packages/core/client/src/modules/actions/save-record/customizeSaveRecordActionSettings.tsx b/packages/core/client/src/modules/actions/save-record/customizeSaveRecordActionSettings.tsx index 87eaf97dcd4..16590012289 100644 --- a/packages/core/client/src/modules/actions/save-record/customizeSaveRecordActionSettings.tsx +++ b/packages/core/client/src/modules/actions/save-record/customizeSaveRecordActionSettings.tsx @@ -52,10 +52,6 @@ export const customizeSaveRecordActionSettings = new SchemaSettings({ { name: 'afterSuccessfulSubmission', Component: AfterSuccess, - useVisible() { - const fieldSchema = useFieldSchema(); - return isValid(fieldSchema?.['x-action-settings']?.onSuccess); - }, }, { name: 'bindWorkflow', diff --git a/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx b/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx index a7756c36f3f..ee1373e4e00 100644 --- a/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx +++ b/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx @@ -28,6 +28,8 @@ import { } from '../../../schema-component/antd/action/Action.Designer'; import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks/useCollectionState'; import { SchemaSettingsModalItem } from '../../../schema-settings/SchemaSettings'; +import { useParentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider'; +import { useDataBlockProps } from '../../../data-source'; const Tree = connect( AntdTree, @@ -163,6 +165,10 @@ export const createSubmitActionSettings = new SchemaSettings({ { name: 'saveMode', Component: SaveMode, + useVisible() { + const { type } = useDataBlockProps() || ({} as any); + return type !== 'publicForm'; + }, }, { name: 'assignFieldValues', @@ -176,8 +182,8 @@ export const createSubmitActionSettings = new SchemaSettings({ name: 'afterSuccessfulSubmission', Component: AfterSuccess, useVisible() { - const fieldSchema = useFieldSchema(); - return isValid(fieldSchema?.['x-action-settings']?.onSuccess); + const { type } = useDataBlockProps() || ({} as any); + return type !== 'publicForm'; }, }, { @@ -188,6 +194,10 @@ export const createSubmitActionSettings = new SchemaSettings({ isPopupAction: false, }; }, + useVisible() { + const parentRecord = useParentPopupRecord(); + return !!parentRecord; + }, }, { name: 'remove', diff --git a/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx b/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx index a22fbaf4d20..1ec8816445c 100644 --- a/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx +++ b/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx @@ -71,10 +71,6 @@ export const updateSubmitActionSettings = new SchemaSettings({ { name: 'afterSuccessfulSubmission', Component: AfterSuccess, - useVisible() { - const fieldSchema = useFieldSchema(); - return isValid(fieldSchema?.['x-action-settings']?.onSuccess); - }, }, { name: 'refreshDataBlockRequest', diff --git a/packages/core/client/src/modules/actions/update-record/UpdateRecordActionInitializer.tsx b/packages/core/client/src/modules/actions/update-record/UpdateRecordActionInitializer.tsx index eb4f6640c73..80afad6a8d9 100644 --- a/packages/core/client/src/modules/actions/update-record/UpdateRecordActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/update-record/UpdateRecordActionInitializer.tsx @@ -25,7 +25,7 @@ export const UpdateRecordActionInitializer = (props) => { 'x-action-settings': { assignedValues: {}, onSuccess: { - manualClose: true, + manualClose: false, redirecting: false, successMessage: '{{t("Updated successfully")}}', }, diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaInitializer.test.ts index 93a59a41ec0..286fc202429 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaInitializer.test.ts @@ -76,9 +76,7 @@ test.describe('configure fields', () => { await expect( page.getByLabel('block-item-CollectionField-general-details-general.singleSelect-Single select'), ).toBeVisible(); - await expect( - page.getByLabel('block-item-CollectionField-general-details-general.manyToOne.nickname'), - ).toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-details-users.nickname-Nickname')).toBeVisible(); // delete fields await formItemInitializer.hover(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/ReadPrettyFormItemInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/details-single/ReadPrettyFormItemInitializers.tsx index b47af5d54ea..1386471f38d 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-single/ReadPrettyFormItemInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/ReadPrettyFormItemInitializers.tsx @@ -85,6 +85,9 @@ const commonOptions = { type: 'void', 'x-editable': false, 'x-decorator': 'FormItem', + 'x-decorator-props': { + engine: 'handlebars', + }, // 'x-designer': 'Markdown.Void.Designer', 'x-toolbar': 'FormItemSchemaToolbar', 'x-settings': 'blockSettings:markdown', diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/RecordReadPrettyFormBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/details-single/RecordReadPrettyFormBlockInitializer.tsx index fc35bfbd83f..89519efc310 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-single/RecordReadPrettyFormBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/RecordReadPrettyFormBlockInitializer.tsx @@ -13,7 +13,7 @@ import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } import { useCollection_deprecated } from '../../../../collection-manager'; import { useAssociationName, useCollectionManager } from '../../../../data-source'; import { useRecordCollectionDataSourceItems } from '../../../../schema-initializer/utils'; -import { useSchemaTemplateManager } from '../../../../schema-templates'; +import { useBlockTemplateContext, useSchemaTemplateManager } from '../../../../schema-templates'; import { createDetailsUISchema } from './createDetailsUISchema'; export const RecordReadPrettyFormBlockInitializer = () => { @@ -38,11 +38,12 @@ export function useCreateSingleDetailsSchema() { const { getTemplateSchemaByMode } = useSchemaTemplateManager(); const association = useAssociationName(); const cm = useCollectionManager(); + const { componentNamePrefix } = useBlockTemplateContext(); const templateWrap = useCallback( (templateSchema, options) => { const { item } = options; - if (item.template.componentName === 'ReadPrettyFormItem') { + if (item.template.componentName === `${componentNamePrefix}ReadPrettyFormItem`) { const collectionName = item.collectionName || item.name; const collection = cm.getCollection(collectionName); const blockSchema = createDetailsUISchema( @@ -69,7 +70,7 @@ export function useCreateSingleDetailsSchema() { return templateSchema; } }, - [association, cm], + [association, cm, componentNamePrefix], ); const createSingleDetailsSchema = useCallback( diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/RecordFormBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/RecordFormBlockInitializer.tsx index b9fc36213fc..ac496ce3ff4 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/RecordFormBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/form/RecordFormBlockInitializer.tsx @@ -13,7 +13,7 @@ import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } import { useCollection_deprecated } from '../../../../collection-manager'; import { useAssociationName, useCollectionManager } from '../../../../data-source'; import { useRecordCollectionDataSourceItems } from '../../../../schema-initializer/utils'; -import { useSchemaTemplateManager } from '../../../../schema-templates'; +import { useBlockTemplateContext, useSchemaTemplateManager } from '../../../../schema-templates'; import { createEditFormBlockUISchema } from './createEditFormBlockUISchema'; /** @@ -50,6 +50,7 @@ export function useCreateEditFormBlock() { const { insert } = useSchemaInitializer(); const association = useAssociationName(); const cm = useCollectionManager(); + const { componentNamePrefix } = useBlockTemplateContext(); const createEditFormBlock = useCallback( ({ item }) => { @@ -76,7 +77,7 @@ export function useCreateEditFormBlock() { const templateWrap = useCallback( (templateSchema, { item }) => { - if (item.template.componentName === 'FormItem') { + if (item.template.componentName === `${componentNamePrefix}FormItem`) { const collectionName = item.collectionName || item.name; const collection = cm.getCollection(collectionName); const blockSchema = createEditFormBlockUISchema( @@ -102,7 +103,7 @@ export function useCreateEditFormBlock() { return templateSchema; } }, - [association, cm], + [association, cm, componentNamePrefix], ); return { createEditFormBlock, templateWrap }; diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaInitializer.test.ts index 619e4f45bb2..4ad63921c9a 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaInitializer.test.ts @@ -68,7 +68,7 @@ test.describe('configure fields', () => { await page.mouse.move(300, 0); await expect(page.getByLabel('block-item-CollectionField-general-form-general.id-ID')).toBeVisible(); - await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne.nickname')).toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-users.nickname-Nickname')).toBeVisible(); // delete fields await page.getByLabel('schema-initializer-Grid-form:configureFields-general').hover(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings.test.ts index 59843c85134..1756f341215 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings.test.ts @@ -76,6 +76,10 @@ test.describe('set default value', () => { await expect( page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), ).toHaveValue('new value'); + + // 再次打开设置默认值的弹窗,变量输入框应该显示设置的变量值 + await openDialog('longText'); + await expect(page.getByLabel('block-item-VariableInput-')).toHaveText(/Current form \/ singleLineText/); }); test('subform: basic fields', async ({ page, mockPage }) => { diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/bulkEditForm.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/bulkEditForm.test.ts index ab3a456550b..d4d5c943517 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/bulkEditForm.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/bulkEditForm.test.ts @@ -26,7 +26,10 @@ test.describe('bulk edit form', () => { // 2. 打开弹窗,显示出批量编辑表单 await page.getByLabel('action-Action-Bulk edit-').click(); - // 默认为 "Changed to" 模式,此时应该显示字段是必填的 + await page.getByLabel('block-item-BulkEditField-').click(); + await page.getByRole('option', { name: 'Changed to' }).click(); + + // "Changed to" 模式,此时应该显示字段是必填的 await expect(page.getByLabel('block-item-BulkEditField-').getByText('*')).toBeVisible(); // 3. 输入值,点击提交 @@ -45,7 +48,10 @@ test.describe('bulk edit form', () => { // 1. 打开弹窗,显示出批量编辑表单 await page.getByLabel('action-Action-Bulk edit-').click(); - // 默认为 "Changed to" 模式,此时应该显示字段是必填的 + await page.getByLabel('block-item-BulkEditField-').click(); + await page.getByRole('option', { name: 'Changed to' }).click(); + + // "Changed to" 模式,此时应该显示字段是必填的 await expect(page.getByLabel('block-item-BulkEditField-').getByText('*')).toBeVisible(); // 2. 切换为其它模式,此时应该不显示字段是必填的 diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts index d824e98d98d..40ea937b177 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts @@ -9,6 +9,7 @@ import { expect, oneEmptyTableBlockWithActions, test } from '@nocobase/test/e2e'; import { T3848 } from '../../../details-single/__e2e__/templatesOfBug'; +import { addAssociationFields } from './templatesOfBug'; test.describe('where edit form block can be added', () => { test('popup', async ({ page, mockPage, mockRecord }) => { @@ -69,4 +70,30 @@ test.describe('where edit form block can be added', () => { test.describe('configure actions', () => {}); -test.describe('configure fields', () => {}); +test.describe('configure fields', () => { + test('add association fields', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(addAssociationFields).waitForInit(); + const record = await mockRecord('general', 3); + await nocoPage.goto(); + + // Create association fields for the first, second, and third levels respectively, and assert whether the values are correct + await page.getByLabel('action-Action.Link-Edit-').first().click(); + await page.getByLabel('schema-initializer-Grid-form:').hover(); + await page.getByRole('menuitem', { name: 'manyToOne1', exact: true }).click(); + await page.getByRole('menuitem', { name: 'manyToOne1 right' }).hover(); + await page.getByRole('menuitem', { name: 'manyToOne2', exact: true }).click(); + await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover(); + await page.getByRole('menuitem', { name: 'manyToOne3' }).click(); + await page.mouse.move(600, 0); + + await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne1-manyToOne1')).toHaveText( + `manyToOne1:${record.manyToOne1.id}`, + ); + await expect( + page.getByLabel('block-item-CollectionField-general-form-targetCollection1.manyToOne2-manyToOne2'), + ).toHaveText(`manyToOne2:${record.manyToOne1.manyToOne2.id}`); + await expect( + page.getByLabel('block-item-CollectionField-general-form-targetCollection2.manyToOne3-manyToOne3'), + ).toHaveText(`manyToOne3:${record.manyToOne1.manyToOne2.manyToOne3.id}`); + }); +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaSettings.test.ts index 9e0a667471a..7d8da954dd8 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaSettings.test.ts @@ -163,11 +163,9 @@ test.describe('edit form block schema settings', () => { await page.getByLabel('action-Action.Link-Edit record-update-general-table-').click(); await page.getByRole('spinbutton').fill(''); await page.getByRole('spinbutton').fill('10'); - await expect( - page - .getByLabel('block-item-CollectionField-general-form-general.formula-formula') - .locator('.nb-read-pretty-input-number'), - ).toHaveText('11'); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.formula-formula')).toHaveText( + 'formula:11', + ); await page.getByLabel('drawer-Action.Container-general-Edit record-mask').click(); await expect(page.getByText('Unsaved changes')).toBeVisible(); }); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts index b41e4fe9474..3709035b629 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts @@ -1111,3 +1111,370 @@ export const T3924: PageConfig = { 'x-index': 1, }, }; +export const addAssociationFields = { + collections: [ + { + name: 'general', + fields: [ + { + interface: 'm2o', + name: 'manyToOne1', + target: 'targetCollection1', + }, + { + interface: 'input', + name: 'generalText', + }, + ], + }, + { + name: 'targetCollection1', + fields: [ + { + interface: 'm2o', + name: 'manyToOne2', + target: 'targetCollection2', + }, + { + interface: 'input', + name: 'targetCollection1Text', + }, + ], + }, + { + name: 'targetCollection2', + fields: [ + { + interface: 'm2o', + name: 'manyToOne3', + target: 'emptyCollection', + }, + { + interface: 'input', + name: 'targetCollection2Text', + }, + ], + }, + { + name: 'emptyCollection', + }, + ], + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + '6d3i9i6e7vg': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: { + krwfqgbq4fy: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.19-beta', + properties: { + js8p7716v8y: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.19-beta', + properties: { + nwk3otm0wbu: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'general:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'general', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.3.19-beta', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.3.19-beta', + 'x-uid': 'wkkuk2ca9oz', + 'x-async': false, + 'x-index': 1, + }, + s00slxnrdog: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.3.19-beta', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.3.19-beta', + properties: { + mpthybxwhi6: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.3.19-beta', + properties: { + yp1u36754xm: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Edit") }}', + 'x-action': 'update', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:edit', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'drawer', + icon: 'EditOutlined', + }, + 'x-action-context': { + dataSource: 'main', + collection: 'general', + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Edit record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Edit")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:common:addBlock', + properties: { + dhu7la398bc: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.19-beta', + properties: { + ex9i4u2sm9o: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.19-beta', + properties: { + gdna46r5948: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: false, + }, + 'x-acl-action': 'general:update', + 'x-decorator': 'FormBlockProvider', + 'x-use-decorator-props': + 'useEditFormBlockDecoratorProps', + 'x-decorator-props': { + action: 'get', + dataSource: 'main', + collection: 'general', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:editForm', + 'x-component': 'CardItem', + 'x-app-version': '1.3.19-beta', + properties: { + q7vswewc9bk: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useEditFormBlockProps', + 'x-app-version': '1.3.19-beta', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + 'x-app-version': '1.3.19-beta', + 'x-uid': 'fu2wc0gvxg7', + 'x-async': false, + 'x-index': 1, + }, + jvi33jag3zs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'editForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + }, + 'x-app-version': '1.3.19-beta', + 'x-uid': 'zox6jjontst', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'e91gqbinrdp', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'd2lh71p96uu', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'e5pfc9sit0l', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '50qpb5g4zcw', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '3u9vcm0gftv', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '59ihnv5o5u5', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '7i9nnjy10pf', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'kov4btsbs2c', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '4fixo409z6d', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'kacm537dm0m', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'agw5b9adl6m', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '85dhumz9qp7', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '300ltjt2u3c', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ogjd21lszel', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'fn1knfs54ow', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'im83gpz4p8l', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'tir06tp0oyx', + 'x-async': true, + 'x-index': 1, + }, +}; diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-view/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-view/schemaSettings.test.ts index 6fdd933a25d..7628c37f7d5 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-view/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-view/schemaSettings.test.ts @@ -7,7 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { expect, commonFormViewPage, test } from '@nocobase/test/e2e'; +import { commonFormViewPage, expect, test } from '@nocobase/test/e2e'; test.describe('field schema settings', () => { test('linkage style color', async ({ page, mockPage, mockRecord }) => { @@ -60,4 +60,32 @@ test.describe('field schema settings', () => { .locator('div.ant-formily-item-control-content-component'); await expect(cell).toHaveCSS('background-color', 'rgb(163, 79, 204)'); }); + + test('text align', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(commonFormViewPage).waitForInit(); + await mockRecord('general', { singleLineText: 'asdfcedg' }); + await nocoPage.goto(); + await page.getByText('asdfcedg', { exact: true }).hover(); + await page.getByLabel('block-item-CollectionField-general-details-general.singleLineText-singleLineText').hover(); + + await page + .getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-general-general.singleLineText', { + exact: true, + }) + .click(); + await page.getByRole('menuitem', { name: 'Style' }).click(); + await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); + await page.getByText('Add property').click(); + await page.getByTestId('select-linkage-properties').click(); + await page.getByText('Text Align', { exact: true }).click(); + await page.getByTestId('select-single').click(); + await page.getByRole('option', { name: 'right' }).click(); + await page.getByRole('button', { name: 'OK' }).click(); + + const cell = page + .getByLabel('block-item-CollectionField-general-details-general.singleLineText-singleLineText') + .locator('div.ant-formily-item-control-content-component'); + + await expect(cell).toHaveCSS('text-align', 'right'); + }); }); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__tests__/fieldSettingsFormItem.test.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/__tests__/fieldSettingsFormItem.test.tsx index d6b1e2fa09a..db40bac6472 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__tests__/fieldSettingsFormItem.test.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__tests__/fieldSettingsFormItem.test.tsx @@ -99,7 +99,7 @@ describe('FieldSettingsFormItem', () => { describe('menu list', () => { describe('edit mode', () => { - it('common field', async () => { + it.skip('common field', async () => { await renderSettings(commonFieldOptions()); await checkSettings( @@ -213,7 +213,7 @@ describe('FieldSettingsFormItem', () => { }); describe('read pretty mode', () => { - it('common field', async () => { + it.skip('common field', async () => { await renderReadPrettySettings(commonFieldOptions()); await checkSettings( @@ -406,7 +406,7 @@ describe('FieldSettingsFormItem', () => { ]); }); - test('Set default value', async () => { + test.skip('Set default value', async () => { await renderSettings(commonFieldOptions()); const newValue = 'new test'; @@ -432,7 +432,7 @@ describe('FieldSettingsFormItem', () => { ]); }); - test('Pattern', async () => { + test.skip('Pattern', async () => { await renderSettings(associationFieldOptions()); await checkSettings([ @@ -464,7 +464,7 @@ describe('FieldSettingsFormItem', () => { ]); }); - test('EditValidationRules', async () => { + test.skip('EditValidationRules', async () => { await renderSingleSettings(commonFieldOptions(true)); await checkSettings([ { diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/createFormActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/createFormActionInitializers.tsx index 1cc415fa806..689c87e4f9e 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/createFormActionInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/form/createFormActionInitializers.tsx @@ -8,6 +8,7 @@ */ import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer'; +import { useDataBlockProps } from '../../../../data-source'; const commonOptions = { title: '{{t("Configure actions")}}', @@ -25,6 +26,10 @@ const commonOptions = { name: 'customRequest', title: '{{t("Custom request")}}', Component: 'CustomRequestInitializer', + useVisible() { + const { type } = useDataBlockProps() || ({} as any); + return type !== 'publicForm'; + }, }, ], }; diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/fieldSettingsFormItem.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/fieldSettingsFormItem.tsx index 3c8e65bfe2a..522a0fcaf23 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/fieldSettingsFormItem.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/form/fieldSettingsFormItem.tsx @@ -6,29 +6,30 @@ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ -import React from 'react'; import { ArrayCollapse, FormLayout } from '@formily/antd-v5'; import { Field } from '@formily/core'; import { ISchema, useField, useFieldSchema } from '@formily/react'; import _ from 'lodash'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { useApp, useSchemaToolbar } from '../../../../application'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider'; import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../../collection-manager'; -import { useCollection } from '../../../../data-source'; import { useFieldComponentName } from '../../../../common/useFieldComponentName'; +import { useCollection } from '../../../../data-source'; +import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem'; import { useDesignable, useValidateSchema } from '../../../../schema-component'; -import { useIsFormReadPretty } from '../../../../schema-component/antd/form-item/FormItem.Settings'; -import { getTempFieldState } from '../../../../schema-component/antd/form-v2/utils'; -import { isPatternDisabled } from '../../../../schema-settings'; +import { + useIsFieldReadPretty, + useIsFormReadPretty, +} from '../../../../schema-component/antd/form-item/FormItem.Settings'; +import { SchemaSettingsLinkageRules, isPatternDisabled } from '../../../../schema-settings'; +import { useIsAllowToSetDefaultValue } from '../../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; +import { getTempFieldState } from '../../../../schema-settings/LinkageRules/bindLinkageRulesToFiled'; import { ActionType } from '../../../../schema-settings/LinkageRules/type'; import { SchemaSettingsDefaultValue } from '../../../../schema-settings/SchemaSettingsDefaultValue'; -import { useIsAllowToSetDefaultValue } from '../../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; -import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem'; -import { SchemaSettingsLinkageRules } from '../../../../schema-settings'; -import { useIsFieldReadPretty } from '../../../../schema-component/antd/form-item/FormItem.Settings'; export const fieldSettingsFormItem = new SchemaSettings({ name: 'fieldSettings:FormItem', items: [ diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockProps.ts b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockProps.ts index b5881055eca..b14581d866b 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockProps.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockProps.ts @@ -10,5 +10,5 @@ import { useFormBlockProps } from '../../../../../block-provider/FormBlockProvider'; export function useEditFormBlockProps() { - return useFormBlockProps(true); + return useFormBlockProps(); } diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardActionInitializers.tsx index d47dc645fd9..614bb6d0db5 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardActionInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardActionInitializers.tsx @@ -8,7 +8,6 @@ */ import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer'; -import { useCollection } from '../../../../data-source'; import { useActionAvailable } from '../../useActionAvailable'; const commonOptions = { title: "{{t('Configure actions')}}", @@ -73,6 +72,14 @@ const commonOptions = { }, }, }, + { + name: 'customRequest', + title: '{{t("Custom request")}}', + Component: 'CustomRequestInitializer', + schema: { + 'x-action': 'customize:table:request:global', + }, + }, ], }; diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts index 8790f8e3662..1cc78d4eec1 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts @@ -159,7 +159,7 @@ test.describe('configure fields', () => { await page.mouse.move(300, 0); await expect(page.getByLabel('block-item-CollectionField-general-grid-card-general.id-ID').first()).toBeVisible(); await expect( - page.getByLabel('block-item-CollectionField-general-grid-card-general.manyToOne.nickname').first(), + page.getByLabel('block-item-CollectionField-general-grid-card-users.nickname-Nickname').first(), ).toBeVisible(); // delete fields diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/ListActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/list/ListActionInitializers.tsx index cbc1ac84186..97421222f78 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/list/ListActionInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/list/ListActionInitializers.tsx @@ -8,7 +8,6 @@ */ import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer'; -import { useCollection } from '../../../../data-source'; import { useActionAvailable } from '../../useActionAvailable'; const commonOptions = { @@ -74,6 +73,14 @@ const commonOptions = { }, }, }, + { + name: 'customRequest', + title: '{{t("Custom request")}}', + Component: 'CustomRequestInitializer', + schema: { + 'x-action': 'customize:table:request:global', + }, + }, ], }; diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts index 07858f5e616..d36800be189 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts @@ -157,7 +157,7 @@ test.describe('configure fields', () => { await page.mouse.move(300, 0); await expect(page.getByLabel('block-item-CollectionField-general-list-general.id-ID').first()).toBeVisible(); await expect( - page.getByLabel('block-item-CollectionField-general-list-general.manyToOne.nickname').first(), + page.getByLabel('block-item-CollectionField-general-list-users.nickname-Nickname').first(), ).toBeVisible(); // delete fields diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/listBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/list/listBlockSettings.ts index 9a572040c31..8c0f8d5e636 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/list/listBlockSettings.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/list/listBlockSettings.ts @@ -187,11 +187,12 @@ export const listBlockSettings = new SchemaSettings({ title: t('Records per page'), value: field.decoratorProps?.params?.pageSize || 20, options: [ + { label: '5', value: 5 }, { label: '10', value: 10 }, { label: '20', value: 20 }, { label: '50', value: 50 }, - { label: '80', value: 80 }, { label: '100', value: 100 }, + { label: '200', value: 200 }, ], onChange: (pageSize) => { _.set(fieldSchema, 'x-decorator-props.params.pageSize', pageSize); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table-selector/tableSelectorBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/table-selector/tableSelectorBlockSettings.ts index 1cf878c7056..6be42ae99b4 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table-selector/tableSelectorBlockSettings.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table-selector/tableSelectorBlockSettings.ts @@ -251,6 +251,7 @@ export const tableSelectorBlockSettings = new SchemaSettings({ title: t('Records per page'), value: field.decoratorProps?.params?.pageSize || 20, options: [ + { label: '5', value: 5 }, { label: '10', value: 10 }, { label: '20', value: 20 }, { label: '50', value: 50 }, diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/TableActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/TableActionInitializers.tsx index b8e8027ab9b..c31d340ede9 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/TableActionInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/TableActionInitializers.tsx @@ -90,6 +90,14 @@ const commonOptions = { return collection.tree && treeTable; }, }, + { + name: 'customRequest', + title: '{{t("Custom request")}}', + Component: 'CustomRequestInitializer', + schema: { + 'x-action': 'customize:table:request:global', + }, + }, ], }; diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/linkageStyleSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/linkageStyleSettings.test.ts index 350073ebbf6..5ec289de963 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/linkageStyleSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/linkageStyleSettings.test.ts @@ -57,4 +57,29 @@ test.describe('view', () => { const bgColor = await cell.evaluate((el) => getComputedStyle(el.parentElement).backgroundColor); expect(bgColor).toContain('163, 79, 204'); }); + + test('text align', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneTableBlockWithIntegerAndIDColumn).waitForInit(); + await mockRecord('general', { integer: '423' }); + await nocoPage.goto(); + await page.getByText('integer', { exact: true }).hover(); + + await page + .getByRole('button', { + name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-general', + }) + .click(); + await page.getByRole('menuitem', { name: 'Style' }).click(); + await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); + await page.getByText('Add property').click(); + await page.getByTestId('select-linkage-properties').click(); + await page.getByText('Text Align', { exact: true }).click(); + await page.getByTestId('select-single').click(); + await page.getByRole('option', { name: 'right' }).click(); + await page.getByRole('button', { name: 'OK' }).click(); + + const cell = page.getByRole('button', { name: '423' }); + const textAlign = await cell.evaluate((el) => getComputedStyle(el.parentElement).textAlign); + expect(textAlign).toContain('right'); + }); }); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts index a1be4dbb8ba..1c281c1b0aa 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts @@ -8,6 +8,7 @@ */ import { Page, expect, oneEmptyTable, test } from '@nocobase/test/e2e'; +import { addAssociationFields } from '../../form/__e2e__/form-edit/templatesOfBug'; import { oneTableWithInheritFields } from './templatesOfBug'; const deleteButton = async (page: Page, name: string) => { @@ -113,6 +114,141 @@ test.describe('configure columns', () => { await expect(page.getByRole('button', { name: 'Nickname', exact: true })).not.toBeVisible(); }); + test('multiple depths of association fields', async ({ page, mockPage, mockRecord, mockRecords }) => { + const nocoPage = await mockPage(addAssociationFields).waitForInit(); + + // The purpose here is to make the IDs in the same row different + await mockRecords('targetCollection1', 2, 0); + await mockRecords('targetCollection2', 1, 0); + + const record = await mockRecord('general', 3); + await nocoPage.goto(); + + // 1. Create association fields for the first, second, and third levels + await page.getByLabel('schema-initializer-TableV2-').hover(); + await page.getByRole('menuitem', { name: 'manyToOne1', exact: true }).click(); + await page.getByRole('menuitem', { name: 'manyToOne1 right' }).hover(); + await page.getByRole('menuitem', { name: 'manyToOne2', exact: true }).click(); + await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover(); + await page.getByRole('menuitem', { name: 'manyToOne3' }).click(); + await page.mouse.move(600, 0); + + // 2. Click on the association field, create a details block in the popup, display the ID field, and assert if it's correct + await page + .getByRole('button', { name: `${record.manyToOne1.id}`, exact: true }) + .getByText(record.manyToOne1.id) + .click(); + await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'table Details right' }).hover(); + await page.getByRole('menuitem', { name: 'Current record' }).click(); + await page.getByLabel('schema-initializer-Grid-details:configureFields-targetCollection1').hover(); + await page.getByRole('menuitem', { name: 'ID', exact: true }).click(); + await page.mouse.move(600, 0); + await expect(page.getByLabel('block-item-CollectionField-')).toHaveText(`ID:${record.manyToOne1.id}`); + await page.getByLabel('drawer-AssociationField.Viewer-targetCollection1-View record-mask').click(); + + await page + .getByRole('button', { name: `${record.manyToOne1.manyToOne2.id}`, exact: true }) + .getByText(record.manyToOne1.manyToOne2.id) + .click(); + await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'table Details right' }).hover(); + await page.getByRole('menuitem', { name: 'Current record' }).click(); + await page.getByLabel('schema-initializer-Grid-details:configureFields-targetCollection2').hover(); + await page.getByRole('menuitem', { name: 'ID', exact: true }).click(); + await page.mouse.move(600, 0); + await expect(page.getByLabel('block-item-CollectionField-')).toHaveText(`ID:${record.manyToOne1.manyToOne2.id}`); + await page.getByLabel('drawer-AssociationField.Viewer-targetCollection2-View record-mask').click(); + + await page + .getByRole('button', { name: `${record.manyToOne1.manyToOne2.manyToOne3.id}`, exact: true }) + .getByText(record.manyToOne1.manyToOne2.manyToOne3.id) + .click(); + await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'table Details right' }).hover(); + await page.getByRole('menuitem', { name: 'Current record' }).click(); + await page.getByLabel('schema-initializer-Grid-details:configureFields-emptyCollection').hover(); + await page.getByRole('menuitem', { name: 'ID', exact: true }).click(); + await page.mouse.move(600, 0); + await expect(page.getByLabel('block-item-CollectionField-')).toHaveText( + `ID:${record.manyToOne1.manyToOne2.manyToOne3.id}`, + ); + await page.getByLabel('drawer-AssociationField.Viewer-emptyCollection-View record-mask').click(); + + // 测试详情区块的关系字段 + // 1. 点击行操作按钮打开弹窗,创建一个详情区块,并配置第一、二、三级关系字段 + await page.getByLabel('action-Action.Link-Edit-').first().click(); + await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'table Details right' }).hover(); + await page.getByRole('menuitem', { name: 'Current record' }).click(); + await page.getByLabel('schema-initializer-Grid-details:configureFields-general').hover(); + await page.getByRole('menuitem', { name: 'manyToOne1', exact: true }).click(); + await page.getByRole('menuitem', { name: 'manyToOne1 right' }).hover(); + await page.getByRole('menuitem', { name: 'manyToOne2', exact: true }).click(); + await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover(); + await page.getByRole('menuitem', { name: 'manyToOne3' }).click(); + await page.mouse.move(600, 0); + + // 2. 点击每一个关系字段,创建一个详情区块,显示 ID 字段,断言 ID 是否正确 + await page + .getByLabel('block-item-CollectionField-general-details-general.manyToOne1-manyToOne1') + .getByText(String(record.manyToOne1.id), { exact: true }) + .click(); + await page + .getByTestId('drawer-AssociationField.Viewer-targetCollection1-View record') + .getByLabel('schema-initializer-Grid-popup') + .hover(); + await page.getByRole('menuitem', { name: 'table Details right' }).hover(); + await page.getByRole('menuitem', { name: 'Current record' }).click(); + await page.getByLabel('schema-initializer-Grid-details:configureFields-targetCollection1').hover(); + await page.getByRole('menuitem', { name: 'ID', exact: true }).click(); + await expect( + page + .getByTestId('drawer-AssociationField.Viewer-targetCollection1-View record') + .getByLabel('block-item-CollectionField-'), + ).toHaveText(`ID:${record.manyToOne1.id}`); + await page.getByLabel('drawer-AssociationField.Viewer-targetCollection1-View record-mask').click(); + + // another field + await page + .getByLabel('block-item-CollectionField-general-details-targetCollection1.manyToOne2-') + .getByText(String(record.manyToOne1.manyToOne2.id), { exact: true }) + .click(); + await page + .getByTestId('drawer-AssociationField.Viewer-targetCollection2-View record') + .getByLabel('schema-initializer-Grid-popup') + .hover(); + await page.getByRole('menuitem', { name: 'table Details right' }).hover(); + await page.getByRole('menuitem', { name: 'Current record' }).click(); + await page.getByLabel('schema-initializer-Grid-details:configureFields-targetCollection2').hover(); + await page.getByRole('menuitem', { name: 'ID', exact: true }).click(); + await expect( + page + .getByTestId('drawer-AssociationField.Viewer-targetCollection2-View record') + .getByLabel('block-item-CollectionField-'), + ).toHaveText(`ID:${record.manyToOne1.manyToOne2.id}`); + await page.getByLabel('drawer-AssociationField.Viewer-targetCollection2-View record-mask').click(); + + // another field + await page + .getByLabel('block-item-CollectionField-general-details-targetCollection2.manyToOne3-') + .getByText(String(record.manyToOne1.manyToOne2.manyToOne3.id), { exact: true }) + .click(); + await page + .getByTestId('drawer-AssociationField.Viewer-emptyCollection-View record') + .getByLabel('schema-initializer-Grid-popup') + .hover(); + await page.getByRole('menuitem', { name: 'table Details right' }).hover(); + await page.getByRole('menuitem', { name: 'Current record' }).click(); + await page.getByLabel('schema-initializer-Grid-details:configureFields-emptyCollection').hover(); + await page.getByRole('menuitem', { name: 'ID', exact: true }).click(); + await expect( + page + .getByTestId('drawer-AssociationField.Viewer-emptyCollection-View record') + .getByLabel('block-item-CollectionField-'), + ).toHaveText(`ID:${record.manyToOne1.manyToOne2.manyToOne3.id}`); + }); + test.pgOnly('display inherit fields', async ({ page, mockPage, mockRecord }) => { const nocoPage = await mockPage(oneTableWithInheritFields).waitForInit(); const record = await mockRecord('child'); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts index 022dba03f7e..cfbfd1bc41b 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts @@ -843,7 +843,6 @@ test.describe('actions schema settings', () => { const expectNewValue = async (value: string) => { await page.getByLabel('action-Action.Link-Update record-customize:update-users2-table-0').click(); - await page.getByRole('button', { name: 'OK', exact: true }).click(); await page.getByLabel('action-Action-Refresh-refresh').click(); await expect(page.getByLabel('block-item-CardItem-users2-').getByText(value)).toBeVisible(); }; diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx index bd5dbad0232..f9ab661981f 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx @@ -16,7 +16,6 @@ import { findFilterTargets } from '../../../../../block-provider/hooks'; import { DataBlock, useFilterBlock } from '../../../../../filter-provider/FilterProvider'; import { mergeFilter } from '../../../../../filter-provider/utils'; import { removeNullCondition } from '../../../../../schema-component'; -import { useCollection } from '../../../../../data-source'; export const useTableBlockProps = () => { const field = useField(); @@ -56,11 +55,12 @@ export const useTableBlockProps = () => { loading: ctx?.service?.loading, showIndex: ctx.showIndex, dragSort: ctx.dragSort && ctx.dragSortBy, - rowKey: ctx.rowKey || 'id', + rowKey: ctx.rowKey || fieldSchema?.['x-component-props']?.rowKey || 'id', pagination: fieldSchema?.['x-component-props']?.pagination === false ? false : field.componentProps.pagination, - onRowSelectionChange: useCallback((selectedRowKeys) => { + onRowSelectionChange: useCallback((selectedRowKeys, selectedRowData) => { ctx.field.data = ctx?.field?.data || {}; ctx.field.data.selectedRowKeys = selectedRowKeys; + ctx.field.data.selectedRowData = selectedRowData; ctx?.field?.onRowSelect?.(selectedRowKeys); }, []), onRowDragEnd: useCallback( diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx index f7d70ff2b18..ec735aa1ade 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx @@ -164,6 +164,7 @@ export const tableBlockSettings = new SchemaSettings({ title: t('Records per page'), value: field.decoratorProps?.params?.pageSize || 20, options: [ + { label: '5', value: 5 }, { label: '10', value: 10 }, { label: '20', value: 20 }, { label: '50', value: 50 }, diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/tableColumnSettings.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/tableColumnSettings.tsx index 6c6cf6cfc86..d6b01c61d2e 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/tableColumnSettings.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/tableColumnSettings.tsx @@ -7,22 +7,22 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import React from 'react'; import { ISchema } from '@formily/json-schema'; import { useField, useFieldSchema } from '@formily/react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { useApp, useSchemaToolbar } from '../../../../application'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { useCollectionManager_deprecated } from '../../../../collection-manager'; import { useFieldComponentName } from '../../../../common/useFieldComponentName'; import { useCollection } from '../../../../data-source'; +import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem'; import { useDesignable } from '../../../../schema-component'; import { useAssociationFieldContext } from '../../../../schema-component/antd/association-field/hooks'; import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator'; +import { SchemaSettingsLinkageRules } from '../../../../schema-settings'; import { SchemaSettingsDefaultValue } from '../../../../schema-settings/SchemaSettingsDefaultValue'; import { isPatternDisabled } from '../../../../schema-settings/isPatternDisabled'; -import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem'; -import { SchemaSettingsLinkageRules } from '../../../../schema-settings'; export const tableColumnSettings = new SchemaSettings({ name: 'fieldSettings:TableColumn', @@ -156,7 +156,9 @@ export const tableColumnSettings = new SchemaSettings({ const { currentMode } = useAssociationFieldContext(); return ( - interfaceCfg?.sortable === true && !currentMode && collection?.name === collectionField?.collectionName + interfaceCfg?.sortable === true && + !currentMode && + (collection?.name === collectionField?.collectionName || !collectionField?.collectionName) ); }, useComponentProps() { diff --git a/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseItemFieldSettings.ts b/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseItemFieldSettings.ts index 8e62a09772c..e6d7555a323 100644 --- a/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseItemFieldSettings.ts +++ b/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseItemFieldSettings.ts @@ -13,11 +13,11 @@ import { useTranslation } from 'react-i18next'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider'; import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../../collection-manager'; +import { useCollection } from '../../../../data-source'; import { useCollectionManager } from '../../../../data-source/collection/CollectionManagerProvider'; import { useCompile, useDesignable } from '../../../../schema-component'; -import { SchemaSettingsDefaultSortingRules } from '../../../../schema-settings'; +import { SchemaSettingsDefaultSortingRules, SchemaSettingsDefaultValue } from '../../../../schema-settings'; import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope'; -import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem'; export const filterCollapseItemFieldSettings = new SchemaSettings({ name: 'fieldSettings:FilterCollapseItem', @@ -135,6 +135,12 @@ export const filterCollapseItemFieldSettings = new SchemaSettings({ }, }; }, + useVisible() { + const fieldSchema = useFieldSchema(); + const collection = useCollection(); + const collectionField = collection.getField(fieldSchema['name']); + return !!collectionField?.target; + }, }, { name: 'setDefaultSortingRules', @@ -150,6 +156,12 @@ export const filterCollapseItemFieldSettings = new SchemaSettings({ name: collectionField?.target, }; }, + useVisible() { + const fieldSchema = useFieldSchema(); + const collection = useCollection(); + const collectionField = collection.getField(fieldSchema['name']); + return !!collectionField?.target; + }, }, { name: 'titleField', @@ -197,8 +209,20 @@ export const filterCollapseItemFieldSettings = new SchemaSettings({ onChange: onTitleFieldChange, }; }, + useVisible() { + const fieldSchema = useFieldSchema(); + const collection = useCollection(); + const collectionField = collection.getField(fieldSchema['name']); + return !!collectionField?.target; + }, }, - fieldComponentSettingsItem, + { + name: 'setDefaultValue', + Component: SchemaSettingsDefaultValue, + componentProps: { + hideVariableButton: true, + }, + } as any, ]; }, }, diff --git a/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/schemaInitializer.test.ts index f1f8fde1ac5..74e4ec18e40 100644 --- a/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/schemaInitializer.test.ts @@ -8,7 +8,7 @@ */ import { Page, createBlockInPage, expect, oneEmptyFilterFormBlock, test } from '@nocobase/test/e2e'; -import { oneFilterFormWithInherit } from './templates'; +import { displayManyTOManyAssociationFields, oneFilterFormWithInherit } from './templates'; const deleteButton = async (page: Page, name: string) => { await page.getByLabel(`action-Action-${name}-`).hover(); @@ -82,6 +82,33 @@ test.describe('configure fields', () => { await expect(page.getByLabel('block-item-Markdown.Void-general-filter-form')).toBeVisible(); }); + test('display manyToMany association fields', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(displayManyTOManyAssociationFields).waitForInit(); + const record = await mockRecord('users', 2); + await nocoPage.goto(); + + // display the `users.roles` field + await page.getByLabel('schema-initializer-Grid-filterForm:configureFields-users').hover(); + await page.getByRole('menuitem', { name: 'Roles right' }).hover(); + await page.getByRole('menuitem', { name: 'Role name' }).click(); + + // before filter + await expect(page.getByRole('button', { name: record.roles.map((item) => item.name).join(',') })).toBeVisible(); + + // input a some value to filter + await page.getByLabel('block-item-CollectionField-').getByRole('textbox').fill('root'); + await page.getByLabel('action-Action-Filter-submit-').click({ + position: { + x: 10, + y: 10, + }, + }); + + // expect: should only display the record with the `root` role + await expect(page.getByRole('button', { name: 'root' })).toBeVisible(); + await expect(page.getByRole('button', { name: record.roles.map((item) => item.name).join(',') })).not.toBeVisible(); + }); + test.pgOnly('display inherit fields', async ({ page, mockPage }) => { await mockPage(oneFilterFormWithInherit).goto(); diff --git a/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/templates.ts b/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/templates.ts index ca5ce130b8f..083b6fc132b 100644 --- a/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/templates.ts +++ b/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/templates.ts @@ -1035,3 +1035,318 @@ export const T4798 = { 'x-index': 1, }, }; +export const displayManyTOManyAssociationFields = { + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + '9r802f25xpw': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: { + y7zqna9ok64: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.13-beta', + properties: { + e01ptiv7169: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.13-beta', + properties: { + '8c03fmw7mv0': { + 'x-uid': 'osrsw28bg16', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'FilterFormBlockProvider', + 'x-use-decorator-props': 'useFilterFormBlockDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + collection: 'users', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:filterForm', + 'x-component': 'CardItem', + 'x-filter-targets': [ + { + uid: 'vl641x6hrd6', + }, + ], + 'x-app-version': '1.3.13-beta', + properties: { + vikjl7uaxai: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useFilterFormBlockProps', + 'x-app-version': '1.3.13-beta', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'filterForm:configureFields', + 'x-app-version': '1.3.13-beta', + 'x-uid': 'f38j0uj5vvq', + 'x-async': false, + 'x-index': 1, + }, + '0av2ioa3zjr': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'filterForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + style: { + float: 'right', + }, + }, + 'x-app-version': '1.3.13-beta', + properties: { + lsxm71r395m: { + _isJSONSchemaObject: true, + version: '2.0', + title: '{{ t("Filter") }}', + 'x-action': 'submit', + 'x-component': 'Action', + 'x-use-component-props': 'useFilterBlockActionProps', + 'x-designer': 'Action.Designer', + 'x-component-props': { + type: 'primary', + htmlType: 'submit', + }, + 'x-action-settings': {}, + type: 'void', + 'x-app-version': '1.3.13-beta', + 'x-uid': '6kj3gzqqubb', + 'x-async': false, + 'x-index': 1, + }, + '7ukzfx8trmp': { + _isJSONSchemaObject: true, + version: '2.0', + title: '{{ t("Reset") }}', + 'x-component': 'Action', + 'x-use-component-props': 'useResetBlockActionProps', + 'x-designer': 'Action.Designer', + 'x-action-settings': {}, + type: 'void', + 'x-app-version': '1.3.13-beta', + 'x-uid': '48rt2qm0rip', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'rv5qfqgaxs1', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'pmc05hvyobp', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'bzkbofpet3m', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'wlpzuhipi0n', + 'x-async': false, + 'x-index': 1, + }, + ektxc4qt6td: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.13-beta', + properties: { + zftl6dfyeia: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.13-beta', + properties: { + broo6eq0m2p: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.3.13-beta', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.3.13-beta', + 'x-uid': 'w46bhgi0e9b', + 'x-async': false, + 'x-index': 1, + }, + c4fknjf3dgj: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.3.13-beta', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.3.13-beta', + properties: { + '5mb0ksk9n7d': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.3.13-beta', + 'x-uid': 'b8lt0cpa8sk', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'uygsokuvjjo', + 'x-async': false, + 'x-index': 1, + }, + blco479x6xr: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-app-version': '1.3.13-beta', + properties: { + roles: { + _isJSONSchemaObject: true, + version: '2.0', + 'x-collection-field': 'users.roles', + 'x-component': 'CollectionField', + 'x-component-props': { + fieldNames: { + value: 'name', + label: 'name', + }, + ellipsis: true, + size: 'small', + }, + 'x-read-pretty': true, + 'x-decorator': null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': '1.3.13-beta', + 'x-uid': 'gocdkziqwlz', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '3pfwlp96981', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'c6nm8b9emvw', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'vl641x6hrd6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '5x1f6kzfwce', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'o9e7bi2jvp7', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'bcez1hvmvm0', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '1z5t01zmibi', + 'x-async': true, + 'x-index': 1, + }, + keepUid: true, +}; diff --git a/packages/core/client/src/modules/blocks/other-blocks/markdown/MarkdownFormItemInitializer.tsx b/packages/core/client/src/modules/blocks/other-blocks/markdown/MarkdownFormItemInitializer.tsx index 18f2be306fc..3766d7e2db2 100644 --- a/packages/core/client/src/modules/blocks/other-blocks/markdown/MarkdownFormItemInitializer.tsx +++ b/packages/core/client/src/modules/blocks/other-blocks/markdown/MarkdownFormItemInitializer.tsx @@ -23,6 +23,9 @@ export const MarkdownFormItemInitializer = () => { type: 'void', 'x-editable': false, 'x-decorator': 'FormItem', + 'x-decorator-props': { + engine: 'handlebars', + }, // 'x-designer': 'Markdown.Void.Designer', 'x-toolbar': 'FormItemSchemaToolbar', 'x-settings': 'blockSettings:markdown', diff --git a/packages/core/client/src/modules/fields/__e2e__/ellipsis.test.ts b/packages/core/client/src/modules/fields/__e2e__/ellipsis.test.ts new file mode 100644 index 00000000000..120d37b4cae --- /dev/null +++ b/packages/core/client/src/modules/fields/__e2e__/ellipsis.test.ts @@ -0,0 +1,357 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { expect, test } from '@nocobase/test/e2e'; +import { ellipsis } from './templates'; + +test.describe('ellipsis', () => { + test('Input & Input.URL & Input.TextArea & Input.JSON & RichText & Markdown & MarkdownVditor', async ({ + page, + mockPage, + mockRecord, + }) => { + const nocoPage = await mockPage(ellipsis).waitForInit(); + await mockRecord('testEllipsis', { + input: '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ这是一段很长的输入文本,用于测试省略功能。', + inputURL: 'https://www.nocobase.com/docs/welcome/introduction/getting-started/installation/docker-compose', + inputTextArea: + '1234567890abcdefghijklmnopqrstuvwxyz用于测试多行文本的省略效果用于测试多行文本的省略效果用于测试多行文本的省略效果用于测试多行文本的省略效果\n第二行文本,这里有更多的内容\n第三行文本,继续添加更多文字\n第四行文本,确保内容足够长\n第五行文本,用于测试多行文本的省略效果', + inputJSON: `{ + "99999": "其他", + "F3134": "软件销售", + "I3007": "人工智能基础软件开发", + "I3008": "人工智能应用软件开发", + "I3014": "数字文化创意软件开发", + "I3027": "信息技术咨询服务", + "I3034": "计算机系统服务", + "P1029": "业务培训(不含教育培训、职业技能培训等需取得许可的培训)" +}`, + richText: + '用于测试多行文本的省略效果用于测试多行文本的省略效果用于测试多行文本的省略效果用于测试多行文本的省略效果

NocoBase简介

1234567890abcdefghijklmnopqrstuvwxyz

这是第二段落,介绍NocoBase的主要特性

这是第三段落,讨论NocoBase的应用场景

  • 企业内部系统
  • 工作流管理
  • 数据分析平台
', + markdown: + '用于测试多行文本的省略效果用于测试多行文本的省略效果用于测试多行文本的省略效果用于测试多行文本的省略效果# NocoBase:开源无代码平台\n\n1234567890abcdefghijklmnopqrstuvwxyz\n\n## 为什么选择NocoBase?\n\n- 快速开发\n- 灵活定制\n- 开源免费\n\n### 核心功能\n\n1. 数据模型设计\n2. 界面配置\n3. 工作流引擎\n4. 权限管理\n\n> NocoBase让每个人都能轻松构建自己的软件系统', + markdownVditor: + '用于测试多行文本的省略效果用于测试多行文本的省略效果用于测试多行文本的省略效果用于测试多行文本的省略效果# Vditor:强大的Markdown编辑器\n\n1234567890abcdefghijklmnopqrstuvwxyz\n\n> Vditor是一个强大的Markdown编辑器,支持所见即所得、即时渲染和分屏预览等模式\n\n## 主要特性\n\n- 支持多种编辑模式\n- 丰富的快捷键\n- 自定义主题\n\n```js\nconsole.log("Vditor是NocoBase默认的Markdown编辑器");\n```\n\n更多信息请访问[Vditor官网](https://b3log.org/vditor/)', + type: '1', + }); + await nocoPage.goto(); + + // 1. Table ------------------------------------------------------------------------------------------------------- + await page.getByRole('button', { name: 'input', exact: true }).hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-testEllipsis' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByRole('button', { name: 'inputURL', exact: true }).hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-testEllipsis' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByRole('button', { name: 'inputTextArea', exact: true }).hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-testEllipsis' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByRole('button', { name: 'inputJSON', exact: true }).hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-testEllipsis' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByRole('button', { name: 'richText', exact: true }).hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-testEllipsis' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByRole('button', { name: 'markdown', exact: true }).hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-testEllipsis' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + // 2. Details ------------------------------------------------------------------------------------------------------- + await page.getByLabel('block-item-CollectionField-testEllipsis-details-testEllipsis.input-input').hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByLabel('block-item-CollectionField-testEllipsis-details-testEllipsis.inputURL-inputURL').hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByLabel('block-item-CollectionField-testEllipsis-details-testEllipsis.inputTextArea-').hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByLabel('block-item-CollectionField-testEllipsis-details-testEllipsis.inputJSON-inputJSON').hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByLabel('block-item-CollectionField-testEllipsis-details-testEllipsis.richText-richText').hover(); + await page + .getByRole('button', { + name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.richText', + exact: true, + }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByLabel('block-item-CollectionField-testEllipsis-details-testEllipsis.markdown-markdown').hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + // 3. List ------------------------------------------------------------------------------------------------------- + await page + .getByLabel('block-item-CardItem-testEllipsis-list') + .getByLabel('block-item-CollectionField-testEllipsis-list-testEllipsis.input-input') + .hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page + .getByLabel('block-item-CardItem-testEllipsis-list') + .getByLabel('block-item-CollectionField-testEllipsis-list-testEllipsis.inputURL-inputURL') + .hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page + .getByLabel('block-item-CardItem-testEllipsis-list') + .getByLabel('block-item-CollectionField-testEllipsis-list-testEllipsis.inputTextArea-') + .hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page + .getByLabel('block-item-CardItem-testEllipsis-list') + .getByLabel('block-item-CollectionField-testEllipsis-list-testEllipsis.inputJSON-inputJSON') + .hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page + .getByLabel('block-item-CardItem-testEllipsis-list') + .getByLabel('block-item-CollectionField-testEllipsis-list-testEllipsis.richText-richText') + .hover(); + await page + .getByRole('button', { + name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.richText', + exact: true, + }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page + .getByLabel('block-item-CardItem-testEllipsis-list') + .getByLabel('block-item-CollectionField-testEllipsis-list-testEllipsis.markdown-markdown') + .hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + // 4. GridCard ------------------------------------------------------------------------------------------------------- + await page.getByLabel('block-item-CollectionField-testEllipsis-grid-card-testEllipsis.input-input').hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByLabel('block-item-CollectionField-testEllipsis-grid-card-testEllipsis.inputURL-inputURL').hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByLabel('block-item-CollectionField-testEllipsis-grid-card-testEllipsis.inputURL-inputURL').hover(); + await page + .getByRole('button', { + name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.inputURL', + exact: true, + }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByLabel('block-item-CollectionField-testEllipsis-grid-card-testEllipsis.inputTextArea-').hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByLabel('block-item-CollectionField-testEllipsis-grid-card-testEllipsis.inputJSON-').hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByLabel('block-item-CollectionField-testEllipsis-grid-card-testEllipsis.richText-richText').hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByLabel('block-item-CollectionField-testEllipsis-grid-card-testEllipsis.markdown-markdown').hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-' }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + // 5. Kanban ------------------------------------------------------------------------------------------------------- + await page.getByLabel('block-item-CollectionField-testEllipsis-kanban-testEllipsis.input-input').hover(); + await page + .getByTestId('card-1') + .getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.input', { + exact: true, + }) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByLabel('block-item-CollectionField-testEllipsis-kanban-testEllipsis.inputURL-inputURL').hover(); + await page + .getByTestId('card-1') + .getByLabel( + 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.inputURL', + { + exact: true, + }, + ) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByLabel('block-item-CollectionField-testEllipsis-kanban-testEllipsis.inputTextArea-').hover(); + await page + .getByTestId('card-1') + .getByLabel( + 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.inputTextArea', + { + exact: true, + }, + ) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByLabel('block-item-CollectionField-testEllipsis-kanban-testEllipsis.inputJSON-').hover(); + await page + .getByTestId('card-1') + .getByLabel( + 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.inputJSON', + { + exact: true, + }, + ) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByLabel('block-item-CollectionField-testEllipsis-kanban-testEllipsis.richText-richText').hover(); + await page + .getByTestId('card-1') + .getByLabel( + 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.richText', + { + exact: true, + }, + ) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + await page.mouse.move(600, 0); + + await page.getByLabel('block-item-CollectionField-testEllipsis-kanban-testEllipsis.markdown-markdown').hover(); + await page + .getByTestId('card-1') + .getByLabel( + 'designer-schema-settings-CollectionField-fieldSettings:FormItem-testEllipsis-testEllipsis.markdown', + { + exact: true, + }, + ) + .hover(); + await page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch').check(); + await expect(page.getByRole('menuitem', { name: 'Ellipsis' }).getByRole('switch')).toBeChecked({ checked: true }); + }); +}); diff --git a/packages/core/client/src/modules/fields/__e2e__/templates.ts b/packages/core/client/src/modules/fields/__e2e__/templates.ts index 21877d6b6b8..c968b2781b9 100644 --- a/packages/core/client/src/modules/fields/__e2e__/templates.ts +++ b/packages/core/client/src/modules/fields/__e2e__/templates.ts @@ -378,3 +378,1915 @@ export const oneFormAndOneTable = { 'x-async': true, }, }; +export const ellipsis = { + collections: [ + { + name: 'testEllipsis', + fields: [ + { + name: 'input', + interface: 'input', + }, + { + name: 'inputURL', + interface: 'url', + }, + { + name: 'inputTextArea', + interface: 'textarea', + }, + { + name: 'inputJSON', + interface: 'json', + }, + { + name: 'richText', + interface: 'richText', + }, + { + name: 'markdown', + interface: 'markdown', + }, + { + name: 'type', + interface: 'select', + uiSchema: { + enum: [ + { + value: '1', + label: 'type1', + }, + { + value: '2', + label: 'type2', + }, + { + value: '3', + label: 'type3', + }, + ], + type: 'string', + 'x-component': 'Select', + title: 'Text type', + }, + }, + { + name: 'sort', + interface: 'sort', + scopeKey: 'type', + }, + // no vditor plugin in E2E + // { + // name: 'markdownVditor', + // interface: 'vditor', + // }, + ], + }, + ], + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + g965qvtvtmj: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: { + '47yh43cnlx6': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + d3lptktsh4f: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + rscvdeyaju5: { + 'x-uid': 'trvwcgwqrne', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action': 'testEllipsis:list', + 'x-decorator': 'KanbanBlockProvider', + 'x-decorator-props': { + collection: 'testEllipsis', + dataSource: 'main', + action: 'list', + groupField: 'type', + sortField: 'sort', + params: { + paginate: false, + sort: ['sort'], + }, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:kanban', + 'x-component': 'CardItem', + 'x-app-version': '1.4.0-alpha', + 'x-component-props': { + title: 'Kanban', + }, + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'kanban:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'sqcr02yymd5', + 'x-async': false, + 'x-index': 1, + }, + gzcs432mvah: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-component': 'Kanban', + 'x-use-component-props': 'useKanbanBlockProps', + 'x-app-version': '1.4.0-alpha', + properties: { + card: { + 'x-uid': 'tfgo9j7lsov', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-read-pretty': true, + 'x-label-disabled': true, + 'x-decorator': 'BlockItem', + 'x-component': 'Kanban.Card', + 'x-component-props': { + openMode: 'drawer', + }, + 'x-designer': 'Kanban.Card.Designer', + 'x-app-version': '1.4.0-alpha', + 'x-action-context': { + dataSource: 'main', + collection: 'testEllipsis', + }, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-component-props': { + dndContext: false, + }, + 'x-app-version': '1.4.0-alpha', + properties: { + j74fxk1mii4: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + properties: { + r3yoqm2t6sw: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + input: { + 'x-uid': 'cmtoq1tlgdd', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.input', + 'x-component-props': { + ellipsis: false, + }, + 'x-read-pretty': true, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'lzbi2cq9y83', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'odaqjgm1vrr', + 'x-async': false, + 'x-index': 1, + }, + '68ssogjqxmm': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + properties: { + b4720r6gtvv: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + inputURL: { + 'x-uid': '5iouqnm9ad9', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.inputURL', + 'x-component-props': { + ellipsis: false, + }, + 'x-read-pretty': true, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'dhejhsb1she', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'oniopbss0x6', + 'x-async': false, + 'x-index': 2, + }, + g9jo9l3iazi: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + properties: { + rr5i7shb90r: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + inputTextArea: { + 'x-uid': 'hg85l2u35fm', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.inputTextArea', + 'x-component-props': { + ellipsis: false, + }, + 'x-read-pretty': true, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'p11om0d81vk', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'q6ejh1x8nbr', + 'x-async': false, + 'x-index': 3, + }, + '9qragbojca7': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + properties: { + '64iwk8759di': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + inputJSON: { + 'x-uid': '9yq6010lb9x', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.inputJSON', + 'x-component-props': { + ellipsis: false, + }, + 'x-read-pretty': true, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ok3ch83z9q1', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'mrr6rhu2aua', + 'x-async': false, + 'x-index': 4, + }, + kg1e2nql18g: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + properties: { + cfmae8pdbr9: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + richText: { + 'x-uid': '9b6esxdq60r', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.richText', + 'x-component-props': { + ellipsis: false, + }, + 'x-read-pretty': true, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'a0u0o2i8wy6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'mx2k9u8tg7g', + 'x-async': false, + 'x-index': 5, + }, + qk4i0u51ih9: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + properties: { + phusp5bo3yf: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + markdown: { + 'x-uid': 'i610e8gvexu', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.markdown', + 'x-component-props': { + ellipsis: false, + }, + 'x-read-pretty': true, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'j9kcoshwqcz', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'jntewcvl8kp', + 'x-async': false, + 'x-index': 6, + }, + }, + 'x-uid': 'g0iysqk2ap4', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + cardViewer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("View") }}', + 'x-designer': 'Action.Designer', + 'x-component': 'Kanban.CardViewer', + 'x-action': 'view', + 'x-component-props': { + openMode: 'drawer', + }, + 'x-app-version': '1.4.0-alpha', + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("View record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + 'x-app-version': '1.4.0-alpha', + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + 'x-app-version': '1.4.0-alpha', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Details")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:common:addBlock', + 'x-app-version': '1.4.0-alpha', + 'x-uid': '3c7oi86zmc1', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'pqclgxhsiny', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'x4a9lybp1x8', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ny00k7fdxcc', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'jdsdt7u9du6', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'wvx00mxj9g6', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'c0f710dofpg', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '1w93tt24e5j', + 'x-async': false, + 'x-index': 1, + }, + aga7m08a6c5: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + urulcx1gmb7: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + pz8btjnnmso: { + 'x-uid': 'h8gqtuvahgr', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'testEllipsis:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'testEllipsis', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.4.0-alpha', + 'x-component-props': { + title: 'Table', + }, + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'izgz249whns', + 'x-async': false, + 'x-index': 1, + }, + mn9nlj3ow1o: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.4.0-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.4.0-alpha', + properties: { + '96qmif5wzzi': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'pk4d8ksx6x0', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 't4n99un60td', + 'x-async': false, + 'x-index': 1, + }, + '1oe2pn371kt': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-app-version': '1.4.0-alpha', + properties: { + input: { + _isJSONSchemaObject: true, + version: '2.0', + 'x-collection-field': 'testEllipsis.input', + 'x-component': 'CollectionField', + 'x-component-props': { + ellipsis: true, + }, + 'x-read-pretty': true, + 'x-decorator': null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'ehv6e4f31y1', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'kujg1rf3yoy', + 'x-async': false, + 'x-index': 2, + }, + b0f7n4ot0uf: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-app-version': '1.4.0-alpha', + properties: { + inputURL: { + _isJSONSchemaObject: true, + version: '2.0', + 'x-collection-field': 'testEllipsis.inputURL', + 'x-component': 'CollectionField', + 'x-component-props': {}, + 'x-read-pretty': true, + 'x-decorator': null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'c30nzrikd4q', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'vp9hay0y3xd', + 'x-async': false, + 'x-index': 3, + }, + '9ciu8x0w6m2': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-app-version': '1.4.0-alpha', + properties: { + inputTextArea: { + _isJSONSchemaObject: true, + version: '2.0', + 'x-collection-field': 'testEllipsis.inputTextArea', + 'x-component': 'CollectionField', + 'x-component-props': { + ellipsis: true, + }, + 'x-read-pretty': true, + 'x-decorator': null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'ua7s6vv4js2', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'swksvx7kdqk', + 'x-async': false, + 'x-index': 4, + }, + vqzoy6wh3v9: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-app-version': '1.4.0-alpha', + properties: { + inputJSON: { + _isJSONSchemaObject: true, + version: '2.0', + 'x-collection-field': 'testEllipsis.inputJSON', + 'x-component': 'CollectionField', + 'x-component-props': {}, + 'x-read-pretty': true, + 'x-decorator': null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'qkx9xmrpqtr', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '4accak7p0qv', + 'x-async': false, + 'x-index': 5, + }, + '85j1ll3q04j': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-app-version': '1.4.0-alpha', + properties: { + richText: { + _isJSONSchemaObject: true, + version: '2.0', + 'x-collection-field': 'testEllipsis.richText', + 'x-component': 'CollectionField', + 'x-component-props': { + ellipsis: true, + }, + 'x-read-pretty': true, + 'x-decorator': null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': '1.4.0-alpha', + 'x-uid': '9ofnczj2lvz', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '63zulw34tn4', + 'x-async': false, + 'x-index': 6, + }, + kshui6i56xv: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-app-version': '1.4.0-alpha', + properties: { + markdown: { + _isJSONSchemaObject: true, + version: '2.0', + 'x-collection-field': 'testEllipsis.markdown', + 'x-component': 'CollectionField', + 'x-component-props': { + ellipsis: true, + }, + 'x-read-pretty': true, + 'x-decorator': null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'et6l8blhhvd', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'hplvy9e9kli', + 'x-async': false, + 'x-index': 7, + }, + }, + 'x-uid': 'tsj4d14zx8s', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'q3jchapkn19', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '8d8ganq4ooq', + 'x-async': false, + 'x-index': 2, + }, + gc9ffz397dj: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + lwvmogv3iub: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + zomdsijnphc: { + 'x-uid': '3mmm519tj8y', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action': 'testEllipsis:view', + 'x-decorator': 'DetailsBlockProvider', + 'x-use-decorator-props': 'useDetailsWithPaginationDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + collection: 'testEllipsis', + readPretty: true, + action: 'list', + params: { + pageSize: 1, + }, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:detailsWithPagination', + 'x-component': 'CardItem', + 'x-app-version': '1.4.0-alpha', + 'x-component-props': { + title: 'Details', + }, + properties: { + q4sxbegbfrc: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Details', + 'x-read-pretty': true, + 'x-use-component-props': 'useDetailsWithPaginationProps', + 'x-app-version': '1.4.0-alpha', + properties: { + hjwsp9e5tel: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'details:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 24, + }, + }, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'atz6i01mjx1', + 'x-async': false, + 'x-index': 1, + }, + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'details:configureFields', + 'x-app-version': '1.4.0-alpha', + properties: { + '92sx6204v0g': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + iuy9o1gjepl: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + input: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.input', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'q9fpbjwdira', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'mltsb815ojg', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '9xvn79znd7e', + 'x-async': false, + 'x-index': 1, + }, + su4uh3mv5tr: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + z7nuq3tt62h: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + inputURL: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.inputURL', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'k90a3p14af2', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '5dcjcop0lgk', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'h3nib7nzhxr', + 'x-async': false, + 'x-index': 2, + }, + rpvszdqdlyo: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + xvavsr4dpuv: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + inputTextArea: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.inputTextArea', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': '53ynbm1dfae', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'lu8ngjdigry', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '761mge52ckk', + 'x-async': false, + 'x-index': 3, + }, + u119pc69wdu: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + '71il5i0uwj1': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + inputJSON: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.inputJSON', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'zled414bjdg', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'oigd8yqkr6z', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'mzos557de1m', + 'x-async': false, + 'x-index': 4, + }, + iazxy8avs8h: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + '2qxnd682jcw': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + richText: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.richText', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'wrzndo8aqjt', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '9go4enrcgte', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'f373k9h33kp', + 'x-async': false, + 'x-index': 5, + }, + '0i4jsug4icz': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + gub9cdiv9m5: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + markdown: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.markdown', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'twvvwp38n03', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '5ckll512oq5', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'x1e55qw6vng', + 'x-async': false, + 'x-index': 6, + }, + }, + 'x-uid': 'nivnp8d6ja5', + 'x-async': false, + 'x-index': 2, + }, + pagination: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Pagination', + 'x-use-component-props': 'useDetailsPaginationProps', + 'x-app-version': '1.4.0-alpha', + 'x-uid': 't33e0npeqng', + 'x-async': false, + 'x-index': 3, + }, + }, + 'x-uid': 'ek2ocsznoa8', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'zcihllchlwv', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'taj8od92vlk', + 'x-async': false, + 'x-index': 3, + }, + '6wr9h8rtsps': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + '4unxqlrawlv': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + '1nexmi69n86': { + 'x-uid': '302sznn4xja', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action': 'testEllipsis:view', + 'x-decorator': 'List.Decorator', + 'x-use-decorator-props': 'useListBlockDecoratorProps', + 'x-decorator-props': { + collection: 'testEllipsis', + dataSource: 'main', + readPretty: true, + action: 'list', + params: { + pageSize: 10, + }, + runWhenParamsChanged: true, + rowKey: 'id', + }, + 'x-component': 'CardItem', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:list', + 'x-app-version': '1.4.0-alpha', + 'x-component-props': { + title: 'List', + }, + properties: { + actionBar: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'list:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.4.0-alpha', + 'x-uid': '1sv29ingnfm', + 'x-async': false, + 'x-index': 1, + }, + list: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-component': 'List', + 'x-use-component-props': 'useListBlockProps', + 'x-app-version': '1.4.0-alpha', + properties: { + item: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'object', + 'x-component': 'List.Item', + 'x-read-pretty': true, + 'x-use-component-props': 'useListItemProps', + 'x-app-version': '1.4.0-alpha', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'details:configureFields', + 'x-app-version': '1.4.0-alpha', + properties: { + frmr83bjszl: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + '2dl26hscje6': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + input: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.input', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'uny79lee2lp', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'cgdnw331nj2', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'uj6esc5yiqi', + 'x-async': false, + 'x-index': 1, + }, + zzv7dje76hr: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + '1i326ws5mtd': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + inputURL: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.inputURL', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'tbraiag1qzf', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '02347u0s0ss', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'v9jwkbjhn15', + 'x-async': false, + 'x-index': 2, + }, + qfcotp79sqd: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + d4lp4f8z8s4: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + inputTextArea: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.inputTextArea', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'qt5rwnbzo72', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'i50wpr0134t', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'uyf89ww8fxp', + 'x-async': false, + 'x-index': 3, + }, + '2dxqgysh2lz': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + l7nofmkyct0: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + inputJSON: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.inputJSON', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'gnhcv2evznj', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'fxym8vkrtf5', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '2cvkfh2mfhm', + 'x-async': false, + 'x-index': 4, + }, + e5rl5jcyha6: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + m0cnws87pj0: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + richText: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.richText', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': '8e81mjvpmd7', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'z96bi1of3oo', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '96tmezq68bl', + 'x-async': false, + 'x-index': 5, + }, + l9npx8a01sa: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + '0t70ay46j5b': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + markdown: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.markdown', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'hjcpn1qhvog', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'uwl0bvglk4d', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'll64su7i3do', + 'x-async': false, + 'x-index': 6, + }, + }, + 'x-uid': '456jc3ohfgk', + 'x-async': false, + 'x-index': 1, + }, + actionBar: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-align': 'left', + 'x-initializer': 'list:configureItemActions', + 'x-component': 'ActionBar', + 'x-use-component-props': 'useListActionBarProps', + 'x-component-props': { + layout: 'one-column', + }, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'n8p6b2qcsn9', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'ukw31qvotuj', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'l68f49rnjmy', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '36t6bxu86ub', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'hg45dt831lp', + 'x-async': false, + 'x-index': 4, + }, + '2yi1elrltsf': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + '9ptvkwrgc57': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + '20ukde66ghk': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action': 'testEllipsis:view', + 'x-decorator': 'GridCard.Decorator', + 'x-use-decorator-props': 'useGridCardBlockDecoratorProps', + 'x-decorator-props': { + collection: 'testEllipsis', + dataSource: 'main', + readPretty: true, + action: 'list', + params: { + pageSize: 12, + }, + runWhenParamsChanged: true, + rowKey: 'id', + }, + 'x-component': 'BlockItem', + 'x-use-component-props': 'useGridCardBlockItemProps', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:gridCard', + 'x-app-version': '1.4.0-alpha', + properties: { + actionBar: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'gridCard:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'wtt4c62i90w', + 'x-async': false, + 'x-index': 1, + }, + list: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-component': 'GridCard', + 'x-use-component-props': 'useGridCardBlockProps', + 'x-app-version': '1.4.0-alpha', + properties: { + item: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'object', + 'x-component': 'GridCard.Item', + 'x-read-pretty': true, + 'x-use-component-props': 'useGridCardItemProps', + 'x-app-version': '1.4.0-alpha', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'details:configureFields', + 'x-app-version': '1.4.0-alpha', + properties: { + gqzlaba1um3: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + rqev67avkcg: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + input: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.input', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'qdsrg1nhuyv', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'no89ehqn0p5', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '3borzpb9spq', + 'x-async': false, + 'x-index': 1, + }, + vabc618iob9: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + x200si2lp2o: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + inputURL: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.inputURL', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': '6jvpbc5arkv', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'qu7kx2x4cqn', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'oo6ie8c38k2', + 'x-async': false, + 'x-index': 2, + }, + hzg1lcj809i: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + kboo5qirkn7: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + inputTextArea: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.inputTextArea', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'sgnvmbuzy25', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '4zd1fg1qu5o', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'wolac9gfhwt', + 'x-async': false, + 'x-index': 3, + }, + j6p2fykaqxd: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + t4ehboas9ma: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + inputJSON: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.inputJSON', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'yos6wc0ouvj', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'houb8q3dc2f', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'hwtmz9xtvi0', + 'x-async': false, + 'x-index': 4, + }, + '4lfggynjp26': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + ggqr2mg8wym: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + richText: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.richText', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'd9d9ek039au', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'imt0f9z7tcj', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '9kb6nqrq0nc', + 'x-async': false, + 'x-index': 5, + }, + '4j7sg3potav': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.4.0-alpha', + properties: { + w9j02mmgzro: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.4.0-alpha', + properties: { + markdown: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'testEllipsis.markdown', + 'x-component-props': {}, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'ff64l0hqaen', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'aqwvp7swn3e', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '4i16nph2r2b', + 'x-async': false, + 'x-index': 6, + }, + }, + 'x-uid': 'k3ndudpzcp5', + 'x-async': false, + 'x-index': 1, + }, + actionBar: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-align': 'left', + 'x-initializer': 'gridCard:configureItemActions', + 'x-component': 'ActionBar', + 'x-use-component-props': 'useGridCardActionBarProps', + 'x-component-props': { + layout: 'one-column', + }, + 'x-app-version': '1.4.0-alpha', + 'x-uid': 'iapjbqie0ni', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'zuvfss2wgq6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'u4b8delxyvw', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'c4yjem1mj32', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'bla6ddipf6e', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'vvfih1g0q7n', + 'x-async': false, + 'x-index': 5, + }, + }, + 'x-uid': '47ks7rghprw', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'g77bu4dcydq', + 'x-async': true, + 'x-index': 1, + }, +}; diff --git a/packages/core/client/src/modules/fields/component/DatePicker/datePickerComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/DatePicker/datePickerComponentFieldSettings.tsx index 4e243074e89..905b26ac7f2 100644 --- a/packages/core/client/src/modules/fields/component/DatePicker/datePickerComponentFieldSettings.tsx +++ b/packages/core/client/src/modules/fields/component/DatePicker/datePickerComponentFieldSettings.tsx @@ -29,3 +29,21 @@ export const datePickerComponentFieldSettings = new SchemaSettings({ }, ], }); + +export const rangePickerPickerComponentFieldSettings = new SchemaSettings({ + name: 'fieldSettings:component:DatePicker.RangePicker', + items: [ + { + name: 'dateDisplayFormat', + Component: SchemaSettingsDateFormat as any, + useComponentProps() { + const schema = useFieldSchema(); + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + const fieldSchema = tableColumnSchema || schema; + return { + fieldSchema, + }; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/fields/component/Input.JSON/inputJSONSettings.tsx b/packages/core/client/src/modules/fields/component/Input.JSON/inputJSONSettings.tsx new file mode 100644 index 00000000000..3c9c11cc8d4 --- /dev/null +++ b/packages/core/client/src/modules/fields/component/Input.JSON/inputJSONSettings.tsx @@ -0,0 +1,15 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { ellipsisSettingsItem } from '../Input/inputComponentSettings'; + +export const inputJSONSettings = new SchemaSettings({ + name: 'fieldSettings:component:Input.JSON', + items: [ellipsisSettingsItem], +}); diff --git a/packages/core/client/src/modules/fields/component/Input.TextArea/inputTextAreaSettings.tsx b/packages/core/client/src/modules/fields/component/Input.TextArea/inputTextAreaSettings.tsx new file mode 100644 index 00000000000..cb3f8839a80 --- /dev/null +++ b/packages/core/client/src/modules/fields/component/Input.TextArea/inputTextAreaSettings.tsx @@ -0,0 +1,16 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { ellipsisSettingsItem } from '../Input/inputComponentSettings'; + +export const inputTextAreaSettings = new SchemaSettings({ + name: 'fieldSettings:component:Input.TextArea', + items: [ellipsisSettingsItem], +}); diff --git a/packages/core/client/src/modules/fields/component/Input.URL/inputURLSettings.tsx b/packages/core/client/src/modules/fields/component/Input.URL/inputURLSettings.tsx new file mode 100644 index 00000000000..e49124ea1b2 --- /dev/null +++ b/packages/core/client/src/modules/fields/component/Input.URL/inputURLSettings.tsx @@ -0,0 +1,16 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { ellipsisSettingsItem } from '../Input/inputComponentSettings'; + +export const inputURLSettings = new SchemaSettings({ + name: 'fieldSettings:component:Input.URL', + items: [ellipsisSettingsItem], +}); diff --git a/packages/core/client/src/modules/fields/component/Input/inputComponentSettings.tsx b/packages/core/client/src/modules/fields/component/Input/inputComponentSettings.tsx new file mode 100644 index 00000000000..b07da433541 --- /dev/null +++ b/packages/core/client/src/modules/fields/component/Input/inputComponentSettings.tsx @@ -0,0 +1,64 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { useField, useFieldSchema } from '@formily/react'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { SchemaSettingsItemType } from '../../../../application/schema-settings/types'; +import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator'; +import { useDesignable } from '../../../../schema-component/hooks/useDesignable'; + +export const ellipsisSettingsItem: SchemaSettingsItemType = { + name: 'ellipsis', + type: 'switch', + useComponentProps() { + const { fieldSchema: tableFieldSchema, filedInstanceList } = useColumnSchema(); + const fieldSchema = useFieldSchema(); + const formField = useField(); + const { dn } = useDesignable(); + const { t } = useTranslation(); + + const schema = tableFieldSchema || fieldSchema; + const hidden = tableFieldSchema + ? filedInstanceList[0] + ? !filedInstanceList[0].readPretty + : !tableFieldSchema['x-read-pretty'] + : !formField.readPretty; + + return { + title: t('Ellipsis overflow content'), + checked: !!schema['x-component-props']?.ellipsis, + hidden, + onChange: async (checked) => { + await dn.emit('patch', { + schema: { + 'x-uid': schema['x-uid'], + 'x-component-props': { + ...schema['x-component-props'], + ellipsis: checked, + }, + }, + }); + + if (tableFieldSchema && filedInstanceList) { + filedInstanceList.forEach((fieldInstance) => { + fieldInstance.componentProps.ellipsis = checked; + }); + } else { + formField.componentProps.ellipsis = checked; + } + }, + }; + }, +}; + +export const inputComponentSettings = new SchemaSettings({ + name: 'fieldSettings:component:Input', + items: [ellipsisSettingsItem], +}); diff --git a/packages/core/client/src/modules/fields/component/Markdown/markdownSettings.tsx b/packages/core/client/src/modules/fields/component/Markdown/markdownSettings.tsx new file mode 100644 index 00000000000..6011b64ce76 --- /dev/null +++ b/packages/core/client/src/modules/fields/component/Markdown/markdownSettings.tsx @@ -0,0 +1,16 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { ellipsisSettingsItem } from '../Input/inputComponentSettings'; + +export const markdownSettings = new SchemaSettings({ + name: 'fieldSettings:component:Markdown', + items: [ellipsisSettingsItem], +}); diff --git a/packages/core/client/src/modules/fields/component/MarkdownVditor/markdownVditorSettings.tsx b/packages/core/client/src/modules/fields/component/MarkdownVditor/markdownVditorSettings.tsx new file mode 100644 index 00000000000..31da15d31b0 --- /dev/null +++ b/packages/core/client/src/modules/fields/component/MarkdownVditor/markdownVditorSettings.tsx @@ -0,0 +1,16 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { ellipsisSettingsItem } from '../Input/inputComponentSettings'; + +export const markdownVditorSettings = new SchemaSettings({ + name: 'fieldSettings:component:MarkdownVditor', + items: [ellipsisSettingsItem], +}); diff --git a/packages/core/client/src/modules/fields/component/Nester/subformComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/Nester/subformComponentFieldSettings.tsx index 35de5557129..8540f641569 100644 --- a/packages/core/client/src/modules/fields/component/Nester/subformComponentFieldSettings.tsx +++ b/packages/core/client/src/modules/fields/component/Nester/subformComponentFieldSettings.tsx @@ -20,7 +20,7 @@ import { useIsFieldReadPretty, useIsFormReadPretty, } from '../../../../schema-component/antd/form-item/FormItem.Settings'; -import { setDefaultSortingRules } from '../SubTable/subTablePopoverComponentFieldSettings'; +import { linkageRules, setDefaultSortingRules } from '../SubTable/subTablePopoverComponentFieldSettings'; const allowMultiple: any = { name: 'allowMultiple', @@ -142,5 +142,6 @@ export const subformComponentFieldSettings = new SchemaSettings({ }, }, setDefaultSortingRules, + linkageRules, ], }); diff --git a/packages/core/client/src/modules/fields/component/Picker/TableSelectorInitializers.tsx b/packages/core/client/src/modules/fields/component/Picker/TableSelectorInitializers.tsx index d8ba0d35254..d07cb8914f5 100644 --- a/packages/core/client/src/modules/fields/component/Picker/TableSelectorInitializers.tsx +++ b/packages/core/client/src/modules/fields/component/Picker/TableSelectorInitializers.tsx @@ -70,21 +70,9 @@ const commonOptions = { name: 'otherBlocks', children: [ { - title: '{{t("Add text")}}', - Component: 'BlockItemInitializer', - name: 'addText', - schema: { - type: 'void', - 'x-editable': false, - 'x-decorator': 'BlockItem', - // 'x-designer': 'Markdown.Void.Designer', - 'x-toolbar': 'BlockSchemaToolbar', - 'x-settings': 'blockSettings:markdown', - 'x-component': 'Markdown.Void', - 'x-component-props': { - content: '{{t("This is a demo text, **supports Markdown syntax**.")}}', - }, - }, + name: 'markdown', + title: '{{t("Markdown")}}', + Component: 'MarkdownBlockInitializer', }, ], }, diff --git a/packages/core/client/src/modules/fields/component/PopoverNester/subformPopoverComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/PopoverNester/subformPopoverComponentFieldSettings.tsx index 0149ddcede5..d7c7e616eff 100644 --- a/packages/core/client/src/modules/fields/component/PopoverNester/subformPopoverComponentFieldSettings.tsx +++ b/packages/core/client/src/modules/fields/component/PopoverNester/subformPopoverComponentFieldSettings.tsx @@ -12,12 +12,13 @@ import { useField, useFieldSchema } from '@formily/react'; import { useTranslation } from 'react-i18next'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { useFieldComponentName } from '../../../../common/useFieldComponentName'; +import { useCollectionField } from '../../../../data-source'; import { useDesignable, useFieldModeOptions, useIsAddNewForm } from '../../../../schema-component'; import { isSubMode } from '../../../../schema-component/antd/association-field/util'; import { useIsFieldReadPretty } from '../../../../schema-component/antd/form-item/FormItem.Settings'; -import { useCollectionField } from '../../../../data-source'; import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator'; import { titleField } from '../Picker/recordPickerComponentFieldSettings'; +import { linkageRules } from '../SubTable/subTablePopoverComponentFieldSettings'; const allowMultiple: any = { name: 'allowMultiple', @@ -103,5 +104,5 @@ const fieldComponent: any = { export const subformPopoverComponentFieldSettings = new SchemaSettings({ name: 'fieldSettings:component:PopoverNester', - items: [fieldComponent, allowMultiple, titleField], + items: [fieldComponent, allowMultiple, titleField, linkageRules], }); diff --git a/packages/core/client/src/modules/fields/component/RichText/richTextSettings.tsx b/packages/core/client/src/modules/fields/component/RichText/richTextSettings.tsx new file mode 100644 index 00000000000..a1021ddff34 --- /dev/null +++ b/packages/core/client/src/modules/fields/component/RichText/richTextSettings.tsx @@ -0,0 +1,16 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { ellipsisSettingsItem } from '../Input/inputComponentSettings'; + +export const richTextSettings = new SchemaSettings({ + name: 'fieldSettings:component:RichText', + items: [ellipsisSettingsItem], +}); diff --git a/packages/core/client/src/modules/fields/component/Select/selectComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/Select/selectComponentFieldSettings.tsx index 57032353b28..df9ea2847ff 100644 --- a/packages/core/client/src/modules/fields/component/Select/selectComponentFieldSettings.tsx +++ b/packages/core/client/src/modules/fields/component/Select/selectComponentFieldSettings.tsx @@ -34,6 +34,7 @@ import { SchemaSettingsSortingRule } from '../../../../schema-settings/SchemaSet import { useIsShowMultipleSwitch } from '../../../../schema-settings/hooks/useIsShowMultipleSwitch'; import { useLocalVariables, useVariables } from '../../../../variables'; import { useOpenModeContext } from '../../../popup/OpenModeProvider'; +import { useDataBlockProps } from '../../../../data-source'; const enableLink = { name: 'enableLink', @@ -358,7 +359,8 @@ export const selectComponentFieldSettings = new SchemaSettings({ const isAssociationField = useIsAssociationField(); const readPretty = useIsFieldReadPretty(); const { fieldSchema } = useColumnSchema(); - return isAssociationField && !fieldSchema && !readPretty; + const { type } = useDataBlockProps() || ({} as any); + return isAssociationField && !fieldSchema && !readPretty && type !== 'publicForm'; }, }, { diff --git a/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx index a0d9d1aada8..06339a4bd59 100644 --- a/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx +++ b/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx @@ -7,12 +7,17 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { Field } from '@formily/core'; -import { useField, useFieldSchema, ISchema } from '@formily/react'; -import { useTranslation } from 'react-i18next'; import { ArrayItems } from '@formily/antd-v5'; +import { Field } from '@formily/core'; +import { ISchema, useField, useFieldSchema } from '@formily/react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useCollectionManager_deprecated, useSortFields } from '../../../../collection-manager'; import { useFieldComponentName } from '../../../../common/useFieldComponentName'; +import { useCollectionManager, useRerenderDataBlock } from '../../../../data-source'; +import { FlagProvider } from '../../../../flag-provider/FlagProvider'; +import { withDynamicSchemaProps } from '../../../../hoc/withDynamicSchemaProps'; import { useDesignable, useFieldModeOptions, @@ -20,8 +25,9 @@ import { useIsFieldReadPretty, } from '../../../../schema-component'; import { isSubMode } from '../../../../schema-component/antd/association-field/util'; -import { useCollectionManager_deprecated, useSortFields } from '../../../../collection-manager'; import { useIsAssociationField } from '../../../../schema-component/antd/form-item'; +import { FormLinkageRules } from '../../../../schema-settings/LinkageRules'; +import { SchemaSettingsLinkageRules } from '../../../../schema-settings/SchemaSettings'; const fieldComponent: any = { name: 'fieldComponent', @@ -253,7 +259,39 @@ export const allowAddNewData = { }; }, }; + +const LinkageRulesComponent = withDynamicSchemaProps( + (props) => { + return ( + // the purpose is to display the `Current object` variable in the linkage rule configuration dialog + + + + ); + }, + { displayName: 'LinkageRulesComponent' }, +); + +export const linkageRules = { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const field = useField(); + const fieldSchema = useFieldSchema(); + const cm = useCollectionManager(); + const collectionField = cm.getCollectionField(fieldSchema['x-collection-field']); + const { rerenderDataBlock } = useRerenderDataBlock(); + + return { + collectionName: collectionField?.target, + Component: LinkageRulesComponent, + readPretty: field.readPretty, + afterSubmit: rerenderDataBlock, + }; + }, +}; + export const subTablePopoverComponentFieldSettings = new SchemaSettings({ name: 'fieldSettings:component:SubTable', - items: [fieldComponent, allowAddNewData, allowSelectExistingRecord, setDefaultSortingRules], + items: [fieldComponent, allowAddNewData, allowSelectExistingRecord, setDefaultSortingRules, linkageRules], }); diff --git a/packages/core/client/src/modules/popup/OpenModeProvider.tsx b/packages/core/client/src/modules/popup/OpenModeProvider.tsx index a3d102e8fe8..8a32996563c 100644 --- a/packages/core/client/src/modules/popup/OpenModeProvider.tsx +++ b/packages/core/client/src/modules/popup/OpenModeProvider.tsx @@ -30,6 +30,11 @@ interface OpenModeProviderProps { * 隐藏 open mode 的配置选项 */ hideOpenMode?: boolean; + /** + * @default false + * 是否为Mobile路由下 + */ + isMobile?: boolean; } const defaultContext: OpenModeProviderProps = { @@ -46,6 +51,7 @@ const OpenModeContext = React.createContext<{ defaultOpenMode: OpenModeProviderProps['defaultOpenMode']; hideOpenMode: boolean; getComponentByOpenMode: (openMode: OpenMode) => any; + isMobile: boolean; }>(null); /** @@ -66,7 +72,9 @@ export const OpenModeProvider: FC = (props) => { if (props.hideOpenMode !== undefined) { result.hideOpenMode = props.hideOpenMode; } - + if (props.isMobile) { + result.isMobile = props.isMobile; + } return result; }, [props.defaultOpenMode, props.openModeToComponent, props.hideOpenMode]); @@ -88,8 +96,9 @@ export const OpenModeProvider: FC = (props) => { defaultOpenMode: context.defaultOpenMode, hideOpenMode: context.hideOpenMode, getComponentByOpenMode, + isMobile: context.isMobile, }; - }, [context.defaultOpenMode, context.hideOpenMode, getComponentByOpenMode]); + }, [context.defaultOpenMode, context.hideOpenMode, getComponentByOpenMode, context.isMobile]); return {props.children}; }; diff --git a/packages/core/client/src/modules/popup/PopupContextProvider.tsx b/packages/core/client/src/modules/popup/PopupContextProvider.tsx index 4aead695685..488aa5abdc4 100644 --- a/packages/core/client/src/modules/popup/PopupContextProvider.tsx +++ b/packages/core/client/src/modules/popup/PopupContextProvider.tsx @@ -17,7 +17,10 @@ import { PopupVisibleProvider, PopupVisibleProviderContext } from '../../schema- * @param props * @returns */ -export const PopupContextProvider: React.FC = (props) => { +export const PopupContextProvider: React.FC<{ + visible?: boolean; + setVisible?: (visible: boolean) => void; +}> = (props) => { const [visible, setVisible] = useState(false); const { visible: visibleWithURL, setVisible: setVisibleWithURL } = useContext(PopupVisibleProviderContext) || { visible: false, @@ -26,10 +29,11 @@ export const PopupContextProvider: React.FC = (props) => { const fieldSchema = useFieldSchema(); const _setVisible = useCallback( (value: boolean): void => { + props.setVisible?.(value); setVisible?.(value); setVisibleWithURL?.(value); }, - [setVisibleWithURL], + [props, setVisibleWithURL], ); const openMode = fieldSchema['x-component-props']?.['openMode'] || 'drawer'; const openSize = fieldSchema['x-component-props']?.['openSize']; @@ -37,7 +41,7 @@ export const PopupContextProvider: React.FC = (props) => { return ( { await addBlock('form Table'); await addBlock('form Form'); await addBlock('Collapse'); - await addBlock('Add text'); + await addBlock('Markdown'); await expect(page.getByLabel('block-item-CardItem-roles-table-selector')).toBeVisible(); await expect(page.getByLabel('block-item-CardItem-roles-filter-form')).toBeVisible(); await expect(page.getByLabel('block-item-CardItem-roles-filter-collapse')).toBeVisible(); - await expect(page.getByLabel('block-item-Markdown.Void-roles-form')).toBeVisible(); + await expect(page.getByLabel('block-item-Markdown.Void-')).toBeVisible(); async function addBlock(name: string) { await page.getByLabel('schema-initializer-Grid-popup:tableSelector:addBlock-roles').hover(); diff --git a/packages/core/client/src/modules/popup/__e2e__/subPage.test.ts b/packages/core/client/src/modules/popup/__e2e__/subPage.test.ts index 2ecf6fde468..4d48df81f35 100644 --- a/packages/core/client/src/modules/popup/__e2e__/subPage.test.ts +++ b/packages/core/client/src/modules/popup/__e2e__/subPage.test.ts @@ -40,7 +40,7 @@ test.describe('sub page', () => { page.getByLabel('block-item-Markdown.Void-').getByText('Markdown:从弹窗中打开的子页面,tab2。'), ).toBeVisible(); await page.getByLabel('back-button').click(); - await page.goBack(); + await page.getByLabel('drawer-Action.Container-users-View record-mask').click(); // 从嵌套弹窗中打开子页面 -------------------------------------------------------------------- await page.getByLabel('action-Action.Link-View-view-').nth(2).click(); diff --git a/packages/core/client/src/modules/popup/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/popup/__e2e__/templatesOfBug.ts index df02d6b9082..2d71859186c 100644 --- a/packages/core/client/src/modules/popup/__e2e__/templatesOfBug.ts +++ b/packages/core/client/src/modules/popup/__e2e__/templatesOfBug.ts @@ -4695,3 +4695,432 @@ export const shouldBackAfterClickBackButton = { 'x-index': 1, }, }; +export const zIndexOfSubpage = { + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + '54q3u7i1u4k': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: { + '3gn9anq7t4e': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.25-beta', + properties: { + '0gxjlcll8gp': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.25-beta', + properties: { + '7i3uif7wk62': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.3.25-beta', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.3.25-beta', + 'x-uid': 'afsi4hnbyuj', + 'x-async': false, + 'x-index': 1, + }, + zpgpevopc1c: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.3.25-beta', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.3.25-beta', + properties: { + '8u7j0st5wgs': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.3.25-beta', + properties: { + s2vaxfg91i2: { + 'x-uid': 'qh2sc6ljtem', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: 'open subpage level1', + 'x-action': 'view', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:view', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'page', + iconColor: '#1677FF', + danger: false, + }, + 'x-action-context': { + dataSource: 'main', + collection: 'users', + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("View record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Details")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:common:addBlock', + properties: { + '4qymqz8hwn4': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.25-beta', + properties: { + ihbvd00t4q6: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.25-beta', + properties: { + hxrps0ye253: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: false, + }, + 'x-acl-action': 'users:update', + 'x-decorator': 'FormBlockProvider', + 'x-use-decorator-props': + 'useEditFormBlockDecoratorProps', + 'x-decorator-props': { + action: 'get', + dataSource: 'main', + collection: 'users', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:editForm', + 'x-component': 'CardItem', + 'x-app-version': '1.3.25-beta', + properties: { + ql4garzgr8x: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useEditFormBlockProps', + 'x-app-version': '1.3.25-beta', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + 'x-app-version': '1.3.25-beta', + 'x-uid': 'mabh2ibgait', + 'x-async': false, + 'x-index': 1, + }, + ajprlhs22up: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'editForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + }, + 'x-app-version': '1.3.25-beta', + properties: { + b9cne10fcgs: { + 'x-uid': 'xl8j6o0nh9y', + _isJSONSchemaObject: true, + version: '2.0', + title: '{{ t("Submit") }}', + 'x-action': 'submit', + 'x-component': 'Action', + 'x-use-component-props': + 'useUpdateActionProps', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:updateSubmit', + 'x-component-props': { + type: 'primary', + htmlType: 'submit', + }, + 'x-action-settings': { + triggerWorkflows: [], + schemaUid: '1bxeyl4dr8h', + }, + type: 'void', + 'x-app-version': '1.3.25-beta', + 'x-async': false, + 'x-index': 1, + }, + tqyracw6eb6: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Popup") }}', + 'x-action': 'customize:popup', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:popup', + 'x-component': 'Action', + 'x-component-props': { + openMode: 'drawer', + refreshDataBlockRequest: true, + }, + 'x-action-context': { + dataSource: 'main', + collection: 'users', + }, + 'x-app-version': '1.3.25-beta', + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Popup") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + 'x-app-version': '1.3.25-beta', + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + 'x-app-version': '1.3.25-beta', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Details")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + 'x-app-version': '1.3.25-beta', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': + 'popup:common:addBlock', + 'x-app-version': + '1.3.25-beta', + 'x-uid': 'zwuqnls6u3c', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '55kyuw5ttyx', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ky6g3f2n28w', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'vp4s1dwnd8z', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'jo22w4l8an4', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '84yctndy38l', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'g6b2c8f2ovk', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'l3o4h95xmdl', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'heydlbpp6my', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'bulxu6j236w', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '72yx2p3t3la', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'uj053epotun', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'cehviykewuu', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'lnnbghowqab', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'nbvrk0k8yff', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'j5hb8ndh6ga', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'jkn0ynmjn3m', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'glwchpg5y33', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'fnefg28f6n0', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ct8jzsn4eip', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '8c4x82h3quo', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'iha0m2ozo2a', + 'x-async': true, + 'x-index': 1, + }, +}; diff --git a/packages/core/client/src/modules/popup/__e2e__/zIndex.test.ts b/packages/core/client/src/modules/popup/__e2e__/zIndex.test.ts index 0add00003e3..3d05489c244 100644 --- a/packages/core/client/src/modules/popup/__e2e__/zIndex.test.ts +++ b/packages/core/client/src/modules/popup/__e2e__/zIndex.test.ts @@ -8,10 +8,9 @@ */ import { expect, test } from '@nocobase/test/e2e'; -import { T2797, T2838 } from './templatesOfBug'; +import { T2797, T2838, zIndexOfSubpage } from './templatesOfBug'; test.describe('z-index of dialog', () => { - // https://nocobase.height.app/T-2797 test('edit block title', async ({ page, mockPage }) => { await mockPage(T2797).goto(); @@ -26,7 +25,6 @@ test.describe('z-index of dialog', () => { await expect(page.getByLabel('block-item-Input-users-Block')).not.toBeVisible(); }); - // https://nocobase.height.app/T-2838 test('multilevel modal', async ({ page, mockPage, mockRecord }) => { const nocoPage = await mockPage(T2838).waitForInit(); await mockRecord('general'); @@ -39,4 +37,40 @@ test.describe('z-index of dialog', () => { await page.getByLabel('action-Action-Submit-submit-').click(); await expect(page.getByLabel('block-item-CollectionField-')).not.toBeVisible(); }); + + test('z-index of subpage', async ({ page, mockPage }) => { + await mockPage(zIndexOfSubpage).goto(); + + // 1. Open the "Assign field values" popup in the subpage, it should be displayed on the top layer + await page.getByText('open subpage level1').click(); + await page.getByLabel('action-Action-Submit-submit-').hover(); + await page.getByLabel('designer-schema-settings-Action-actionSettings:updateSubmit-users').hover(); + await page.getByRole('menuitem', { name: 'Assign field values' }).click(); + await expect(page.getByRole('dialog').getByText('Assign field values')).toBeVisible(); + // If the click is ineffective, it means the popup is not displayed on the top layer + await page.getByRole('button', { name: 'Cancel' }).click({ timeout: 1000 }); + await expect(page.getByRole('dialog').getByText('Assign field values')).not.toBeVisible(); + }); + + test('Users & Permissions', async ({ page }) => { + await page.goto('/admin/settings/users-permissions/roles'); + + // Data source + await page.getByRole('tab', { name: 'Data sources' }).click(); + await page.getByLabel('action-Action.Link-Configure-').click(); + await page.getByRole('tab', { name: 'Action permissions', exact: true }).click(); + await page.getByLabel('action-Action.Link-Configure-dataSourcesCollections-users', { exact: true }).click(); + await page.getByLabel('Individual').check(); + await page + .getByTestId('drawer-Action.Drawer-dataSourcesCollections-Configure permission') + .getByRole('cell') + .locator('.ant-select-selector') + .first() + .click(); + await page + .getByTestId('drawer-RecordPicker.Selector-dataSourcesCollections-Select record') + .getByLabel('action-Action-Add new-create-') + .click(); + await expect(page.getByText('Add condition', { exact: true })).toBeVisible(); + }); }); diff --git a/packages/core/client/src/modules/variable/__e2e__/basic.test.ts b/packages/core/client/src/modules/variable/__e2e__/basic.test.ts index 28394eb95ab..901f4fbf2bc 100644 --- a/packages/core/client/src/modules/variable/__e2e__/basic.test.ts +++ b/packages/core/client/src/modules/variable/__e2e__/basic.test.ts @@ -8,7 +8,7 @@ */ import { expect, test } from '@nocobase/test/e2e'; -import { APIToken, tableViewLinkageRulesVariables } from './templates'; +import { APIToken, tableSelectedRecords, tableViewLinkageRulesVariables } from './templates'; test.describe('variables', () => { test('linkage rules of table view action', async ({ page, mockPage }) => { @@ -43,4 +43,32 @@ test.describe('variables', () => { await page.getByRole('button', { name: 'OK', exact: true }).click(); await expect(page.getByRole('textbox')).toHaveValue(token); }); + + test('Table selected records', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(tableSelectedRecords).waitForInit(); + const record = await mockRecord('testTableSelectedRecords'); + await nocoPage.goto(); + + // 1. First select a row, then click Add new + await page.getByLabel('table-index-1', { exact: true }).click(); + await page.getByLabel('action-Action-Add new-create-').click(); + + // 2. Set default value for the field, field content should be the associated records of the previously selected record + await page.getByLabel('block-item-CollectionField-').hover(); + await page.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-').hover(); + await page.getByRole('menuitem', { name: 'Set default value' }).click(); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'Table selected records right' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'm2m' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect(page.getByLabel('block-item-CollectionField-')).toHaveText( + `m2m:${record.m2m.map((r) => r.id).join('')}`, + ); + + // 3. Deselect the row, click Add new again, field content should be empty + await page.getByLabel('drawer-Action.Container-testTableSelectedRecords-Add record-mask').click(); + await page.getByLabel('table-index-1', { exact: true }).click(); + await page.getByLabel('action-Action-Add new-create-').click(); + await expect(page.getByLabel('block-item-CollectionField-')).toHaveText('m2m:'); + }); }); diff --git a/packages/core/client/src/modules/variable/__e2e__/templates.ts b/packages/core/client/src/modules/variable/__e2e__/templates.ts index b756b8a1b23..c502a0955a7 100644 --- a/packages/core/client/src/modules/variable/__e2e__/templates.ts +++ b/packages/core/client/src/modules/variable/__e2e__/templates.ts @@ -863,3 +863,437 @@ export const T4874 = { 'x-index': 1, }, }; +export const tableSelectedRecords = { + collections: [ + { + name: 'testTableSelectedRecords', + fields: [ + { + name: 'm2m', + interface: 'm2m', + target: 'testTableSelectedRecords', + }, + { + name: 'title', + interface: 'input', + }, + ], + }, + ], + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + bfbkuonks1e: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: { + bbpnv79ppud: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.25-beta', + properties: { + '11nn7v51v72': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.25-beta', + properties: { + '94x4wxloe4r': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'testTableSelectedRecords:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'testTableSelectedRecords', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.3.25-beta', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.3.25-beta', + properties: { + pxcv8n30d4o: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-action': 'create', + 'x-acl-action': 'create', + title: "{{t('Add new')}}", + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:addNew', + 'x-component': 'Action', + 'x-decorator': 'ACLActionProvider', + 'x-component-props': { + openMode: 'drawer', + type: 'primary', + component: 'CreateRecordAction', + icon: 'PlusOutlined', + }, + 'x-action-context': { + dataSource: 'main', + collection: 'testTableSelectedRecords', + }, + 'x-align': 'right', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-app-version': '1.3.25-beta', + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Add record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + 'x-app-version': '1.3.25-beta', + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + 'x-initializer-props': { + gridInitializer: 'popup:addNew:addBlock', + }, + 'x-app-version': '1.3.25-beta', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Add new")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + 'x-app-version': '1.3.25-beta', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:addNew:addBlock', + 'x-app-version': '1.3.25-beta', + properties: { + auahz06brj6: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.25-beta', + properties: { + abjk8e5yzfq: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.25-beta', + properties: { + ijm0brgvegg: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-acl-action': 'testTableSelectedRecords:create', + 'x-decorator': 'FormBlockProvider', + 'x-use-decorator-props': 'useCreateFormBlockDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + collection: 'testTableSelectedRecords', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:createForm', + 'x-component': 'CardItem', + 'x-app-version': '1.3.25-beta', + properties: { + lr839d1xqs7: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useCreateFormBlockProps', + 'x-app-version': '1.3.25-beta', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + 'x-app-version': '1.3.25-beta', + properties: { + '9mk2xu1lzbj': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.25-beta', + properties: { + '8bfxrqicaz4': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.25-beta', + properties: { + m2m: { + 'x-uid': 'iijs3xapp0r', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': + 'testTableSelectedRecords.m2m', + 'x-component-props': { + fieldNames: { + label: 'id', + value: 'id', + }, + }, + 'x-app-version': '1.3.25-beta', + default: null, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '1v2n8jvdprt', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'qpcykg8zxi4', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ovjbhso54ry', + 'x-async': false, + 'x-index': 1, + }, + wjx5a7h79ln: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'createForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + }, + 'x-app-version': '1.3.25-beta', + 'x-uid': '0bam1xybltd', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'z9hll5knsnw', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'h35uvzr6v1h', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ujw62wzvaza', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '5avddlitgwa', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ujmnybap6z8', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'pen8qmp14dk', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '72c5i3pnqy1', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'niditkbbb28', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ziy0tpkmr0q', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'yigduqp0evh', + 'x-async': false, + 'x-index': 1, + }, + '5ify9wgqk4q': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.3.25-beta', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.3.25-beta', + properties: { + '4daepz6lwyh': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.3.25-beta', + 'x-uid': '348pidu78op', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'nxv4u82cosx', + 'x-async': false, + 'x-index': 1, + }, + '4pwwe3tqhip': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-app-version': '1.3.25-beta', + properties: { + m2m: { + _isJSONSchemaObject: true, + version: '2.0', + 'x-collection-field': 'testTableSelectedRecords.m2m', + 'x-component': 'CollectionField', + 'x-component-props': { + fieldNames: { + value: 'id', + label: 'id', + }, + ellipsis: true, + size: 'small', + }, + 'x-read-pretty': true, + 'x-decorator': null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': '1.3.25-beta', + 'x-uid': 'n1t3oaopwi4', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'pw0sv35l2ty', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '3sscxqoxcqx', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'l25cpwxqdx7', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ftqzihfzwp4', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'twnr4ap7vk6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'oohol9ikhrl', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '3e4ienzcipu', + 'x-async': true, + 'x-index': 1, + }, +}; diff --git a/packages/core/client/src/nocobase-buildin-plugin/index.tsx b/packages/core/client/src/nocobase-buildin-plugin/index.tsx index e846be160ae..a27b7414824 100644 --- a/packages/core/client/src/nocobase-buildin-plugin/index.tsx +++ b/packages/core/client/src/nocobase-buildin-plugin/index.tsx @@ -142,7 +142,7 @@ const getProps = (app: Application) => { }; } - if (app.error.code === 'APP_ERROR' || app.error.code === 'LOAD_ERROR') { + if (['ENOENT', 'APP_ERROR', 'LOAD_ERROR'].includes(app.error.code)) { return { status: 'error', title: 'App error', @@ -205,7 +205,11 @@ const getProps = (app: Application) => { return { ...props, ...commands[app.error?.command?.name] }; } - return {}; + return { + status: 'warning', + title: 'App warning', + subTitle: app.error?.message, + }; }; const AppMaintaining: FC<{ app: Application; error: Error }> = observer( diff --git a/packages/core/client/src/pm/PluginCard.tsx b/packages/core/client/src/pm/PluginCard.tsx index 4ee053ac051..9971006f8e6 100644 --- a/packages/core/client/src/pm/PluginCard.tsx +++ b/packages/core/client/src/pm/PluginCard.tsx @@ -7,14 +7,13 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { App, Card, Divider, Popconfirm, Space, Switch, Typography, message } from 'antd'; +import { DeleteOutlined, LoadingOutlined, ReadOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons'; +import { css } from '@emotion/css'; +import { App, Card, Divider, Modal, Popconfirm, Result, Space, Switch, Typography } from 'antd'; import classnames from 'classnames'; import React, { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; - -import { DeleteOutlined, ReadOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons'; -import { css } from '@emotion/css'; import { useAPIClient } from '../api-client'; import { useApp } from '../application'; import { PluginDetail } from './PluginDetail'; @@ -29,8 +28,19 @@ interface IPluginInfo extends IPluginCard { function PluginInfo(props: IPluginInfo) { const { data, onClick } = props; const app = useApp(); - const { name, displayName, isCompatible, packageName, updatable, builtIn, enabled, description, error, homepage } = - data; + const { + name, + displayName, + isCompatible, + packageName, + updatable, + builtIn, + enabled, + removable, + description, + error, + homepage, + } = data; const { styles, theme } = useStyles(); const navigate = useNavigate(); const { t } = useTranslation(); @@ -109,30 +119,58 @@ function PluginInfo(props: IPluginInfo) { {t('Update')} )} - {enabled ? ( - app.pluginSettingsManager.has(name) && ( - { - e.stopPropagation(); - navigate(app.pluginSettingsManager.getRoutePath(name)); - }} - > - {t('Settings')} - - ) - ) : ( + {enabled && app.pluginSettingsManager.has(name) && ( + { + e.stopPropagation(); + navigate(app.pluginSettingsManager.getRoutePath(name)); + }} + > + {t('Settings')} + + )} + {removable && ( { e.stopPropagation(); - api.request({ + await api.request({ url: `pm:remove`, params: { filterByTk: name, }, }); + Modal.info({ + icon: null, + width: 520, + content: ( + } + title={t('Plugin removing')} + subTitle={t('Plugin is removing, please wait...')} + /> + ), + footer: null, + }); + function __health_check() { + api + .silent() + .request({ + url: `__health_check`, + method: 'get', + }) + .then((response) => { + if (response?.data === 'ok') { + window.location.reload(); + } + }) + .catch((error) => { + // console.error('Health check failed:', error); + }); + } + setInterval(__health_check, 1000); }} onCancel={(e) => e.stopPropagation()} okText={t('Yes')} @@ -157,7 +195,20 @@ function PluginInfo(props: IPluginInfo) { onChange={async (checked, e) => { e.stopPropagation(); if (!isCompatible && checked) { - message.error(t("Dependencies check failed, can't enable.")); + modal.confirm({ + title: t('Plugin dependency version mismatch'), + content: t( + 'The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?', + ), + onOk: async () => { + await api.request({ + url: `pm:enable`, + params: { + filterByTk: name, + }, + }); + }, + }); return; } if (!checked) { @@ -202,34 +253,6 @@ function PluginInfo(props: IPluginInfo) { ) } /> - {/* {!isCompatible && !error && ( - - )} */} - {/* -
- - {type && ( - - )} - - {!error && ( - - )} - - */} ); diff --git a/packages/core/client/src/pm/PluginDetail.tsx b/packages/core/client/src/pm/PluginDetail.tsx index 343e839a69a..46d45570e25 100644 --- a/packages/core/client/src/pm/PluginDetail.tsx +++ b/packages/core/client/src/pm/PluginDetail.tsx @@ -13,7 +13,6 @@ import relativeTime from 'dayjs/plugin/relativeTime'; import React, { FC, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useRequest } from '../api-client'; -import { PluginDocument } from './PluginDocument'; import { useStyles } from './style'; import { IPluginData } from './types'; @@ -121,7 +120,7 @@ export const PluginDetail: FC = ({ plugin, onCancel }) => { children: ( {plugin.name && ( - +
{t('Name')} {plugin.name} @@ -129,7 +128,7 @@ export const PluginDetail: FC = ({ plugin, onCancel }) => { )} {plugin.displayName && ( -
+
{t('DisplayName')} {plugin.displayName} @@ -169,7 +168,7 @@ export const PluginDetail: FC = ({ plugin, onCancel }) => { )} {data?.data?.packageJson.license && ( -
+
{t('License')} {data?.data?.packageJson.license} @@ -177,20 +176,14 @@ export const PluginDetail: FC = ({ plugin, onCancel }) => { )} {author && ( -
+
{t('Author')} {author}
)} - -
- {t('Last updated')} - {dayjs(data?.data?.lastUpdated).fromNow()} -
- - +
{t('Version')} {plugin?.version} @@ -231,11 +224,11 @@ export const PluginDetail: FC = ({ plugin, onCancel }) => { ), }, - { - key: 'changelog', - label: t('Changelog'), - children: plugin?.changelogUrl ? : t('No CHANGELOG.md file'), - }, + // { + // key: 'changelog', + // label: t('Changelog'), + // children: plugin?.changelogUrl ? : t('No CHANGELOG.md file'), + // }, ]; return ( @@ -248,9 +241,6 @@ export const PluginDetail: FC = ({ plugin, onCancel }) => { {plugin.packageName}  • }> {plugin.version} - - {t('Last updated')} {dayjs(data?.data?.lastUpdated).fromNow()} - void; isUpgrade: boolean; @@ -40,12 +43,41 @@ export const PluginUploadForm: FC = ({ onClose, pluginDa if (pluginData?.packageName) { formData.append('packageName', pluginData.packageName); } - api.request({ + await api.request({ url: `pm:${isUpgrade ? 'update' : 'add'}`, method: 'post', data: formData, }); + Modal.info({ + icon: null, + width: 520, + content: ( + } + title={t('Plugin uploading')} + subTitle={t('Plugin is uploading, please wait...')} + /> + ), + footer: null, + }); onClose(true); + function __health_check() { + api + .silent() + .request({ + url: `__health_check`, + method: 'get', + }) + .then((response) => { + if (response?.data === 'ok') { + window.location.reload(); + } + }) + .catch((error) => { + // console.error('Health check failed:', error); + }); + } + setInterval(__health_check, 1000); }, }; }; diff --git a/packages/core/client/src/pm/PluginForm/modal/PluginAddModal.tsx b/packages/core/client/src/pm/PluginForm/modal/PluginAddModal.tsx index a312b05bfc5..c78190b7256 100644 --- a/packages/core/client/src/pm/PluginForm/modal/PluginAddModal.tsx +++ b/packages/core/client/src/pm/PluginForm/modal/PluginAddModal.tsx @@ -27,7 +27,7 @@ export const PluginAddModal: FC = ({ onClose, isShow }) => { const [type, setType] = useState<'npm' | 'upload' | 'url'>('npm'); return ( - onClose()} footer={null} destroyOnClose title={t('Add plugin')} width={580} open={isShow}> + onClose()} footer={null} destroyOnClose title={t('Add & update')} width={580} open={isShow}> {/* */}
setType(e.target.value)}> diff --git a/packages/core/client/src/pm/PluginManager.tsx b/packages/core/client/src/pm/PluginManager.tsx index 0b53d8ccbec..112f41a56ad 100644 --- a/packages/core/client/src/pm/PluginManager.tsx +++ b/packages/core/client/src/pm/PluginManager.tsx @@ -100,6 +100,7 @@ const LocalPlugins = () => { 'Workflow', 'Users & permissions', 'Authentication', + 'Notification', 'System management', 'Logging and monitoring', 'Others', @@ -197,7 +198,7 @@ const LocalPlugins = () => {
diff --git a/packages/core/client/src/pm/types.ts b/packages/core/client/src/pm/types.ts index b82d588de28..d2f58650bd7 100644 --- a/packages/core/client/src/pm/types.ts +++ b/packages/core/client/src/pm/types.ts @@ -16,6 +16,7 @@ export interface IPluginData { packageName: string; version: string; enabled: boolean; + removable?: boolean; installed: boolean; builtIn: boolean; registry?: string; diff --git a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx index a81d461af67..2557af79b68 100644 --- a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx +++ b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx @@ -429,15 +429,33 @@ export const InternalAdminLayout = () => { align-items: center; `} > - + {result?.data?.data?.logo?.url ? ( + + ) : ( + + {result?.data?.data?.title} + + )}
{ const { getComponentByOpenMode, defaultOpenMode } = useOpenModeContext(); const { openMode = defaultOpenMode } = useActionContext(); - const { currentLevel } = useCurrentPopupContext(); + const popupLevel = React.useContext(PopupLevelContext); + const currentLevel = popupLevel + 1; const Component = getComponentByOpenMode(openMode); - return ; + return ( + + + + ); }, { displayName: 'ActionContainer' }, ); diff --git a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx index 5862d10a376..0d5446fbec9 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx @@ -8,7 +8,7 @@ */ import { ArrayTable } from '@formily/antd-v5'; -import { Field, onFieldValueChange } from '@formily/core'; +import { onFieldValueChange } from '@formily/core'; import { ISchema, useField, useFieldSchema, useForm, useFormEffects } from '@formily/react'; import { isValid, uid } from '@formily/shared'; import { Alert, Flex, ModalProps, Tag } from 'antd'; @@ -251,14 +251,28 @@ export function SkipValidation() { /> ); } + export function AfterSuccess() { const { dn } = useDesignable(); const { t } = useTranslation(); const fieldSchema = useFieldSchema(); + const { onSuccess } = fieldSchema?.['x-action-settings'] || {}; return ( - workflowPlugin - .getTriggersOptions() + triggerOptions .filter((item) => { return typeof item.options.isActionTriggerable === 'function' || item.options.isActionTriggerable === true; }) .map((item) => item.value), - [workflowPlugin], + [triggerOptions], ); useFormEffects(() => { @@ -435,7 +469,7 @@ function WorkflowSelect({ formAction, buttonAction, actionType, ...props }) { }} optionFilter={optionFilter} optionRender={({ label, data }) => { - const typeOption = workflowPlugin.getTriggersOptions().find((item) => item.value === data.type); + const typeOption = triggerOptions.find((item) => item.value === data.type); return typeOption ? ( {label} @@ -819,34 +853,80 @@ export function SecondConFirm() { const { dn } = useDesignable(); const fieldSchema = useFieldSchema(); const { t } = useTranslation(); - const field = useField(); + const field = useField(); + const compile = useCompile(); return ( - { - if (!fieldSchema['x-component-props']) { - fieldSchema['x-component-props'] = {}; - } - if (value) { - fieldSchema['x-component-props'].confirm = value - ? { - title: 'Perform the {{title}}', - content: 'Are you sure you want to perform the {{title}} action?', - } - : {}; - } else { - fieldSchema['x-component-props'].confirm = {}; - } - field.componentProps.confirm = { ...fieldSchema['x-component-props']?.confirm }; + initialValues={{ + title: + compile(fieldSchema?.['x-component-props']?.confirm?.title) || + t('Perform the {{title}}', { title: compile(fieldSchema.title) }), + content: + compile(fieldSchema?.['x-component-props']?.confirm?.content) || + t('Are you sure you want to perform the {{title}} action?', { title: compile(fieldSchema.title) }), + }} + schema={ + { + type: 'object', + title: t('Secondary confirmation'), + properties: { + enable: { + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': t('Enable secondary confirmation'), + default: + fieldSchema?.['x-component-props']?.confirm?.enable !== false && + !!fieldSchema?.['x-component-props']?.confirm?.content, + 'x-component-props': {}, + }, + title: { + 'x-decorator': 'FormItem', + 'x-component': 'Input.TextArea', + title: t('Title'), + 'x-reactions': { + dependencies: ['enable'], + fulfill: { + state: { + required: '{{$deps[0]}}', + }, + }, + }, + }, + content: { + 'x-decorator': 'FormItem', + 'x-component': 'Input.TextArea', + title: t('Content'), + 'x-reactions': { + dependencies: ['enable'], + fulfill: { + state: { + required: '{{$deps[0]}}', + }, + }, + }, + }, + }, + } as ISchema + } + onSubmit={({ enable, title, content }) => { + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props'].confirm = {}; + fieldSchema['x-component-props'].confirm.enable = enable; + fieldSchema['x-component-props'].confirm.title = title; + fieldSchema['x-component-props'].confirm.content = content; + field.componentProps.confirm = { ...fieldSchema['x-component-props']?.confirm }; dn.emit('patch', { schema: { ['x-uid']: fieldSchema['x-uid'], - 'x-component-props': { ...fieldSchema['x-component-props'] }, + 'x-component-props': { + ...fieldSchema['x-component-props'], + }, }, }); + dn.refresh(); }} /> ); diff --git a/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx b/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx index 3a560d2f198..4a1c7da78f9 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx @@ -14,10 +14,12 @@ import React, { useMemo } from 'react'; import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; import { ErrorFallback } from '../error-fallback'; import { useCurrentPopupContext } from '../page/PagePopups'; +import { TabsContextProvider, useTabsContext } from '../tabs/context'; import { useStyles } from './Action.Drawer.style'; import { useActionContext } from './hooks'; import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer'; import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types'; +import { useZIndexContext, zIndexContext } from './zIndexContext'; const DrawerErrorFallback: React.FC = (props) => { const { visible, setVisible } = useActionContext(); @@ -40,6 +42,8 @@ export const InternalActionDrawer: React.FC = observer( const schema = useFieldSchema(); const field = useField(); const { componentCls, hashId } = useStyles(); + const tabContext = useTabsContext(); + const parentZIndex = useZIndexContext(); const footerSchema = schema.reduceProperties((buf, s) => { if (s['x-component'] === footerNodeName) { return s; @@ -59,41 +63,48 @@ export const InternalActionDrawer: React.FC = observer( useSetAriaLabelForDrawer(visible); } + const zIndex = parentZIndex + (props.level || 0); + return ( - setVisible(false, true)} - rootClassName={classNames(componentCls, hashId, drawerProps?.className, others.className, 'reset')} - footer={ - footerSchema && ( -
- { - return s['x-component'] === footerNodeName; - }} - /> -
- ) - } - > - { - return s['x-component'] !== footerNodeName; - }} - /> -
+ + + setVisible(false, true)} + rootClassName={classNames(componentCls, hashId, drawerProps?.className, others.className, 'reset')} + footer={ + footerSchema && ( +
+ { + return s['x-component'] === footerNodeName; + }} + /> +
+ ) + } + > + { + return s['x-component'] !== footerNodeName; + }} + /> +
+
+
); }, { displayName: 'ActionDrawer' }, diff --git a/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx b/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx index 938bc4105b9..bf054edf250 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx @@ -16,9 +16,11 @@ import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; import { useToken } from '../../../style'; import { ErrorFallback } from '../error-fallback'; import { useCurrentPopupContext } from '../page/PagePopups'; +import { TabsContextProvider, useTabsContext } from '../tabs/context'; import { useActionContext } from './hooks'; import { useSetAriaLabelForModal } from './hooks/useSetAriaLabelForModal'; import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types'; +import { useZIndexContext, zIndexContext } from './zIndexContext'; const ModalErrorFallback: React.FC = (props) => { const { visible, setVisible } = useActionContext(); @@ -44,6 +46,8 @@ export const InternalActionModal: React.FC> = obse const form = useForm(); const field = useField(); const { token } = useToken(); + const tabContext = useTabsContext(); + const parentZIndex = useZIndexContext(); const footerSchema = schema.reduceProperties((buf, s) => { if (s['x-component'] === footerNodeName) { return s; @@ -67,76 +71,83 @@ export const InternalActionModal: React.FC> = obse useSetAriaLabelForModal(visible); } + const zIndex = parentZIndex + (props.level || 0); + return ( - { - setVisible(false, true); - form.reset(); - }} - className={classNames( - others.className, - modalProps?.className, - css` - &.nb-action-popup { - .ant-modal-header { - display: none; - } + + + { + setVisible(false, true); + form.reset(); + }} + className={classNames( + others.className, + modalProps?.className, + css` + &.nb-action-popup { + .ant-modal-header { + display: none; + } - .ant-modal-content { - background: var(--nb-box-bg); - border: 1px solid rgba(255, 255, 255, 0.1); - padding-bottom: 0; - } + .ant-modal-content { + background: var(--nb-box-bg); + border: 1px solid rgba(255, 255, 255, 0.1); + padding-bottom: 0; + } - // 这里的样式是为了保证页面 tabs 标签下面的分割线和页面内容对齐(页面内边距可以通过主题编辑器调节) - .ant-tabs-nav { - padding-left: ${token.paddingLG - token.paddingPageHorizontal}px; - padding-right: ${token.paddingLG - token.paddingPageHorizontal}px; - margin-left: ${token.paddingPageHorizontal - token.paddingLG}px; - margin-right: ${token.paddingPageHorizontal - token.paddingLG}px; - } + // 这里的样式是为了保证页面 tabs 标签下面的分割线和页面内容对齐(页面内边距可以通过主题编辑器调节) + .ant-tabs-nav { + padding-left: ${token.paddingLG - token.paddingPageHorizontal}px; + padding-right: ${token.paddingLG - token.paddingPageHorizontal}px; + margin-left: ${token.paddingPageHorizontal - token.paddingLG}px; + margin-right: ${token.paddingPageHorizontal - token.paddingLG}px; + } - .ant-modal-footer { - display: ${showFooter ? 'block' : 'none'}; - } + .ant-modal-footer { + display: ${showFooter ? 'block' : 'none'}; + } + } + `, + )} + footer={ + showFooter ? ( + { + return s['x-component'] === footerNodeName; + }} + /> + ) : ( + false + ) } - `, - )} - footer={ - showFooter ? ( + > { - return s['x-component'] === footerNodeName; + return s['x-component'] !== footerNodeName; }} /> - ) : ( - false - ) - } - > - { - return s['x-component'] !== footerNodeName; - }} - /> - + + + ); }, { displayName: 'ActionModal' }, diff --git a/packages/core/client/src/schema-component/antd/action/Action.Page.tsx b/packages/core/client/src/schema-component/antd/action/Action.Page.tsx index 885d1e93e6f..427accb25cc 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Page.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Page.tsx @@ -15,6 +15,7 @@ import { BackButtonUsedInSubPage } from '../page/BackButtonUsedInSubPage'; import { TabsContextProvider, useTabsContext } from '../tabs/context'; import { useActionPageStyle } from './Action.Page.style'; import { usePopupOrSubpagesContainerDOM } from './hooks/usePopupSlotDOM'; +import { useZIndexContext, zIndexContext } from './zIndexContext'; export function ActionPage({ level }) { const filedSchema = useFieldSchema(); @@ -22,13 +23,13 @@ export function ActionPage({ level }) { const { getContainerDOM } = usePopupOrSubpagesContainerDOM(); const { styles } = useActionPageStyle(); const tabContext = useTabsContext(); + const parentZIndex = useZIndexContext(); const style = useMemo(() => { return { - // 20 is the z-index value of the main page - zIndex: 20 + level, + zIndex: parentZIndex + (level || 0), }; - }, [level]); + }, [parentZIndex, level]); if (!ctx.visible) { return null; @@ -37,18 +38,16 @@ export function ActionPage({ level }) { const actionPageNode = (
}> - + + +
); const container = getContainerDOM(); - if (container) { - return createPortal(actionPageNode, container); - } - - return actionPageNode; + return createPortal(actionPageNode, container || document.body); } ActionPage.Footer = observer( diff --git a/packages/core/client/src/schema-component/antd/action/Action.tsx b/packages/core/client/src/schema-component/antd/action/Action.tsx index 773c9966231..3174959b976 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.tsx @@ -55,7 +55,6 @@ export const Action: ComposedAction = withDynamicSchemaProps( observer((props: ActionProps) => { const { popover, - confirm, containerRefKey, component, useAction = useA, @@ -68,10 +67,12 @@ export const Action: ComposedAction = withDynamicSchemaProps( openSize: os, disabled: propsDisabled, actionCallback, + confirm: propsConfirm, /** 如果为 true 则说明该按钮是树表格的 Add child 按钮 */ addChild, onMouseEnter, refreshDataBlockRequest: propsRefreshDataBlockRequest, + confirmTitle, ...others } = useProps(props); // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema const aclCtx = useACLActionParamsContext(); @@ -93,7 +94,7 @@ export const Action: ComposedAction = withDynamicSchemaProps( const openMode = fieldSchema?.['x-component-props']?.['openMode']; const openSize = fieldSchema?.['x-component-props']?.['openSize']; const refreshDataBlockRequest = fieldSchema?.['x-component-props']?.['refreshDataBlockRequest']; - + const confirm = compile(fieldSchema['x-component-props']?.confirm) || propsConfirm; const disabled = form.disabled || field.disabled || field.data?.disabled || propsDisabled || disableAction; const linkageRules = useMemo(() => fieldSchema?.['x-linkage-rules'] || [], [fieldSchema?.['x-linkage-rules']]); const { designable } = useDesignable(); @@ -172,10 +173,11 @@ export const Action: ComposedAction = withDynamicSchemaProps( run, confirm, modal, + setSubmitted: setParentSubmitted, + confirmTitle, }; const buttonElement = RenderButton(buttonProps); - // if (!btnHover) { // return buttonElement; // } @@ -304,11 +306,12 @@ function RenderButton({ run, confirm, modal, + setSubmitted, + confirmTitle, }) { const { t } = useTranslation(); const { isPopupVisibleControlledByURL } = usePopupSettings(); const { openPopup } = usePopupUtils(); - const handleButtonClick = useCallback( (e: React.MouseEvent, checkPortal = true) => { if (checkPortal && isPortalInBody(e.target as Element)) { @@ -322,6 +325,7 @@ function RenderButton({ if (onClick) { onClick(e, () => { if (refreshDataBlockRequest !== false) { + setSubmitted?.(true); service?.refresh?.(); } }); @@ -329,8 +333,8 @@ function RenderButton({ setVisible(true); run?.(); } else { + // Currently, only buttons of these types can control the visibility of popups through URLs. if ( - // Currently, only buttons of these types can control the visibility of popups through URLs. ['view', 'update', 'create', 'customize:popup'].includes(fieldSchema['x-action']) && fieldSchema['x-uid'] ) { @@ -341,10 +345,10 @@ function RenderButton({ } } }; - if (confirm?.content) { + if (confirm?.enable !== false && confirm?.content) { modal.confirm({ - title: t(confirm.title, { title: actionTitle }), - content: t(confirm.content, { title: actionTitle }), + title: t(confirm.title, { title: confirmTitle || actionTitle }), + content: t(confirm.content, { title: confirmTitle || actionTitle }), onOk, }); } else { @@ -357,6 +361,7 @@ function RenderButton({ actionTitle, confirm?.content, confirm?.title, + confirm?.enable, disabled, modal, onClick, diff --git a/packages/core/client/src/schema-component/antd/action/context.tsx b/packages/core/client/src/schema-component/antd/action/context.tsx index eddcfc62377..14617b3b493 100644 --- a/packages/core/client/src/schema-component/antd/action/context.tsx +++ b/packages/core/client/src/schema-component/antd/action/context.tsx @@ -8,7 +8,9 @@ */ import { useFieldSchema } from '@formily/react'; -import React, { createContext, useEffect, useState } from 'react'; +import _ from 'lodash'; +import React, { createContext, useEffect, useMemo, useRef, useState } from 'react'; +import { useParams } from 'react-router-dom'; import { useDataBlockRequest } from '../../../data-source'; import { useCurrentPopupContext } from '../page/PagePopups'; import { getBlockService, storeBlockService } from '../page/pagePopupUtils'; @@ -17,22 +19,48 @@ import { ActionContextProps } from './types'; export const ActionContext = createContext({}); ActionContext.displayName = 'ActionContext'; +/** + * Used to determine if the user closed the sub-page by clicking on the page menu + * @returns + */ +const useIsSubPageClosedByPageMenu = () => { + // Used to trigger re-rendering when URL changes + const params = useParams(); + const prevParamsRef = useRef({}); + const fieldSchema = useFieldSchema(); + + const isSubPageClosedByPageMenu = useMemo(() => { + const result = + _.isEmpty(params['*']) && + fieldSchema?.['x-component-props']?.openMode === 'page' && + !!prevParamsRef.current['*']?.includes(fieldSchema['x-uid']); + + prevParamsRef.current = params; + + return result; + }, [fieldSchema, params]); + + return isSubPageClosedByPageMenu; +}; + export const ActionContextProvider: React.FC = (props) => { const [submitted, setSubmitted] = useState(false); //是否有提交记录 const { visible } = { ...props, ...props.value } || {}; const { setSubmitted: setParentSubmitted } = { ...props, ...props.value }; const service = useBlockServiceInActionButton(); + const isSubPageClosedByPageMenu = useIsSubPageClosedByPageMenu(); useEffect(() => { - if (visible === false && submitted && service) { + if (visible === false && service && !service.loading && (submitted || isSubPageClosedByPageMenu)) { service.refresh(); + service.loading = true; setParentSubmitted?.(true); //传递给上一层 } return () => { setSubmitted(false); }; - }, [visible, service?.refresh, setParentSubmitted]); + }, [visible, service?.refresh, setParentSubmitted, isSubPageClosedByPageMenu]); return ( diff --git a/packages/core/client/src/schema-component/antd/action/zIndexContext.tsx b/packages/core/client/src/schema-component/antd/action/zIndexContext.tsx new file mode 100644 index 00000000000..76721e13a31 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/action/zIndexContext.tsx @@ -0,0 +1,16 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import React from 'react'; + +export const zIndexContext = React.createContext(100); + +export const useZIndexContext = () => { + return React.useContext(zIndexContext); +}; diff --git a/packages/core/client/src/schema-component/antd/association-field/AssociationFieldModeProvider.tsx b/packages/core/client/src/schema-component/antd/association-field/AssociationFieldModeProvider.tsx index 59cae720f05..b45faa3a0f1 100644 --- a/packages/core/client/src/schema-component/antd/association-field/AssociationFieldModeProvider.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/AssociationFieldModeProvider.tsx @@ -7,7 +7,6 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import _ from 'lodash'; import React, { createContext, useCallback, useMemo } from 'react'; import { AssociationSelect } from './AssociationSelect'; import { InternalFileManager } from './FileManager'; @@ -29,7 +28,7 @@ export enum AssociationFieldMode { } interface AssociationFieldModeProviderProps { - modeToComponent: Partial React.FC)>>; + modeToComponent: Partial>; } const defaultModeToComponent = { @@ -45,11 +44,15 @@ const defaultModeToComponent = { const AssociationFieldModeContext = createContext<{ modeToComponent: AssociationFieldModeProviderProps['modeToComponent']; getComponent: (mode: AssociationFieldMode) => React.FC; + getDefaultComponent: (mode: AssociationFieldMode) => React.FC; }>({ modeToComponent: defaultModeToComponent, getComponent: (mode: AssociationFieldMode) => { return defaultModeToComponent[mode]; }, + getDefaultComponent: (mode: AssociationFieldMode) => { + return defaultModeToComponent[mode]; + }, }); export const AssociationFieldModeProvider: React.FC = (props) => { @@ -61,18 +64,19 @@ export const AssociationFieldModeProvider: React.FC { - const component = modeToComponent[mode]; - - if (_.isFunction(component)) { - return component(defaultModeToComponent[mode]) as React.FC; - } - - return component || defaultModeToComponent[mode]; + return modeToComponent[mode] || defaultModeToComponent[mode]; }, [modeToComponent], ); - const value = useMemo(() => ({ modeToComponent, getComponent }), [getComponent, modeToComponent]); + const getDefaultComponent = useCallback((mode: AssociationFieldMode) => { + return defaultModeToComponent[mode]; + }, []); + + const value = useMemo( + () => ({ modeToComponent, getComponent, getDefaultComponent }), + [getComponent, modeToComponent, getDefaultComponent], + ); return {props.children}; }; diff --git a/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx b/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx index dee73be25dc..4097ce05916 100644 --- a/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx @@ -41,7 +41,7 @@ export const filterAnalyses = (filters): any[] => { if (!operator) { return true; } - const regex = /\{\{\$(?:[a-zA-Z_]\w*)\.([a-zA-Z_]\w*)(?:\.id)?\}\}/; + const regex = /\{\{\$[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*\.(\w+)\.id\}\}/; const fieldName = jsonlogic?.value?.match?.(regex)?.[1]; if (fieldName) { results.push(fieldName); @@ -79,9 +79,14 @@ const InternalAssociationSelect = observer( //支持深层次子表单 onFieldInputValueChange('*', (fieldPath: any) => { const linkageFields = filterAnalyses(field.componentProps?.service?.params?.filter) || []; - if (linkageFields.includes(fieldPath?.props?.name) && field.value) { - field.setValue(field.initialValue); - setInnerValue(field.initialValue); + if ( + linkageFields.includes(fieldPath?.props?.name) && + field.value && + fieldPath?.indexes?.[0] === field?.indexes?.[0] && + fieldPath?.props?.name !== field.props.name + ) { + field.setValue(undefined); + setInnerValue(undefined); } }); }); diff --git a/packages/core/client/src/schema-component/antd/association-field/FileManager.tsx b/packages/core/client/src/schema-component/antd/association-field/FileManager.tsx index 48daacd75e9..75b8494cd9b 100644 --- a/packages/core/client/src/schema-component/antd/association-field/FileManager.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/FileManager.tsx @@ -82,14 +82,13 @@ const useTableSelectorProps = () => { }; function FileSelector(props) { - const { disabled, multiple, value, onChange, action, onSelect, quickUpload, selectFile } = props; + const { disabled, multiple, value, onChange, action, onSelect, quickUpload, selectFile, ...other } = props; const { wrapSSR, hashId, componentCls: prefixCls } = useStyles(); const { useFileCollectionStorageRules } = useExpressionScope(); const { t } = useTranslation(); const rules = useFileCollectionStorageRules(); // 兼容旧版本 const showSelectButton = selectFile === undefined && quickUpload === undefined; - return wrapSSR(
@@ -122,6 +121,8 @@ function FileSelector(props) { onChange={onChange} action={action} rules={rules} + disabled={disabled} + {...other} /> ) : null} {selectFile && (multiple || !value) ? ( @@ -216,6 +217,7 @@ const InternalFileManager = (props) => { return (
{ ); }); -export { FileManageReadPretty, InternalFileManager }; +export { FileManageReadPretty, InternalFileManager, FileSelector }; diff --git a/packages/core/client/src/schema-component/antd/association-field/InternalPicker.tsx b/packages/core/client/src/schema-component/antd/association-field/InternalPicker.tsx index 81acd1b72b2..d7617d33272 100644 --- a/packages/core/client/src/schema-component/antd/association-field/InternalPicker.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/InternalPicker.tsx @@ -51,9 +51,6 @@ export const useTableSelectorProps = () => { rowKey, rowSelection: { type: multiple ? 'checkbox' : 'radio', - selectedRowKeys: rcSelectRows - ?.filter((item) => options.every((row) => row[rowKey] !== item[rowKey])) - .map((item) => item[rowKey]), }, onRowSelectionChange(selectedRowKeys, selectedRows) { if (multiple) { diff --git a/packages/core/client/src/schema-component/antd/association-field/InternalViewer.tsx b/packages/core/client/src/schema-component/antd/association-field/InternalViewer.tsx index 87427f36606..a1742a6aa20 100644 --- a/packages/core/client/src/schema-component/antd/association-field/InternalViewer.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/InternalViewer.tsx @@ -9,10 +9,11 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { toArr } from '@formily/shared'; +import _ from 'lodash'; import React, { FC, Fragment, useRef, useState } from 'react'; import { useDesignable } from '../../'; import { WithoutTableFieldResource } from '../../../block-provider'; -import { useCollectionManager, useCollectionRecordData } from '../../../data-source'; +import { CollectionRecordProvider, useCollectionManager, useCollectionRecordData } from '../../../data-source'; import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider'; import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider'; import { useCompile } from '../../hooks'; @@ -133,6 +134,25 @@ interface ReadPrettyInternalViewerProps { }; } +/** + * the sourceData is used to get the sourceId + * @param recordData + * @param fieldSchema + * @returns + */ +const getSourceData = (recordData, fieldSchema) => { + const sourceRecordKey = (fieldSchema.name as string) + .split('.') + .filter((o, i, arr) => i < arr.length - 1) + .join('.'); + + if (!sourceRecordKey) { + return recordData; + } + + return _.get(recordData, sourceRecordKey); +}; + export const ReadPrettyInternalViewer: React.FC = observer( (props: ReadPrettyInternalViewerProps) => { const { value, ButtonList = ButtonLinkList } = props; @@ -146,10 +166,13 @@ export const ReadPrettyInternalViewer: React.FC = observer( const { visibleWithURL, setVisibleWithURL } = usePopupUtils(); const [btnHover, setBtnHover] = useState(!!visibleWithURL); const { defaultOpenMode } = useOpenModeContext(); + const recordData = useCollectionRecordData(); const btnElement = ( - + + + ); diff --git a/packages/core/client/src/schema-component/antd/association-field/Nester.tsx b/packages/core/client/src/schema-component/antd/association-field/Nester.tsx index e05880a826b..3d219697e92 100644 --- a/packages/core/client/src/schema-component/antd/association-field/Nester.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/Nester.tsx @@ -10,12 +10,12 @@ import { CloseOutlined, PlusOutlined } from '@ant-design/icons'; import { css } from '@emotion/css'; import { ArrayField } from '@formily/core'; +import { spliceArrayState } from '@formily/core/esm/shared/internals'; import { RecursionField, observer, useFieldSchema } from '@formily/react'; import { action } from '@formily/reactive'; import { each } from '@formily/shared'; import { Button, Card, Divider, Tooltip } from 'antd'; import React, { useCallback, useContext } from 'react'; -import { spliceArrayState } from '@formily/core/esm/shared/internals'; import { useTranslation } from 'react-i18next'; import { FormActiveFieldsProvider } from '../../../block-provider/hooks/useFormActiveFields'; import { useCollection } from '../../../data-source'; @@ -58,6 +58,7 @@ const ToOneNester = (props) => { const { field } = useAssociationFieldContext(); const recordV2 = useCollectionRecord(); const collection = useCollection(); + const fieldSchema = useFieldSchema(); const isAllowToSetDefaultValue = useCallback( ({ form, fieldSchema, collectionField, getInterface, formBlockType }: IsAllowToSetDefaultValueParams) => { @@ -94,7 +95,7 @@ const ToOneNester = (props) => { return ( - + {props.children} @@ -200,7 +201,7 @@ const ToManyNester = observer( )}
- + diff --git a/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx b/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx index 7648c6da63c..a700727e710 100644 --- a/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx @@ -32,6 +32,7 @@ import { markRecordAsNew } from '../../../data-source/collection-record/isNewRec import { FlagProvider } from '../../../flag-provider'; import { useCompile } from '../../hooks'; import { ActionContextProvider } from '../action'; +import { useSubTableSpecialCase } from '../form-item/hooks/useSpecialCase'; import { Table } from '../table-v2/Table'; import { SubFormProvider, useAssociationFieldContext, useFieldNames } from './hooks'; import { useTableSelectorProps } from './InternalPicker'; @@ -44,9 +45,6 @@ const subTableContainer = css` .ant-formily-item-error-help { display: none; } - .ant-description-textarea { - line-height: 34px; - } .ant-table-cell .ant-formily-item-error-help { display: block; position: absolute; @@ -101,6 +99,9 @@ export const SubTable: any = observer( const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label'); const recordV2 = useCollectionRecord(); const collection = useCollection(); + + useSubTableSpecialCase({ field }); + const move = (fromIndex: number, toIndex: number) => { if (toIndex === undefined) return; if (!isArr(field.value)) return; @@ -172,7 +173,7 @@ export const SubTable: any = observer( {/* 在这里加,是为了让 “当前对象” 的配置显示正确 */} - +
({ + AssociationSelect: () =>
Association Select
, +})); + +vi.mock('../InternalPicker', () => ({ + InternalPicker: () =>
Internal Picker
, +})); + +describe('AssociationFieldModeProvider', () => { + it('should correctly provide the default modeToComponent mapping', () => { + const TestComponent = () => { + const { modeToComponent } = useAssociationFieldModeContext(); + return
{Object.keys(modeToComponent).join(',')}
; + }; + + render( + + + , + ); + + expect(screen.getByText('Picker,Nester,PopoverNester,Select,SubTable,FileManager,CascadeSelect')).toBeTruthy(); + }); + + it('should allow overriding the default modeToComponent mapping', () => { + const CustomComponent = () =>
Custom Component
; + const TestComponent = () => { + const { getComponent } = useAssociationFieldModeContext(); + const Component = getComponent(AssociationFieldMode.Picker); + return ; + }; + + render( + + + , + ); + + expect(screen.getByText('Custom Component')).toBeTruthy(); + }); + + it('getComponent should return the default component if no custom component is found', () => { + const TestComponent = () => { + const { getComponent } = useAssociationFieldModeContext(); + const Component = getComponent(AssociationFieldMode.Select); + return ; + }; + + render( + + + , + ); + + expect(screen.getByText('Association Select')).toBeTruthy(); + }); + + it('getDefaultComponent should always return the default component', () => { + const CustomComponent = () =>
Custom Component
; + const TestComponent = () => { + const { getDefaultComponent } = useAssociationFieldModeContext(); + const Component = getDefaultComponent(AssociationFieldMode.Picker); + return ; + }; + + render( + + + , + ); + + expect(screen.getByText('Internal Picker')).toBeTruthy(); + }); +}); diff --git a/packages/core/client/src/schema-component/antd/association-field/hooks.ts b/packages/core/client/src/schema-component/antd/association-field/hooks.tsx similarity index 65% rename from packages/core/client/src/schema-component/antd/association-field/hooks.ts rename to packages/core/client/src/schema-component/antd/association-field/hooks.tsx index 8e4e812be30..eb7b109ac1b 100644 --- a/packages/core/client/src/schema-component/antd/association-field/hooks.ts +++ b/packages/core/client/src/schema-component/antd/association-field/hooks.tsx @@ -8,22 +8,16 @@ */ import { GeneralField } from '@formily/core'; -import { useField, useFieldSchema } from '@formily/react'; -import { reaction } from '@formily/reactive'; -import { flatten, getValuesByPath } from '@nocobase/utils/client'; -import _, { isString } from 'lodash'; +import { Schema, useField, useFieldSchema } from '@formily/react'; +import { isString } from 'lodash'; import cloneDeep from 'lodash/cloneDeep'; -import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager'; +import React, { createContext, FC, useCallback, useContext, useMemo } from 'react'; +import { useParsedFilter } from '../../../block-provider/hooks/useParsedFilter'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; import { Collection } from '../../../data-source'; import { isInFilterFormBlock } from '../../../filter-provider'; import { mergeFilter } from '../../../filter-provider/utils'; import { useRecord } from '../../../record-provider'; -import { useParseDataScopeFilter } from '../../../schema-settings'; -import { DEBOUNCE_WAIT } from '../../../variables'; -import { getPath } from '../../../variables/utils/getPath'; -import { getVariableName } from '../../../variables/utils/getVariableName'; -import { isVariable } from '../../../variables/utils/isVariable'; import { useDesignable } from '../../hooks'; import { AssociationFieldMode } from './AssociationFieldModeProvider'; import { AssociationFieldContext } from './context'; @@ -65,66 +59,12 @@ export default function useServiceOptions(props) { const { getField } = useCollection_deprecated(); const { getCollectionJoinField } = useCollectionManager_deprecated(); const record = useRecord(); - const { parseFilter, findVariable } = useParseDataScopeFilter(); - const [fieldServiceFilter, setFieldServiceFilter] = useState(null); - - useEffect(() => { - const filterFromSchema = isString(fieldSchema?.['x-component-props']?.service?.params?.filter) + const filterParams = + (isString(fieldSchema?.['x-component-props']?.service?.params?.filter) ? field.componentProps?.service?.params?.filter - : fieldSchema?.['x-component-props']?.service?.params?.filter; + : fieldSchema?.['x-component-props']?.service?.params?.filter) || service?.params?.filter; - const _run = async () => { - const result = await parseFilter(mergeFilter([filterFromSchema || service?.params?.filter])); - setFieldServiceFilter(result); - }; - const run = _.debounce(_run, DEBOUNCE_WAIT); - - _run(); - - const dispose = reaction( - () => { - // 这一步主要是为了使 reaction 能够收集到依赖 - const flat = flatten(filterFromSchema, { - breakOn({ key }) { - return key.startsWith('$') && key !== '$and' && key !== '$or'; - }, - transformValue(value) { - if (!isVariable(value)) { - return value; - } - const variableName = getVariableName(value); - const variable = findVariable(variableName); - - if (process.env.NODE_ENV !== 'production' && !variable) { - throw new Error(`useServiceOptions: can not find variable ${variableName}`); - } - - const result = getValuesByPath( - { - [variableName]: variable?.ctx || {}, - }, - getPath(value), - ); - return result; - }, - }); - return flat; - }, - run, - { - equals: _.isEqual, - }, - ); - - return dispose; - }, [ - field.componentProps?.service?.params?.filter, - fieldSchema, - findVariable, - parseFilter, - record, - service?.params?.filter, - ]); + const { filter: parsedFilterParams } = useParsedFilter({ filterOption: filterParams }); const collectionField = useMemo(() => { return getField(fieldSchema.name) || getCollectionJoinField(fieldSchema?.['x-collection-field']); @@ -143,7 +83,7 @@ export default function useServiceOptions(props) { }, } : null, - fieldServiceFilter, + parsedFilterParams, ]), isOToAny && sourceValue !== undefined && @@ -171,7 +111,7 @@ export default function useServiceOptions(props) { collectionField?.interface, collectionField?.foreignKey, fieldSchema, - fieldServiceFilter, + parsedFilterParams, sourceValue, useOriginalFilter, ]); @@ -202,12 +142,23 @@ export const useFieldNames = ( return { label: 'label', value: 'value', ...fieldNames }; }; -const SubFormContext = createContext<{ +interface SubFormProviderProps { value: any; collection: Collection; -}>(null); + /** + * the schema of the current sub-table or sub-form + */ + fieldSchema?: Schema; +} + +const SubFormContext = createContext(null); SubFormContext.displayName = 'SubFormContext'; -export const SubFormProvider = SubFormContext.Provider; + +export const SubFormProvider: FC<{ value: SubFormProviderProps }> = (props) => { + const { value, collection, fieldSchema } = props.value; + const memoValue = useMemo(() => ({ value, collection, fieldSchema }), [value, collection, fieldSchema]); + return {props.children}; +}; /** * 用于获取子表单所对应的 form 对象,其应该保持响应性,即一个 Proxy 对象; @@ -219,9 +170,10 @@ export const SubFormProvider = SubFormContext.Provider; * @returns */ export const useSubFormValue = () => { - const { value, collection } = useContext(SubFormContext) || {}; + const { value, collection, fieldSchema } = useContext(SubFormContext) || {}; return { formValue: value, collection, + fieldSchema, }; }; diff --git a/packages/core/client/src/schema-component/antd/association-field/index.ts b/packages/core/client/src/schema-component/antd/association-field/index.ts index 427391c679f..d930a102a78 100644 --- a/packages/core/client/src/schema-component/antd/association-field/index.ts +++ b/packages/core/client/src/schema-component/antd/association-field/index.ts @@ -14,8 +14,12 @@ import { InternalPicker } from './InternalPicker'; import { Nester } from './Nester'; import { ReadPretty } from './ReadPretty'; import { SubTable } from './SubTable'; - -export { AssociationFieldModeProvider } from './AssociationFieldModeProvider'; +import { FileSelector } from './FileManager'; +export { + AssociationFieldMode, + AssociationFieldModeProvider, + useAssociationFieldModeContext, +} from './AssociationFieldModeProvider'; export const AssociationField: any = connect(Editable, mapReadPretty(ReadPretty)); AssociationField.SubTable = SubTable; @@ -25,3 +29,4 @@ AssociationField.Selector = Action.Container; AssociationField.Viewer = Action.Container; AssociationField.InternalSelect = InternalPicker; AssociationField.ReadPretty = ReadPretty; +AssociationField.FileSelector = FileSelector; diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.tsx index 097c858fc8a..2714432cce5 100644 --- a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.tsx +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.tsx @@ -11,7 +11,7 @@ import { CloseOutlined, SearchOutlined } from '@ant-design/icons'; import { useFieldSchema } from '@formily/react'; import { Col, Collapse, Input, Row, Tree } from 'antd'; import cls from 'classnames'; -import React, { ChangeEvent, MouseEvent, useMemo, useState } from 'react'; +import React, { ChangeEvent, MouseEvent, useEffect, useMemo, useState } from 'react'; import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; import { SortableItem } from '../../common'; import { useCompile, useDesigner, useProps } from '../../hooks'; @@ -36,6 +36,8 @@ export const AssociationFilterItem = withDynamicSchemaProps( const { list, onSelected, + // Used when setting default values + onChange, handleSearchInput: _handleSearchInput, params, run, @@ -48,7 +50,7 @@ export const AssociationFilterItem = withDynamicSchemaProps( const defaultActiveKeyCollapse = useMemo( () => (defaultCollapse && collectionField?.name ? [collectionField.name] : []), - [collectionField.name, defaultCollapse], + [collectionField?.name, defaultCollapse], ); const valueKey = _valueKey || collectionField?.targetKey || 'id'; const labelKey = _labelKey || fieldSchema['x-component-props']?.fieldNames?.label || valueKey; @@ -59,11 +61,19 @@ export const AssociationFilterItem = withDynamicSchemaProps( }; const [expandedKeys, setExpandedKeys] = useState([]); - const [selectedKeys, setSelectedKeys] = useState([]); + const [selectedKeys, setSelectedKeys] = useState(fieldSchema.default || []); const [autoExpandParent, setAutoExpandParent] = useState(true); const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.title || 'label'); + useEffect(() => { + // by default, if the default is not empty, we will auto run the filter one time + if (fieldSchema.default) { + onSelected(fieldSchema.default); + setSelectedKeys(fieldSchema.default); + } + }, [fieldSchema.default, onSelected]); + if (!collectionField) { return null; } @@ -76,6 +86,7 @@ export const AssociationFilterItem = withDynamicSchemaProps( const onSelect = (selectedKeysValue: React.Key[]) => { setSelectedKeys(selectedKeysValue); onSelected(selectedKeysValue); + onChange?.(selectedKeysValue); }; const handleSearchToggle = (e: MouseEvent) => { diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.tsx index a00595260b0..97d07c76c54 100644 --- a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.tsx +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.tsx @@ -11,22 +11,22 @@ import { css } from '@emotion/css'; import { useFieldSchema } from '@formily/react'; import cls from 'classnames'; import React from 'react'; -import { useCollection_deprecated } from '../../../collection-manager'; -import { DndContext, SortableItem } from '../../common'; -import { useDesigner } from '../../hooks'; -import { useToken } from '../__builtins__'; -import { AssociationFilterBlockDesigner } from './AssociationFilter.BlockDesigner'; -import { AssociationFilterItem } from './AssociationFilter.Item'; -import { AssociationFilterItemDesigner } from './AssociationFilter.Item.Designer'; -import { AssociationFilterProvider } from './AssociationFilterProvider'; import { useSchemaInitializerRender } from '../../../application'; import { Plugin } from '../../../application/Plugin'; +import { useCollection } from '../../../data-source/collection/CollectionProvider'; import { associationFilterFilterBlockInitializer, filterCollapseItemInitializer, filterCollapseItemInitializer_deprecated, } from '../../../modules/blocks/filter-blocks/collapse/filterCollapseItemInitializer'; +import { DndContext, SortableItem } from '../../common'; +import { useDesigner } from '../../hooks'; +import { useToken } from '../__builtins__'; +import { AssociationFilterBlockDesigner } from './AssociationFilter.BlockDesigner'; import { associationFilterInitializer } from './AssociationFilter.Initializer'; +import { AssociationFilterItem } from './AssociationFilter.Item'; +import { AssociationFilterItemDesigner } from './AssociationFilter.Item.Designer'; +import { AssociationFilterProvider } from './AssociationFilterProvider'; import { useAssociationFilterHeight } from './hook'; export const AssociationFilter = (props) => { const { token } = useToken(); @@ -106,8 +106,8 @@ AssociationFilter.BlockDesigner = AssociationFilterBlockDesigner; AssociationFilter.useAssociationField = () => { const fieldSchema = useFieldSchema(); - const { getField } = useCollection_deprecated(); - return React.useMemo(() => getField(fieldSchema.name as any), [fieldSchema.name]); + const collection = useCollection(); + return React.useMemo(() => collection.getField(fieldSchema.name as any), [fieldSchema.name]); }; export class AssociationFilterPlugin extends Plugin { diff --git a/packages/core/client/src/schema-component/antd/block-item/index.tsx b/packages/core/client/src/schema-component/antd/block-item/index.tsx index de84813348a..e3d0ccf25f9 100644 --- a/packages/core/client/src/schema-component/antd/block-item/index.tsx +++ b/packages/core/client/src/schema-component/antd/block-item/index.tsx @@ -9,3 +9,4 @@ export * from './BlockItem'; export * from './TestDesigner'; +export * from './BlockItemCard'; diff --git a/packages/core/client/src/schema-component/antd/collection-select/CollectionSelect.tsx b/packages/core/client/src/schema-component/antd/collection-select/CollectionSelect.tsx index e252773b056..79d40c226d9 100644 --- a/packages/core/client/src/schema-component/antd/collection-select/CollectionSelect.tsx +++ b/packages/core/client/src/schema-component/antd/collection-select/CollectionSelect.tsx @@ -11,6 +11,7 @@ import { connect, mapReadPretty, observer, useField } from '@formily/react'; import { Cascader, Select, SelectProps, Tag } from 'antd'; import React, { useCallback, useContext, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { css } from '@emotion/css'; import { useSelfAndChildrenCollections } from '../../../collection-manager/action-hooks'; import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager/hooks'; import { useCompile } from '../../hooks'; @@ -148,6 +149,57 @@ export const DataSourceSelect = connect((props: DataSourceSelectProps) => { ); }); +const DataSourceCollectionCascaderReadPretty = observer((props: any) => { + const dataSourceManager = useDataSourceManager(); + const compile = useCompile(); + const { value, onChange, dataSourceFilter, collectionFilter, ...others } = props; + const [dataSourceName, collectionName] = parseCollectionName(value); + const path = [dataSourceName, collectionName].filter(Boolean); + const dataSources = dataSourceManager.getDataSources(); + + const options = useMemo(() => { + return (dataSourceFilter ? dataSources.filter(dataSourceFilter) : dataSources).map((dataSource) => { + return { + label: compile(dataSource.displayName), + value: dataSource.key, + children: dataSource.collectionManager.collectionInstancesArr + .filter(collectionFilter ?? ((collection) => !collection.hidden)) + .map((collection) => { + return { + label: compile(collection.title), + value: collection.name, + }; + }), + }; + }); + }, [dataSources, dataSourceFilter, collectionFilter]); + const getDisplayValue = (value: string[], options: any[]): string[] => { + const displayValues: string[] = []; + + let currentOptions = options; + + value.forEach((val) => { + const option = currentOptions.find((item) => item.value === val); + if (option) { + displayValues.push(option.label as string); // 假设label为string类型 + if (option.children) { + currentOptions = option.children; + } else { + currentOptions = []; + } + } + }); + + return displayValues; + }; + const displayValues = getDisplayValue(path, options); + return ( +
+ {displayValues.join(' / ')} +
+ ); +}); + export const DataSourceCollectionCascader = connect((props) => { const dataSourceManager = useDataSourceManager(); const compile = useCompile(); @@ -183,4 +235,4 @@ export const DataSourceCollectionCascader = connect((props) => { [onChange], ); return ; -}); +}, mapReadPretty(DataSourceCollectionCascaderReadPretty)); diff --git a/packages/core/client/src/schema-component/antd/date-picker/DatePicker.tsx b/packages/core/client/src/schema-component/antd/date-picker/DatePicker.tsx index ff6d1a58b07..e78ab96885a 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/DatePicker.tsx +++ b/packages/core/client/src/schema-component/antd/date-picker/DatePicker.tsx @@ -7,13 +7,16 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { connect, mapProps, mapReadPretty } from '@formily/react'; -import { DatePicker as AntdDatePicker, DatePickerProps as AntdDatePickerProps } from 'antd'; -import React, { FC } from 'react'; +import { connect, mapProps, mapReadPretty, useField, useFieldSchema } from '@formily/react'; +import { DatePicker as AntdDatePicker, DatePickerProps as AntdDatePickerProps, Space, Select } from 'antd'; +import { RangePickerProps } from 'antd/es/date-picker'; +import dayjs from 'dayjs'; +import React, { useState } from 'react'; +import { getPickerFormat } from '@nocobase/utils/client'; import { useTranslation } from 'react-i18next'; import { ReadPretty, ReadPrettyComposed } from './ReadPretty'; -import { getDateRanges, mapDatePicker, mapRangePicker } from './util'; -import { RangePickerProps } from 'antd/es/date-picker'; +import { getDateRanges, mapDatePicker, mapRangePicker, inferPickerType } from './util'; +import { useCompile } from '../../'; interface IDatePickerProps { utc?: boolean; @@ -22,6 +25,7 @@ interface IDatePickerProps { type ComposedDatePicker = React.FC & { ReadPretty?: ReadPrettyComposed['DatePicker']; RangePicker?: ComposedRangePicker; + FilterWithPicker?: any; }; type ComposedRangePicker = React.FC & { @@ -45,19 +49,28 @@ const InternalRangePicker = connect( mapReadPretty(ReadPretty.DateRangePicker), ); -export const DatePicker: ComposedDatePicker = (props) => { +export const DatePicker: ComposedDatePicker = (props: any) => { const { utc = true } = useDatePickerContext(); const value = Array.isArray(props.value) ? props.value[0] : props.value; - const newProps = { utc, ...props }; + const newProps = { + utc, + ...props, + showTime: props.showTime ? { defaultValue: dayjs('00:00:00', 'HH:mm:ss') } : false, + }; return ; }; DatePicker.ReadPretty = ReadPretty.DatePicker; -DatePicker.RangePicker = function RangePicker(props) { +DatePicker.RangePicker = function RangePicker(props: any) { + const { value, picker = 'date', format } = props; const { t } = useTranslation(); + const fieldSchema = useFieldSchema(); + const field: any = useField(); const { utc = true } = useDatePickerContext(); const rangesValue = getDateRanges(); + const compile = useCompile(); + const isFilterAction: any = !fieldSchema['x-filter-operator']; // 在筛选按钮中使用 const presets = [ { label: t('Today'), value: rangesValue.today }, { label: t('Last week'), value: rangesValue.lastWeek }, @@ -79,8 +92,138 @@ DatePicker.RangePicker = function RangePicker(props) { { label: t('Last 90 days'), value: rangesValue.last90Days }, { label: t('Next 90 days'), value: rangesValue.next90Days }, ]; - const newProps: any = { utc, presets, ...props }; + + const targetPicker = value ? inferPickerType(value?.[0]) : picker; + const targetFormat = getPickerFormat(targetPicker) || format; + const newProps: any = { + utc, + presets, + ...props, + format: targetFormat, + picker: targetPicker, + showTime: props.showTime ? { defaultValue: [dayjs('00:00:00', 'HH:mm:ss'), dayjs('00:00:00', 'HH:mm:ss')] } : false, + }; + const [stateProps, setStateProps] = useState(newProps); + + if (isFilterAction) { + return ( + + { + const format = getPickerFormat(value); + field.setComponentProps({ + picker: value, + format, + }); + newProps.picker = value; + newProps.format = format; + setStateProps(newProps); + fieldSchema['x-component-props'] = { + ...props, + picker: value, + format, + }; + field.value = null; + }} + /> + + + ); +}; + export default DatePicker; diff --git a/packages/core/client/src/schema-component/antd/date-picker/ReadPretty.tsx b/packages/core/client/src/schema-component/antd/date-picker/ReadPretty.tsx index f05fc9c312e..58e1e60e220 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/ReadPretty.tsx +++ b/packages/core/client/src/schema-component/antd/date-picker/ReadPretty.tsx @@ -34,16 +34,15 @@ export interface ReadPrettyDatePickerProps extends Str2momentOptions, GetDefault showTime?: boolean; } -ReadPretty.DatePicker = function DatePicker(props) { +ReadPretty.DatePicker = function DatePicker(props: any) { + const { value, picker = 'date' } = props; const prefixCls = usePrefixCls('description-date-picker', props); - - if (!props.value) { + if (!value) { return
; } - const getLabels = () => { const format = getDefaultFormat(props) as string; - const m = str2moment(props.value, props); + const m = str2moment(value, props); const labels = dayjs.isDayjs(m) ? m.format(format) : ''; return isArr(labels) ? labels.join('~') : labels; }; diff --git a/packages/core/client/src/schema-component/antd/date-picker/__tests__/date-picker.test.tsx b/packages/core/client/src/schema-component/antd/date-picker/__tests__/date-picker.test.tsx index e89323d675f..6405a5ec1b6 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/__tests__/date-picker.test.tsx +++ b/packages/core/client/src/schema-component/antd/date-picker/__tests__/date-picker.test.tsx @@ -232,9 +232,6 @@ describe('RangePicker', () => { expect( screen.getByText(currentDateString.replace(/-/g, '/'), { selector: '.ant-description-date-picker' }), ).toBeInTheDocument(); - - // Value - expect(screen.getByText(`${currentDateString}T00:00:00.000Z`)).toBeInTheDocument(); }); }); diff --git a/packages/core/client/src/schema-component/antd/date-picker/__tests__/getDateRanges.test.ts b/packages/core/client/src/schema-component/antd/date-picker/__tests__/getDateRanges.test.ts index 69862c4faf3..a18c6502ce4 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/__tests__/getDateRanges.test.ts +++ b/packages/core/client/src/schema-component/antd/date-picker/__tests__/getDateRanges.test.ts @@ -140,6 +140,245 @@ describe('getDateRanges', () => { }); }); +describe('getDateRanges: fieldOperator is $dateBetween', () => { + const dateRanges = getDateRanges(); + + it('today', () => { + const [start, end] = dateRanges.today({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().startOf('day').toISOString()); + expect(end.toISOString()).toBe(dayjs().endOf('day').toISOString()); + }); + + test('yesterday', () => { + const [start, end] = dateRanges.yesterday({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(dayjs(start).isSame(dayjs().subtract(1, 'day'), 'day')).toBe(true); + expect(dayjs(end).isSame(dayjs().subtract(1, 'day'), 'day')).toBe(true); + }); + + test('tomorrow', () => { + const [start, end] = dateRanges.tomorrow({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(dayjs(start).isSame(dayjs().add(1, 'day'), 'day')).toBe(true); + expect(dayjs(end).isSame(dayjs().add(1, 'day'), 'day')).toBe(true); + }); + + it('lastWeek', () => { + const [start, end] = dateRanges.lastWeek({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(-1, 'week').startOf('isoWeek').toISOString()); + expect(end.toISOString()).toBe(dayjs().add(-1, 'week').endOf('isoWeek').toISOString()); + }); + + it('thisWeek', () => { + const [start, end] = dateRanges.thisWeek({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().startOf('isoWeek').toISOString()); + expect(end.toISOString()).toBe(dayjs().endOf('isoWeek').toISOString()); + }); + + it('nextWeek', () => { + const [start, end] = dateRanges.nextWeek({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(1, 'week').startOf('isoWeek').toISOString()); + expect(end.toISOString()).toBe(dayjs().add(1, 'week').endOf('isoWeek').toISOString()); + }); + + it('lastMonth', () => { + const [start, end] = dateRanges.lastMonth({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(-1, 'month').startOf('month').toISOString()); + expect(end.toISOString()).toBe(dayjs().add(-1, 'month').endOf('month').toISOString()); + }); + + it('thisMonth', () => { + const [start, end] = dateRanges.thisMonth({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().startOf('month').toISOString()); + expect(end.toISOString()).toBe(dayjs().endOf('month').toISOString()); + }); + + it('nextMonth', () => { + const [start, end] = dateRanges.nextMonth({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(1, 'month').startOf('month').toISOString()); + expect(end.toISOString()).toBe(dayjs().add(1, 'month').endOf('month').toISOString()); + }); + + it('lastQuarter', () => { + const [start, end] = dateRanges.lastQuarter({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(-1, 'quarter').startOf('quarter').toISOString()); + expect(end.toISOString()).toBe(dayjs().add(-1, 'quarter').endOf('quarter').toISOString()); + }); + + it('thisQuarter', () => { + const [start, end] = dateRanges.thisQuarter({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().startOf('quarter').toISOString()); + expect(end.toISOString()).toBe(dayjs().endOf('quarter').toISOString()); + }); + + it('nextQuarter', () => { + const [start, end] = dateRanges.nextQuarter({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(1, 'quarter').startOf('quarter').toISOString()); + expect(end.toISOString()).toBe(dayjs().add(1, 'quarter').endOf('quarter').toISOString()); + }); + + it('lastYear', () => { + const [start, end] = dateRanges.lastYear({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(-1, 'year').startOf('year').toISOString()); + expect(end.toISOString()).toBe(dayjs().add(-1, 'year').endOf('year').toISOString()); + }); + + it('thisYear', () => { + const [start, end] = dateRanges.thisYear({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().startOf('year').toISOString()); + expect(end.toISOString()).toBe(dayjs().endOf('year').toISOString()); + }); + + it('nextYear', () => { + const [start, end] = dateRanges.nextYear({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(1, 'year').startOf('year').toISOString()); + expect(end.toISOString()).toBe(dayjs().add(1, 'year').endOf('year').toISOString()); + }); + + it('last7Days', () => { + const [start, end] = dateRanges.last7Days({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(-6, 'days').startOf('days').toISOString()); + expect(end.toISOString()).toBe(dayjs().endOf('days').toISOString()); + }); + + it('next7Days', () => { + const [start, end] = dateRanges.next7Days({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(1, 'day').startOf('day').toISOString()); + expect(end.toISOString()).toBe(dayjs().add(7, 'days').endOf('days').toISOString()); + }); + + it('last30Days', () => { + const [start, end] = dateRanges.last30Days({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(-29, 'days').startOf('days').toISOString()); + expect(end.toISOString()).toBe(dayjs().endOf('days').toISOString()); + }); + + it('next30Days', () => { + const [start, end] = dateRanges.next30Days({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(1, 'day').startOf('day').toISOString()); + expect(end.toISOString()).toBe(dayjs().add(30, 'days').endOf('days').toISOString()); + }); + + it('last90Days', () => { + const [start, end] = dateRanges.last90Days({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(-89, 'days').startOf('days').toISOString()); + expect(end.toISOString()).toBe(dayjs().endOf('days').toISOString()); + }); + + it('next90Days', () => { + const [start, end] = dateRanges.next90Days({ fieldOperator: '$dateBetween', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(1, 'day').startOf('day').toISOString()); + expect(end.toISOString()).toBe(dayjs().add(90, 'days').endOf('days').toISOString()); + }); +}); + +describe('getDateRanges: fieldOperator is not $dateBetween', () => { + const dateRanges = getDateRanges(); + + it('today', () => { + const start = dateRanges.today({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().startOf('day').toISOString()); + }); + + test('yesterday', () => { + const start = dateRanges.yesterday({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(dayjs(start).isSame(dayjs().subtract(1, 'day'), 'day')).toBe(true); + }); + + test('tomorrow', () => { + const start = dateRanges.tomorrow({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(dayjs(start).isSame(dayjs().add(1, 'day'), 'day')).toBe(true); + }); + + it('lastWeek', () => { + const start = dateRanges.lastWeek({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(-1, 'week').startOf('isoWeek').toISOString()); + }); + + it('thisWeek', () => { + const start = dateRanges.thisWeek({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().startOf('isoWeek').toISOString()); + }); + + it('nextWeek', () => { + const start = dateRanges.nextWeek({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(1, 'week').startOf('isoWeek').toISOString()); + }); + + it('lastMonth', () => { + const start = dateRanges.lastMonth({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(-1, 'month').startOf('month').toISOString()); + }); + + it('thisMonth', () => { + const start = dateRanges.thisMonth({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().startOf('month').toISOString()); + }); + + it('nextMonth', () => { + const start = dateRanges.nextMonth({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(1, 'month').startOf('month').toISOString()); + }); + + it('lastQuarter', () => { + const start = dateRanges.lastQuarter({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(-1, 'quarter').startOf('quarter').toISOString()); + }); + + it('thisQuarter', () => { + const start = dateRanges.thisQuarter({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().startOf('quarter').toISOString()); + }); + + it('nextQuarter', () => { + const start = dateRanges.nextQuarter({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(1, 'quarter').startOf('quarter').toISOString()); + }); + + it('lastYear', () => { + const start = dateRanges.lastYear({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(-1, 'year').startOf('year').toISOString()); + }); + + it('thisYear', () => { + const start = dateRanges.thisYear({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().startOf('year').toISOString()); + }); + + it('nextYear', () => { + const start = dateRanges.nextYear({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(1, 'year').startOf('year').toISOString()); + }); + + it('last7Days', () => { + const start = dateRanges.last7Days({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(-6, 'days').startOf('days').toISOString()); + }); + + it('next7Days', () => { + const start = dateRanges.next7Days({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(1, 'day').startOf('day').toISOString()); + }); + + it('last30Days', () => { + const start = dateRanges.last30Days({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(-29, 'days').startOf('days').toISOString()); + }); + + it('next30Days', () => { + const start = dateRanges.next30Days({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(1, 'day').startOf('day').toISOString()); + }); + + it('last90Days', () => { + const start = dateRanges.last90Days({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(-89, 'days').startOf('days').toISOString()); + }); + + it('next90Days', () => { + const start = dateRanges.next90Days({ fieldOperator: '$dateOn', isParsingVariable: true }); + expect(start.toISOString()).toBe(dayjs().add(1, 'day').startOf('day').toISOString()); + }); +}); + describe('getDateRanges: shouldBeString is true and utc is false', () => { const dateRanges = getDateRanges({ shouldBeString: true, utc: false }); diff --git a/packages/core/client/src/schema-component/antd/date-picker/__tests__/mapDatePicker.test.ts b/packages/core/client/src/schema-component/antd/date-picker/__tests__/mapDatePicker.test.ts index f966ee3a3e2..41a8feef506 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/__tests__/mapDatePicker.test.ts +++ b/packages/core/client/src/schema-component/antd/date-picker/__tests__/mapDatePicker.test.ts @@ -62,7 +62,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs.utc('2022-02-22 22:22:22')); - expect(props.onChange).toHaveBeenCalledWith('2022-02-22T22:22:22.000Z'); + expect(props.onChange).toHaveBeenCalledWith('2022-02-22 00:00:00'); }); it('should call onChange with correct value when showTime is true and gmt is false', () => { @@ -74,7 +74,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-22 22:22:22'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith(m.toISOString()); + expect(props.onChange).toHaveBeenCalledWith('2022-02-22 00:00:00'); }); it('should call onChange with correct value when showTime is false and gmt is true', () => { @@ -85,7 +85,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs.utc('2022-02-22')); - expect(props.onChange).toHaveBeenCalledWith('2022-02-22T00:00:00.000Z'); + expect(props.onChange).toHaveBeenCalledWith('2022-02-22'); }); it('should call onChange with correct value when showTime is false and gmt is false', () => { @@ -97,7 +97,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-22'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith(m.toISOString()); + expect(props.onChange).toHaveBeenCalledWith('2022-02-22'); }); it('should call onChange with correct value when picker is year and gmt is true', () => { @@ -108,7 +108,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs.utc('2022-01-01T00:00:00.000Z')); - expect(props.onChange).toHaveBeenCalledWith('2022-01-01T00:00:00.000Z'); + expect(props.onChange).toHaveBeenCalledWith('2022-01-01'); }); it('should call onChange with correct value when picker is year and gmt is false', () => { @@ -120,7 +120,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-01 00:00:00'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith(m.startOf('year').toISOString()); + expect(props.onChange).toHaveBeenCalledWith('2022-01-01'); }); it('should call onChange with correct value when picker is month and gmt is true', () => { @@ -131,7 +131,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs.utc('2022-02-22T00:00:00.000Z')); - expect(props.onChange).toHaveBeenCalledWith('2022-02-01T00:00:00.000Z'); + expect(props.onChange).toHaveBeenCalledWith('2022-02-01'); }); it('should call onChange with correct value when picker is month and gmt is false', () => { @@ -143,7 +143,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-01 00:00:00'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith(m.startOf('month').toISOString()); + expect(props.onChange).toHaveBeenCalledWith('2022-02-01'); }); it('should call onChange with correct value when picker is quarter and gmt is true', () => { @@ -154,7 +154,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs.utc('2022-02-22T00:00:00.000Z')); - expect(props.onChange).toHaveBeenCalledWith('2022-01-01T00:00:00.000Z'); + expect(props.onChange).toHaveBeenCalledWith('2022-01-01'); }); it('should call onChange with correct value when picker is quarter and gmt is false', () => { @@ -166,7 +166,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-01 00:00:00'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith(m.startOf('quarter').toISOString()); + expect(props.onChange).toHaveBeenCalledWith('2022-01-01'); }); it('should call onChange with correct value when picker is week and gmt is true', () => { @@ -178,7 +178,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs.utc('2022-02-21T00:00:00.000Z'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith(m.startOf('week').add(1, 'day').toISOString()); + expect(props.onChange).toHaveBeenCalledWith('2022-02-20'); }); it('should call onChange with correct value when picker is week and gmt is false', () => { @@ -190,7 +190,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-21 00:00:00'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith(m.startOf('week').add(1, 'day').toISOString()); + expect(props.onChange).toHaveBeenCalledWith('2022-02-20'); }); it('should call onChange with correct value when utc is false', () => { @@ -202,7 +202,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs('2022-02-22 22:22:22')); - expect(props.onChange).toHaveBeenCalledWith('2022-02-22 22:22:22'); + expect(props.onChange).toHaveBeenCalledWith('2022-02-22 00:00:00'); }); it('should call onChange with correct value when picker is year and utc is false', () => { diff --git a/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts b/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts index a77fbe81831..93387abd605 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts +++ b/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts @@ -29,7 +29,7 @@ describe('str2moment', () => { }); test('picker is month', async () => { - const m = str2moment('2022-06-01T00:00:00.000Z', { picker: 'month' }); + const m = str2moment('2022-06-01T00:00:00.000Z', { picker: 'month', gmt: true }); expect(m.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-06-01 00:00:00'); }); }); @@ -102,7 +102,7 @@ describe('moment2str', () => { test('picker is year', () => { const m = dayjs('2023-06-21 10:10:00'); - const str = moment2str(m, { picker: 'year' }); + const str = moment2str(m, { picker: 'year', gmt: true }); expect(str).toBe('2023-01-01T00:00:00.000Z'); }); @@ -130,10 +130,10 @@ describe('moment2str', () => { expect(str).toBe('2023-06-01T00:00:00.000Z'); }); - test('picker is month', () => { + test('picker is month gmt is false', () => { const m = dayjs('2023-06-21 10:10:00'); - const str = moment2str(m, { picker: 'month' }); - expect(str).toBe('2023-06-01T00:00:00.000Z'); + const str = moment2str(m, { picker: 'month', gmt: false }); + expect(str).toBe(dayjs('2023-06-01 00:00:00').toISOString()); }); test('picker is week, gmt is false', () => { diff --git a/packages/core/client/src/schema-component/antd/date-picker/util.ts b/packages/core/client/src/schema-component/antd/date-picker/util.ts index 59026b3efba..4ca70f222a5 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/util.ts +++ b/packages/core/client/src/schema-component/antd/date-picker/util.ts @@ -7,11 +7,11 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { getDefaultFormat, str2moment, toGmt, toLocal } from '@nocobase/utils/client'; +import { getDefaultFormat, str2moment, toGmt, toLocal, getPickerFormat } from '@nocobase/utils/client'; import type { Dayjs } from 'dayjs'; import dayjs from 'dayjs'; -const toStringByPicker = (value, picker, timezone: 'gmt' | 'local') => { +const toStringByPicker = (value, picker = 'date', timezone: 'gmt' | 'local') => { if (!dayjs.isDayjs(value)) return value; if (timezone === 'local') { const offset = new Date().getTimezoneOffset(); @@ -57,7 +57,7 @@ export interface Moment2strOptions { } export const moment2str = (value?: Dayjs | null, options: Moment2strOptions = {}) => { - const { showTime, gmt, picker, utc = true } = options; + const { showTime, gmt, picker = 'date', utc = true } = options; if (!value) { return value; } @@ -74,21 +74,51 @@ export const moment2str = (value?: Dayjs | null, options: Moment2strOptions = {} return toLocalByPicker(value, picker); }; +const handleChangeOnFilter = (value, picker, showTime) => { + const format = showTime ? 'YYYY-MM-DD HH:mm:ss' : getPickerFormat(picker); + if (value) { + return value.format(format); + } + return value; +}; + +const handleChangeOnForm = (value, dateOnly, utc, picker, showTime, gmt) => { + const format = showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'; + if (!value) { + return value; + } + if (dateOnly) { + return dayjs(value).startOf(picker).format('YYYY-MM-DD'); + } + if (utc) { + if (gmt) { + return toGmt(value); + } + if (picker !== 'date') { + return dayjs(value).startOf(picker).toISOString(); + } + const formattedDate = dayjs(value).format(format); + return dayjs(formattedDate).toISOString(); + } + return dayjs(value).startOf(picker).format(format); +}; + export const mapDatePicker = function () { return (props: any) => { - const format = getDefaultFormat(props) as any; + const { dateOnly, showTime, picker = 'date', utc, gmt, underFilter } = props; + const format = getDefaultFormat(props); const onChange = props.onChange; - return { ...props, format: format, value: str2moment(props.value, props), - onChange: (value: Dayjs | null) => { + onChange: (value: Dayjs | null, dateString) => { if (onChange) { - if (!props.showTime && value) { - value = value.startOf('day'); + if (underFilter) { + onChange(handleChangeOnFilter(value, picker, showTime)); + } else { + onChange(handleChangeOnForm(value, dateOnly, utc, picker, showTime, gmt)); } - onChange(moment2str(value, props)); } }, }; @@ -99,18 +129,26 @@ export const mapRangePicker = function () { return (props: any) => { const format = getDefaultFormat(props) as any; const onChange = props.onChange; - + const { dateOnly, showTime, picker = 'date', utc, gmt, underFilter } = props; return { ...props, format: format, value: str2moment(props.value, props), onChange: (value: Dayjs[]) => { if (onChange) { - onChange( - value - ? [moment2str(getRangeStart(value[0], props), props), moment2str(getRangeEnd(value[1], props), props)] - : [], - ); + if (underFilter) { + onChange( + value + ? [handleChangeOnFilter(value[0], picker, showTime), handleChangeOnFilter(value[1], picker, showTime)] + : [], + ); + } else { + onChange( + value + ? [moment2str(getRangeStart(value[0], props), props), moment2str(getRangeEnd(value[1], props), props)] + : [], + ); + } } }, } as any; @@ -160,58 +198,80 @@ export const getDateRanges = (props?: { }; return { now: () => toString(dayjs()), - today: () => toString([getStart(0, 'day'), getEnd(0, 'day')]), - yesterday: () => toString([getStart(-1, 'day'), getEnd(-1, 'day')]), - tomorrow: () => toString([getStart(1, 'day'), getEnd(1, 'day')]), - thisWeek: () => toString([getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')]), - lastWeek: () => toString([getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')]), - nextWeek: () => toString([getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')]), - thisIsoWeek: () => toString([getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')]), - lastIsoWeek: () => toString([getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')]), - nextIsoWeek: () => toString([getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')]), - thisMonth: () => toString([getStart(0, 'month'), getEnd(0, 'month')]), - lastMonth: () => toString([getStart(-1, 'month'), getEnd(-1, 'month')]), - nextMonth: () => toString([getStart(1, 'month'), getEnd(1, 'month')]), - thisQuarter: () => toString([getStart(0, 'quarter'), getEnd(0, 'quarter')]), - lastQuarter: () => toString([getStart(-1, 'quarter'), getEnd(-1, 'quarter')]), - nextQuarter: () => toString([getStart(1, 'quarter'), getEnd(1, 'quarter')]), - thisYear: () => toString([getStart(0, 'year'), getEnd(0, 'year')]), - lastYear: () => toString([getStart(-1, 'year'), getEnd(-1, 'year')]), - nextYear: () => toString([getStart(1, 'year'), getEnd(1, 'year')]), - last7Days: () => toString([getStart(-6, 'days'), getEnd(0, 'days')]), - next7Days: () => toString([getStart(1, 'day'), getEnd(7, 'days')]), - last30Days: () => toString([getStart(-29, 'days'), getEnd(0, 'days')]), - next30Days: () => toString([getStart(1, 'day'), getEnd(30, 'days')]), - last90Days: () => toString([getStart(-89, 'days'), getEnd(0, 'days')]), - next90Days: () => toString([getStart(1, 'day'), getEnd(90, 'days')]), + today: (params?: any) => withParams(toString([getStart(0, 'day'), getEnd(0, 'day')]), params), + yesterday: (params?: any) => withParams(toString([getStart(-1, 'day'), getEnd(-1, 'day')]), params), + tomorrow: (params?: any) => withParams(toString([getStart(1, 'day'), getEnd(1, 'day')]), params), + thisWeek: (params?: any) => withParams(toString([getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')]), params), + lastWeek: (params?: any) => withParams(toString([getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')]), params), + nextWeek: (params?: any) => withParams(toString([getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')]), params), + thisIsoWeek: (params?: any) => withParams(toString([getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')]), params), + lastIsoWeek: (params?: any) => withParams(toString([getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')]), params), + nextIsoWeek: (params?: any) => withParams(toString([getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')]), params), + thisMonth: (params?: any) => withParams(toString([getStart(0, 'month'), getEnd(0, 'month')]), params), + lastMonth: (params?: any) => withParams(toString([getStart(-1, 'month'), getEnd(-1, 'month')]), params), + nextMonth: (params?: any) => withParams(toString([getStart(1, 'month'), getEnd(1, 'month')]), params), + thisQuarter: (params?: any) => withParams(toString([getStart(0, 'quarter'), getEnd(0, 'quarter')]), params), + lastQuarter: (params?: any) => withParams(toString([getStart(-1, 'quarter'), getEnd(-1, 'quarter')]), params), + nextQuarter: (params?: any) => withParams(toString([getStart(1, 'quarter'), getEnd(1, 'quarter')]), params), + thisYear: (params?: any) => withParams(toString([getStart(0, 'year'), getEnd(0, 'year')]), params), + lastYear: (params?: any) => withParams(toString([getStart(-1, 'year'), getEnd(-1, 'year')]), params), + nextYear: (params?: any) => withParams(toString([getStart(1, 'year'), getEnd(1, 'year')]), params), + last7Days: (params?: any) => withParams(toString([getStart(-6, 'days'), getEnd(0, 'days')]), params), + next7Days: (params?: any) => withParams(toString([getStart(1, 'day'), getEnd(7, 'days')]), params), + last30Days: (params?: any) => withParams(toString([getStart(-29, 'days'), getEnd(0, 'days')]), params), + next30Days: (params?: any) => withParams(toString([getStart(1, 'day'), getEnd(30, 'days')]), params), + last90Days: (params?: any) => withParams(toString([getStart(-89, 'days'), getEnd(0, 'days')]), params), + next90Days: (params?: any) => withParams(toString([getStart(1, 'day'), getEnd(90, 'days')]), params), }; } return { now: () => dayjs().toISOString(), - today: () => [getStart(0, 'day'), getEnd(0, 'day')], - yesterday: () => [getStart(-1, 'day'), getEnd(-1, 'day')], - tomorrow: () => [getStart(1, 'day'), getEnd(1, 'day')], - thisWeek: () => [getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')], - lastWeek: () => [getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')], - nextWeek: () => [getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')], - thisIsoWeek: () => [getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')], - lastIsoWeek: () => [getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')], - nextIsoWeek: () => [getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')], - thisMonth: () => [getStart(0, 'month'), getEnd(0, 'month')], - lastMonth: () => [getStart(-1, 'month'), getEnd(-1, 'month')], - nextMonth: () => [getStart(1, 'month'), getEnd(1, 'month')], - thisQuarter: () => [getStart(0, 'quarter'), getEnd(0, 'quarter')], - lastQuarter: () => [getStart(-1, 'quarter'), getEnd(-1, 'quarter')], - nextQuarter: () => [getStart(1, 'quarter'), getEnd(1, 'quarter')], - thisYear: () => [getStart(0, 'year'), getEnd(0, 'year')], - lastYear: () => [getStart(-1, 'year'), getEnd(-1, 'year')], - nextYear: () => [getStart(1, 'year'), getEnd(1, 'year')], - last7Days: () => [getStart(-6, 'days'), getEnd(0, 'days')], - next7Days: () => [getStart(1, 'day'), getEnd(7, 'days')], - last30Days: () => [getStart(-29, 'days'), getEnd(0, 'days')], - next30Days: () => [getStart(1, 'day'), getEnd(30, 'days')], - last90Days: () => [getStart(-89, 'days'), getEnd(0, 'days')], - next90Days: () => [getStart(1, 'day'), getEnd(90, 'days')], + today: (params?: any) => withParams([getStart(0, 'day'), getEnd(0, 'day')], params), + yesterday: (params?: any) => withParams([getStart(-1, 'day'), getEnd(-1, 'day')], params), + tomorrow: (params?: any) => withParams([getStart(1, 'day'), getEnd(1, 'day')], params), + thisWeek: (params?: any) => withParams([getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')], params), + lastWeek: (params?: any) => withParams([getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')], params), + nextWeek: (params?: any) => withParams([getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')], params), + thisIsoWeek: (params?: any) => withParams([getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')], params), + lastIsoWeek: (params?: any) => withParams([getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')], params), + nextIsoWeek: (params?: any) => withParams([getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')], params), + thisMonth: (params?: any) => withParams([getStart(0, 'month'), getEnd(0, 'month')], params), + lastMonth: (params?: any) => withParams([getStart(-1, 'month'), getEnd(-1, 'month')], params), + nextMonth: (params?: any) => withParams([getStart(1, 'month'), getEnd(1, 'month')], params), + thisQuarter: (params?: any) => withParams([getStart(0, 'quarter'), getEnd(0, 'quarter')], params), + lastQuarter: (params?: any) => withParams([getStart(-1, 'quarter'), getEnd(-1, 'quarter')], params), + nextQuarter: (params?: any) => withParams([getStart(1, 'quarter'), getEnd(1, 'quarter')], params), + thisYear: (params?: any) => withParams([getStart(0, 'year'), getEnd(0, 'year')], params), + lastYear: (params?: any) => withParams([getStart(-1, 'year'), getEnd(-1, 'year')], params), + nextYear: (params?: any) => withParams([getStart(1, 'year'), getEnd(1, 'year')], params), + last7Days: (params?: any) => withParams([getStart(-6, 'days'), getEnd(0, 'days')], params), + next7Days: (params?: any) => withParams([getStart(1, 'day'), getEnd(7, 'days')], params), + last30Days: (params?: any) => withParams([getStart(-29, 'days'), getEnd(0, 'days')], params), + next30Days: (params?: any) => withParams([getStart(1, 'day'), getEnd(30, 'days')], params), + last90Days: (params?: any) => withParams([getStart(-89, 'days'), getEnd(0, 'days')], params), + next90Days: (params?: any) => withParams([getStart(1, 'day'), getEnd(90, 'days')], params), }; }; + +function withParams(value: any[], params: { fieldOperator?: string; isParsingVariable?: boolean }) { + if (params?.isParsingVariable && params?.fieldOperator && params.fieldOperator !== '$dateBetween') { + return value[0]; + } + + return value; +} + +export function inferPickerType(dateString: string): 'year' | 'month' | 'quarter' | 'date' { + if (/^\d{4}$/.test(dateString)) { + return 'year'; + } else if (/^\d{4}-\d{2}$/.test(dateString)) { + return 'month'; + } else if (/^\d{4}Q[1-4]$/.test(dateString)) { + return 'quarter'; + } else if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) { + return 'date'; + } else { + return 'date'; + } +} diff --git a/packages/core/client/src/schema-component/antd/error-fallback/ErrorFallbackModal.tsx b/packages/core/client/src/schema-component/antd/error-fallback/ErrorFallbackModal.tsx index 2c99836acc6..54b455dbcb7 100644 --- a/packages/core/client/src/schema-component/antd/error-fallback/ErrorFallbackModal.tsx +++ b/packages/core/client/src/schema-component/antd/error-fallback/ErrorFallbackModal.tsx @@ -7,11 +7,10 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { Modal } from 'antd'; +import { Modal, Typography } from 'antd'; import React, { FC } from 'react'; import { FallbackProps } from 'react-error-boundary'; import { ErrorFallback } from './ErrorFallback'; -import { Typography } from 'antd'; const { Paragraph, Text } = Typography; export const ErrorFallbackModal: FC = (props) => { @@ -42,7 +41,7 @@ export const ErrorFallbackModal: FC = (props) => { return ( <>
setOpen(true)}>{props.children || defaultChildren}
- setOpen(false)} width={'60%'}> + setOpen(false)} width={'60%'}> diff --git a/packages/core/client/src/schema-settings/DateFormat/ExpiresRadio.tsx b/packages/core/client/src/schema-component/antd/expiresRadio/index.tsx similarity index 71% rename from packages/core/client/src/schema-settings/DateFormat/ExpiresRadio.tsx rename to packages/core/client/src/schema-component/antd/expiresRadio/index.tsx index 7b5372353b2..17b8c46fdfb 100644 --- a/packages/core/client/src/schema-settings/DateFormat/ExpiresRadio.tsx +++ b/packages/core/client/src/schema-component/antd/expiresRadio/index.tsx @@ -9,12 +9,11 @@ import { css } from '@emotion/css'; import dayjs from 'dayjs'; - import { connect, mapProps } from '@formily/react'; import { useBoolean } from 'ahooks'; import { Input, Radio, Space } from 'antd'; -import React, { useState } from 'react'; -import { useToken } from '../../'; +import React, { useEffect, useState } from 'react'; +import { useToken } from '../../../'; const date = dayjs(); @@ -24,7 +23,7 @@ const spaceCSS = css` flex: 1; } `; -export const DateFormatCom = (props?) => { +const DateFormatCom = (props?) => { const date = dayjs(); return (
@@ -53,9 +52,11 @@ const DateTimeFormatPreview = ({ content }) => { }; const InternalExpiresRadio = (props) => { - const { onChange, defaultValue, formats, timeFormat } = props; + const { onChange, defaultValue, formats, picker } = props; const [isCustom, { setFalse, setTrue }] = useBoolean(props.value && !formats.includes(props.value)); - const targetValue = props.value && !formats.includes(props.value) ? props.value : defaultValue; + const [targetValue, setTargetValue] = useState( + props.value && !formats.includes(props.value) ? props.value : defaultValue, + ); const [customFormatPreview, setCustomFormatPreview] = useState(targetValue ? date.format(targetValue) : null); const onSelectChange = (v) => { if (v.target.value === 'custom') { @@ -66,6 +67,19 @@ const InternalExpiresRadio = (props) => { onChange(v.target.value); } }; + useEffect(() => { + if (!formats.includes(props.value)) { + setTrue(); + } else { + setFalse(); + } + setTargetValue(props.value && !formats.includes(props.value) ? props.value : defaultValue); + }, [props.value]); + + useEffect(() => { + setCustomFormatPreview(targetValue ? date.format(targetValue) : null); + }, [targetValue]); + return ( @@ -76,7 +90,7 @@ const InternalExpiresRadio = (props) => { { if (e.target.value) { setCustomFormatPreview(date.format(e.target.value)); @@ -86,19 +100,22 @@ const InternalExpiresRadio = (props) => { if (isCustom) { onChange(e.target.value); } + setTargetValue(e.target.value); }} /> ); } - return ( - - - {v.label} - - - ); + if (!picker || picker === 'date') { + return ( + + + {v.label} + + + ); + } })} @@ -113,4 +130,4 @@ const ExpiresRadio = connect( }), ); -export { ExpiresRadio }; +export { ExpiresRadio, DateFormatCom }; diff --git a/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx b/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx index 36987f612ac..79025dabd22 100644 --- a/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx +++ b/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx @@ -64,6 +64,8 @@ export const DynamicComponent = (props: Props) => { minWidth: 150, ...props.style, }, + utc: false, + underFilter: true, }), name: 'value', 'x-read-pretty': false, diff --git a/packages/core/client/src/schema-component/antd/filter/FilterGroup.tsx b/packages/core/client/src/schema-component/antd/filter/FilterGroup.tsx index 08ec982e745..9aaf6fd6305 100644 --- a/packages/core/client/src/schema-component/antd/filter/FilterGroup.tsx +++ b/packages/core/client/src/schema-component/antd/filter/FilterGroup.tsx @@ -95,6 +95,9 @@ export const FilterGroup = connect((props) => { field.value = { [logic]: items, }; + field.initialValue = { + [logic]: items, + }; }} > {t('Add condition')} diff --git a/packages/core/client/src/schema-component/antd/filter/__tests__/useOperators.test.ts b/packages/core/client/src/schema-component/antd/filter/__tests__/useOperators.test.ts new file mode 100644 index 00000000000..b2cfcc016a7 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/filter/__tests__/useOperators.test.ts @@ -0,0 +1,93 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { useFieldSchema } from '@formily/react'; +import { renderHook } from '@testing-library/react-hooks'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../../collection-manager'; +import { useOperatorList } from '../useOperators'; + +// Mock dependencies +vi.mock('@formily/react', () => ({ + useFieldSchema: vi.fn(), +})); + +vi.mock('../../../../collection-manager', () => ({ + useCollection_deprecated: vi.fn(), + useCollectionManager_deprecated: vi.fn(), +})); + +describe('useOperatorList', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return operators list based on x-designer-props', () => { + const mockSchema = { + 'x-designer-props': { + interface: 'input', + }, + }; + const mockOperators = ['eq', 'ne']; + + vi.mocked(useFieldSchema).mockReturnValue(mockSchema as any); + vi.mocked(useCollection_deprecated).mockReturnValue({ name: 'test' } as any); + vi.mocked(useCollectionManager_deprecated).mockReturnValue({ + getCollectionFields: vi.fn(), + getInterface: vi.fn().mockReturnValue({ + filterable: { + operators: mockOperators, + }, + }), + } as any); + + const { result } = renderHook(() => useOperatorList()); + expect(result.current).toEqual(mockOperators); + }); + + it('should return operators list based on field interface', () => { + const mockSchema = { name: 'testField' }; + const mockOperators = ['contains', 'notContains']; + const mockField = { name: 'testField', interface: 'input' }; + + vi.mocked(useFieldSchema).mockReturnValue(mockSchema as any); + vi.mocked(useCollection_deprecated).mockReturnValue({ name: 'test' } as any); + vi.mocked(useCollectionManager_deprecated).mockReturnValue({ + getCollectionFields: vi.fn().mockReturnValue([mockField]), + getInterface: vi.fn().mockReturnValue({ + filterable: { + operators: mockOperators, + }, + }), + } as any); + + const { result } = renderHook(() => useOperatorList()); + expect(result.current).toEqual(mockOperators); + }); + + it('should filter out invisible operators', () => { + const mockSchema = { name: 'testField' }; + const mockOperators = [{ name: 'eq', visible: () => true }, { name: 'ne', visible: () => false }, { name: 'gt' }]; + const mockField = { name: 'testField', interface: 'input' }; + + vi.mocked(useFieldSchema).mockReturnValue(mockSchema as any); + vi.mocked(useCollection_deprecated).mockReturnValue({ name: 'test' } as any); + vi.mocked(useCollectionManager_deprecated).mockReturnValue({ + getCollectionFields: vi.fn().mockReturnValue([mockField]), + getInterface: vi.fn().mockReturnValue({ + filterable: { + operators: mockOperators, + }, + }), + } as any); + + const { result } = renderHook(() => useOperatorList()); + expect(result.current).toEqual([{ name: 'eq', visible: expect.any(Function) }, { name: 'gt' }]); + }); +}); diff --git a/packages/core/client/src/schema-component/antd/filter/useFilterActionProps.ts b/packages/core/client/src/schema-component/antd/filter/useFilterActionProps.ts index 19a8f50a4ad..e193e2a2df2 100644 --- a/packages/core/client/src/schema-component/antd/filter/useFilterActionProps.ts +++ b/packages/core/client/src/schema-component/antd/filter/useFilterActionProps.ts @@ -21,9 +21,9 @@ export const useGetFilterOptions = () => { const { getCollectionFields } = useCollectionManager_deprecated(); const getFilterFieldOptions = useGetFilterFieldOptions(); - return (collectionName, dataSource?: string) => { + return (collectionName, dataSource?: string, usedInVariable?: boolean) => { const fields = getCollectionFields(collectionName, dataSource); - const options = getFilterFieldOptions(fields); + const options = getFilterFieldOptions(fields, usedInVariable); return options; }; }; @@ -39,18 +39,20 @@ export const useGetFilterFieldOptions = () => { const fieldSchema = useFieldSchema(); const nonfilterable = fieldSchema?.['x-component-props']?.nonfilterable || []; const { getCollectionFields, getInterface } = useCollectionManager_deprecated(); - const field2option = (field, depth) => { + const field2option = (field, depth, usedInVariable?: boolean) => { if (nonfilterable.length && depth === 1 && nonfilterable.includes(field.name)) { return; } if (!field.interface) { return; } + const fieldInterface = getInterface(field.interface); - if (!fieldInterface?.filterable) { + if (!fieldInterface?.filterable && !usedInVariable) { return; } - const { nested, children, operators } = fieldInterface.filterable; + + const { nested, children, operators } = fieldInterface?.filterable || {}; const option = { name: field.name, type: field.type, @@ -80,17 +82,17 @@ export const useGetFilterFieldOptions = () => { } return option; }; - const getOptions = (fields, depth) => { + const getOptions = (fields, depth, usedInVariable?: boolean) => { const options = []; fields.forEach((field) => { - const option = field2option(field, depth); + const option = field2option(field, depth, usedInVariable); if (option) { options.push(option); } }); return options; }; - return (fields) => getOptions(fields, 1); + return (fields, usedInVariable) => getOptions(fields, 1, usedInVariable); }; export const useFilterFieldOptions = (fields) => { diff --git a/packages/core/client/src/schema-component/antd/filter/useOperators.ts b/packages/core/client/src/schema-component/antd/filter/useOperators.ts index 35b17208774..12a5e2328a8 100644 --- a/packages/core/client/src/schema-component/antd/filter/useOperators.ts +++ b/packages/core/client/src/schema-component/antd/filter/useOperators.ts @@ -27,7 +27,8 @@ export const useOperatorList = (): any[] => { return getInterface(fieldInterface)?.filterable?.operators || []; } const field = collectionFields.find((item) => item.name === schema.name); - return getInterface(field?.interface)?.filterable?.operators || []; + const ops = getInterface(field?.interface)?.filterable?.operators || []; + return ops.filter((o) => typeof o.visible !== 'function' || o.visible(field)); }, [schema.name]); return res; }; diff --git a/packages/core/client/src/schema-component/antd/form-item/FormItem.Settings.tsx b/packages/core/client/src/schema-component/antd/form-item/FormItem.Settings.tsx index 57782fff103..788741a5c22 100644 --- a/packages/core/client/src/schema-component/antd/form-item/FormItem.Settings.tsx +++ b/packages/core/client/src/schema-component/antd/form-item/FormItem.Settings.tsx @@ -27,6 +27,7 @@ import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider'; import { useRecord } from '../../../record-provider'; import { useColumnSchema } from '../../../schema-component/antd/table-v2/Table.Column.Decorator'; import { generalSettingsItems } from '../../../schema-items/GeneralSettings'; +import { getTempFieldState } from '../../../schema-settings/LinkageRules/bindLinkageRulesToFiled'; import { ActionType } from '../../../schema-settings/LinkageRules/type'; import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope'; import { SchemaSettingsDateFormat } from '../../../schema-settings/SchemaSettingsDateFormat'; @@ -41,7 +42,6 @@ import { useCompile, useDesignable, useFieldModeOptions } from '../../hooks'; import { isSubMode } from '../association-field/util'; import { removeNullCondition } from '../filter'; import { DynamicComponentProps } from '../filter/DynamicComponent'; -import { getTempFieldState } from '../form-v2/utils'; import { useColorFields } from '../table-v2/Table.Column.Designer'; export const allowAddNew: SchemaSettingsItemType = { diff --git a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx index e17b0b2f44c..62a85114ae9 100644 --- a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx +++ b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx @@ -10,7 +10,7 @@ import { css, cx } from '@emotion/css'; import { IFormItemProps, FormItem as Item } from '@formily/antd-v5'; import { Field } from '@formily/core'; -import { observer, useField, useFieldSchema, useForm } from '@formily/react'; +import { observer, useField, useFieldSchema } from '@formily/react'; import React, { useEffect, useMemo } from 'react'; import { ACLCollectionFieldProvider } from '../../../acl/ACLProvider'; import { useApp } from '../../../application'; @@ -18,15 +18,15 @@ import { useFormActiveFields } from '../../../block-provider/hooks/useFormActive import { Collection_deprecated } from '../../../collection-manager'; import { CollectionFieldProvider } from '../../../data-source/collection-field/CollectionFieldProvider'; import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; +import { useDataFormItemProps } from '../../../modules/blocks/data-blocks/form/hooks/useDataFormItemProps'; import { GeneralSchemaDesigner } from '../../../schema-settings'; import { BlockItem } from '../block-item'; import { HTMLEncode } from '../input/shared'; import { FilterFormDesigner } from './FormItem.FilterFormDesigner'; import { useEnsureOperatorsValid } from './SchemaSettingOptions'; import useLazyLoadDisplayAssociationFieldsOfForm from './hooks/useLazyLoadDisplayAssociationFieldsOfForm'; +import { useLinkageRulesForSubTableOrSubForm } from './hooks/useLinkageRulesForSubTableOrSubForm'; import useParseDefaultValue from './hooks/useParseDefaultValue'; -import { useVariables, useContextVariable } from '../../../variables'; -import { useDataFormItemProps } from '../../../modules/blocks/data-blocks/form/hooks/useDataFormItemProps'; Item.displayName = 'FormilyFormItem'; @@ -51,16 +51,11 @@ export const FormItem: any = withDynamicSchemaProps( const field = useField(); const schema = useFieldSchema(); const { addActiveFieldName } = useFormActiveFields() || {}; - const form = useForm(); const { wrapperStyle } = useDataFormItemProps(); - const variables = useVariables(); - const contextVariable = useContextVariable(); - useEffect(() => { - variables?.registerVariable(contextVariable); - }, [contextVariable, variables]); - // 需要放在注冊完变量之后 + useParseDefaultValue(); useLazyLoadDisplayAssociationFieldsOfForm(); + useLinkageRulesForSubTableOrSubForm(); useEffect(() => { addActiveFieldName?.(schema.name as string); diff --git a/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx b/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx index 9143ab64f47..a5700807d23 100644 --- a/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx +++ b/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx @@ -496,7 +496,6 @@ export const EditOperator = () => { const { dn } = useDesignable(); const operatorList = useOperatorList(); const { getOperator, collectOperator } = useOperators(); - if (operatorList.length && !getOperator(fieldSchema.name)) { collectOperator(fieldSchema.name, operatorList[0].value); } @@ -512,22 +511,22 @@ export const EditOperator = () => { _.set(fieldSchema, 'x-filter-operator', v); const operator = operatorList.find((item) => item.value === v); - let componentProps = {}; - + let componentProps = { ...fieldSchema['x-component-props'] }; + field.value = undefined; //切换操作符清空字段值 // 根据操作符的配置,设置组件的属性 - if (operator?.schema?.['x-component']) { + if (operator?.schema?.['x-component'] && !operator?.onlyFilterAction) { _.set(fieldSchema, 'x-component-props.component', operator.schema?.['x-component']); _.set(field, 'componentProps.component', operator.schema?.['x-component']); - field.reset(); componentProps = { + ...fieldSchema['x-component-props'], component: operator.schema['x-component'], ...operator.schema?.['x-component-props'], }; } else if (fieldSchema['x-component-props']?.component) { _.set(fieldSchema, 'x-component-props.component', null); _.set(field, 'componentProps.component', null); - field.reset(); componentProps = { + ...fieldSchema['x-component-props'], component: null, ...operator.schema?.['x-component-props'], }; diff --git a/packages/core/client/src/schema-component/antd/form-item/__tests__/hooks/useSpecialCase.test.ts b/packages/core/client/src/schema-component/antd/form-item/__tests__/hooks/useSpecialCase.test.ts index 4c741aef8e0..ddcecc88ea2 100644 --- a/packages/core/client/src/schema-component/antd/form-item/__tests__/hooks/useSpecialCase.test.ts +++ b/packages/core/client/src/schema-component/antd/form-item/__tests__/hooks/useSpecialCase.test.ts @@ -8,7 +8,7 @@ */ import { vi } from 'vitest'; -import { isSpecialCaseField, transformValue } from '../../hooks/useSpecialCase'; +import { isSpecialCaseField, isSubset, transformValue } from '../../hooks/useSpecialCase'; describe('transformValue', () => { it('value is an array', () => { @@ -128,3 +128,44 @@ describe('isSpecialCaseField', () => { expect(result).toBe(true); }); }); + +describe('isSubset', () => { + it('should return true when two arrays are identical', () => { + const arr1 = [ + { a: 1, b: 2 }, + { c: 3, d: 4 }, + ]; + const arr2 = [ + { a: 1, b: 2 }, + { c: 3, d: 4 }, + ]; + expect(isSubset(arr1, arr2)).toBe(true); + }); + + it('should return false when array lengths are different', () => { + const arr1 = [{ a: 1 }]; + const arr2 = [{ a: 1 }, { b: 2 }]; + expect(isSubset(arr1, arr2)).toBe(false); + }); + + it('should ignore null values and return true', () => { + const arr1 = [ + { a: 1, b: null }, + { c: 3, d: null }, + ]; + const arr2 = [{ a: 1 }, { c: 3 }]; + expect(isSubset(arr1, arr2)).toBe(true); + }); + + it('should return false when objects do not match', () => { + const arr1 = [ + { a: 1, b: 2 }, + { c: 3, d: 4 }, + ]; + const arr2 = [ + { a: 1, b: 2 }, + { c: 3, d: 5 }, + ]; + expect(isSubset(arr1, arr2)).toBe(false); + }); +}); diff --git a/packages/core/client/src/schema-component/antd/form-item/hooks/useLazyLoadDisplayAssociationFieldsOfForm.ts b/packages/core/client/src/schema-component/antd/form-item/hooks/useLazyLoadDisplayAssociationFieldsOfForm.ts index 170a09049a4..a4f26348db0 100644 --- a/packages/core/client/src/schema-component/antd/form-item/hooks/useLazyLoadDisplayAssociationFieldsOfForm.ts +++ b/packages/core/client/src/schema-component/antd/form-item/hooks/useLazyLoadDisplayAssociationFieldsOfForm.ts @@ -9,6 +9,7 @@ import { Field } from '@formily/core'; import { useField, useFieldSchema, useForm } from '@formily/react'; +import { untracked } from '@formily/reactive'; import { nextTick } from '@nocobase/utils/client'; import _ from 'lodash'; import { useEffect, useMemo, useRef } from 'react'; @@ -20,7 +21,6 @@ import { useVariables } from '../../../../variables'; import { transformVariableValue } from '../../../../variables/utils/transformVariableValue'; import { useSubFormValue } from '../../association-field/hooks'; import { isDisplayField } from '../utils'; -import { untracked } from '@formily/reactive'; /** * 用于懒加载 Form 区块中只用于展示的关联字段的值 @@ -97,7 +97,7 @@ const useLazyLoadDisplayAssociationFieldsOfForm = () => { variables .parseVariable(variableString, formVariable, { appends }) - .then((value) => { + .then(({ value }) => { nextTick(() => { const result = transformVariableValue(value, { targetCollectionField: collectionFieldRef.current }); // fix https://nocobase.height.app/T-2608 diff --git a/packages/core/client/src/schema-component/antd/form-item/hooks/useLinkageRulesForSubTableOrSubForm.ts b/packages/core/client/src/schema-component/antd/form-item/hooks/useLinkageRulesForSubTableOrSubForm.ts new file mode 100644 index 00000000000..835b5e6f3bb --- /dev/null +++ b/packages/core/client/src/schema-component/antd/form-item/hooks/useLinkageRulesForSubTableOrSubForm.ts @@ -0,0 +1,90 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +/* eslint-disable react-hooks/rules-of-hooks */ +import { Field } from '@formily/core'; +import { useField, useFieldSchema } from '@formily/react'; +import { useEffect } from 'react'; +import { useFlag } from '../../../../flag-provider'; +import { bindLinkageRulesToFiled } from '../../../../schema-settings/LinkageRules/bindLinkageRulesToFiled'; +import { forEachLinkageRule } from '../../../../schema-settings/LinkageRules/forEachLinkageRule'; +import useLocalVariables from '../../../../variables/hooks/useLocalVariables'; +import useVariables from '../../../../variables/hooks/useVariables'; +import { useSubFormValue } from '../../association-field/hooks'; + +/** + * used to bind the linkage rules of the sub-table or sub-form with the current field + */ +export const useLinkageRulesForSubTableOrSubForm = () => { + const { isInSubForm, isInSubTable } = useFlag(); + + if (!isInSubForm && !isInSubTable) { + return; + } + + const field = useField(); + const fieldSchema = useFieldSchema(); + const { fieldSchema: schemaOfSubTableOrSubForm, formValue } = useSubFormValue(); + const localVariables = useLocalVariables(); + const variables = useVariables(); + + const linkageRules = getLinkageRules(schemaOfSubTableOrSubForm); + + useEffect(() => { + if (!(field.onUnmount as any).__rested) { + const _onUnmount = field.onUnmount; + field.onUnmount = () => { + (field as any).__disposes?.forEach((dispose) => { + dispose(); + }); + _onUnmount(); + }; + (field.onUnmount as any).__rested = true; + } + + if (!linkageRules) { + return; + } + + if ((field as any).__disposes) { + (field as any).__disposes.forEach((dispose) => { + dispose(); + }); + } + + const disposes = ((field as any).__disposes = []); + + forEachLinkageRule(linkageRules, (action, rule) => { + if (action.targetFields?.includes(fieldSchema.name)) { + disposes.push( + bindLinkageRulesToFiled({ + field, + linkageRules, + formValues: formValue, + localVariables, + action, + rule, + variables, + variableNameOfLeftCondition: '$iteration', + }), + ); + } + }); + + (field as any).__linkageRules = linkageRules; + }, [field, fieldSchema?.name, formValue, JSON.stringify(linkageRules), localVariables, variables]); +}; + +function getLinkageRules(fieldSchema) { + if (!fieldSchema) { + return; + } + + return fieldSchema['x-linkage-rules']?.filter((k) => !k.disabled); +} diff --git a/packages/core/client/src/schema-component/antd/form-item/hooks/useParseDefaultValue.ts b/packages/core/client/src/schema-component/antd/form-item/hooks/useParseDefaultValue.ts index a9b50e010f7..71a5e3b52b1 100644 --- a/packages/core/client/src/schema-component/antd/form-item/hooks/useParseDefaultValue.ts +++ b/packages/core/client/src/schema-component/antd/form-item/hooks/useParseDefaultValue.ts @@ -14,8 +14,12 @@ import { getValuesByPath } from '@nocobase/utils/client'; import _ from 'lodash'; import { useCallback, useEffect } from 'react'; import { useRecordIndex } from '../../../../../src/record-provider'; -import { useCollection_deprecated } from '../../../../collection-manager'; +import { useOperators } from '../../../../block-provider/CollectOperators'; +import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider'; +import { InheritanceCollectionMixin, useCollection_deprecated } from '../../../../collection-manager'; import { useCollectionRecord } from '../../../../data-source/collection-record/CollectionRecordProvider'; +import { DataSourceManager } from '../../../../data-source/data-source/DataSourceManager'; +import { useDataSourceManager } from '../../../../data-source/data-source/DataSourceManagerProvider'; import { useFlag } from '../../../../flag-provider'; import { DEBOUNCE_WAIT, useLocalVariables, useVariables } from '../../../../variables'; import { getPath } from '../../../../variables/utils/getPath'; @@ -39,6 +43,9 @@ const useParseDefaultValue = () => { const { getField } = useCollection_deprecated(); const { isSpecialCase, setDefaultValue } = useSpecialCase(); const index = useRecordIndex(); + const { type, form } = useFormBlockContext(); + const { getOperator } = useOperators(); + const dm = useDataSourceManager(); /** * name: 如 $user @@ -55,6 +62,13 @@ const useParseDefaultValue = () => { ); useEffect(() => { + // fix https://github.com/nocobase/nocobase/issues/4868 + // fix https://tasks.aliyun.nocobase.com/admin/ugmnj2ycfgg/popups/1qlw5c38t3b/puid/dz42x7ffr7i/filterbytk/182 + // to clear the default value of the field + if (type === 'update' && fieldSchema.default && field.form === form) { + field.setValue?.(record?.data?.[fieldSchema.name]); + } + if ( fieldSchema.default == null || isInSetDefaultValueDialog || @@ -86,7 +100,30 @@ const useParseDefaultValue = () => { } } - const value = transformVariableValue(await variables.parseVariable(fieldSchema.default, localVariables), { + const { + value: parsedValue, + collectionName: collectionNameOfVariable, + dataSource = 'main', + } = await variables.parseVariable(fieldSchema.default, localVariables, { + fieldOperator: getOperator(fieldSchema.name), + }); + + // fix https://tasks.aliyun.nocobase.com/admin/ugmnj2ycfgg/popups/1qlw5c38t3b/puid/dz42x7ffr7i/filterbytk/199 + if ( + collectionField?.target && + collectionField.target !== collectionNameOfVariable && + !isInherit({ + collectionName: collectionField.target, + targetCollectionName: collectionNameOfVariable, + dm, + dataSource, + }) + ) { + field.loading = false; + return; + } + + const value = transformVariableValue(parsedValue, { targetCollectionField: collectionField, }); @@ -154,7 +191,30 @@ const useParseDefaultValue = () => { // 解决子表格(或子表单)中新增一行数据时,默认值不生效的问题 field.setValue(fieldSchema.default); } - }, [fieldSchema.default, localVariables]); + }, [fieldSchema.default, localVariables, type, getOperator, dm]); }; export default useParseDefaultValue; + +/** + * Determine if there is an inheritance relationship between two data tables + * @param param0 + * @returns + */ +const isInherit = ({ + collectionName, + targetCollectionName, + dm, + dataSource, +}: { + collectionName: string; + targetCollectionName: string; + dm: DataSourceManager; + dataSource: string; +}) => { + const cm = dm?.getDataSource(dataSource)?.collectionManager; + return cm + ?.getCollection(collectionName) + ?.getAllCollectionsInheritChain() + ?.includes(targetCollectionName); +}; diff --git a/packages/core/client/src/schema-component/antd/form-item/hooks/useSpecialCase.ts b/packages/core/client/src/schema-component/antd/form-item/hooks/useSpecialCase.ts index 4cff8dd9ac2..699d97b65e5 100644 --- a/packages/core/client/src/schema-component/antd/form-item/hooks/useSpecialCase.ts +++ b/packages/core/client/src/schema-component/antd/form-item/hooks/useSpecialCase.ts @@ -45,7 +45,16 @@ export const useSpecialCase = () => { if (parentFieldSchema) { const parentField: any = form.query(parentFieldSchema.name).take(); if (parentField) { - parentField.setInitialValue(transformValue(value, { field: parentField, subFieldSchema: fieldSchema })); + const newValue = _.isEmpty(value) + ? [] + : _.map(transformValue(value, { field: parentField, subFieldSchema: fieldSchema }), (item) => + markRecordAsNew(item), + ); + + // Use isSubset to determine if newValue is a subset of parentField.initialValue, preventing infinite loops + if (!isSubset(newValue, parentField.initialValue)) { + parentField.setInitialValue(newValue); + } } } }, @@ -181,12 +190,41 @@ export function isFromDatabase(value: Record) { export const useSubTableSpecialCase = ({ field }) => { useEffect(() => { if (_.isEmpty(field.value)) { - const value = field.value; - field.value = [markRecordAsNew({})]; + const emptyValue = field.value; + const newValue = [markRecordAsNew({})]; + field.value = newValue; // 因为默认值的解析是异步的,所以下面的代码会优先于默认值的设置,这样就防止了设置完默认值后又被清空的问题 setTimeout(() => { - field.value = value; + if (JSON.stringify(field.value) === JSON.stringify(newValue)) { + field.value = emptyValue; + } }); } }, []); }; + +/** + * Determines if one array is a subset of another array + * @param subset The potential subset + * @param superset The potential superset + * @returns Returns true if subset is a subset of superset, otherwise false + */ +export function isSubset(subset: any[], superset: any[]): boolean { + // If lengths are different, it's definitely not a subset + if (subset.length !== superset.length) { + return false; + } + + // Compare each element + for (let i = 0; i < subset.length; i++) { + const subsetItem = subset[i]; + const supersetItem = superset[i]; + // Use _.omitBy to remove null values, then compare objects with _.isMatch + if (!_.isMatch(_.omitBy(supersetItem, _.isNil), _.omitBy(subsetItem, _.isNil))) { + return false; + } + } + + // All elements match, it's a subset + return true; +} diff --git a/packages/core/client/src/schema-component/antd/form-v2/Form.tsx b/packages/core/client/src/schema-component/antd/form-v2/Form.tsx index 394dfed6818..508aa9e3cb1 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/Form.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/Form.tsx @@ -11,27 +11,19 @@ import { css } from '@emotion/css'; import { FormLayout, IFormLayoutProps } from '@formily/antd-v5'; import { Field, Form as FormilyForm, createForm, onFieldInit, onFormInputChange } from '@formily/core'; import { FieldContext, FormContext, RecursionField, observer, useField, useFieldSchema } from '@formily/react'; -import { reaction } from '@formily/reactive'; import { uid } from '@formily/shared'; -import { getValuesByPath } from '@nocobase/utils/client'; import { ConfigProvider, Spin, theme } from 'antd'; -import _ from 'lodash'; import React, { useEffect, useMemo } from 'react'; import { useActionContext } from '..'; import { useAttach, useComponent, useDesignable } from '../..'; import { useTemplateBlockContext } from '../../../block-provider/TemplateBlockProvider'; import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; -import { ActionType } from '../../../schema-settings/LinkageRules/type'; +import { bindLinkageRulesToFiled } from '../../../schema-settings/LinkageRules/bindLinkageRulesToFiled'; +import { forEachLinkageRule } from '../../../schema-settings/LinkageRules/forEachLinkageRule'; import { useToken } from '../../../style'; import { useLocalVariables, useVariables } from '../../../variables'; -import { VariableOption, VariablesContextType } from '../../../variables/types'; -import { getPath } from '../../../variables/utils/getPath'; -import { getVariableName } from '../../../variables/utils/getVariableName'; -import { getVariablesFromExpression, isVariable } from '../../../variables/utils/isVariable'; -import { getInnermostKeyAndValue, getTargetField } from '../../common/utils/uitls'; import { useProps } from '../../hooks/useProps'; import { useFormBlockHeight } from './hook'; -import { collectFieldStateOfLinkageRules, getTempFieldState } from './utils'; export interface FormProps extends IFormLayoutProps { form?: FormilyForm; @@ -144,49 +136,25 @@ const WithForm = (props: WithFormProps) => { const disposes = []; form.addEffects(id, () => { - linkageRules.forEach((rule) => { - rule.actions?.forEach((action) => { - if (action.targetFields?.length) { - const fields = action.targetFields.join(','); + forEachLinkageRule(linkageRules, (action, rule) => { + if (action.targetFields?.length) { + const fields = action.targetFields.join(','); - // 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `reaction` 代替 - onFieldInit(`*(${fields})`, (field: any, form) => { - field['initStateOfLinkageRules'] = { - display: field.initStateOfLinkageRules?.display || getTempFieldState(true, field.display), - required: field.initStateOfLinkageRules?.required || getTempFieldState(true, field.required || false), - pattern: field.initStateOfLinkageRules?.pattern || getTempFieldState(true, field.pattern), - value: - field.initStateOfLinkageRules?.value || getTempFieldState(true, field.value || field.initialValue), - }; - - disposes.push( - reaction( - // 这里共依赖 3 部分,当这 3 部分中的任意一部分发生变更后,需要触发联动规则: - // 1. 条件中的字段值; - // 2. 条件中的变量值; - // 3. value 表达式中的变量值; - () => { - // 获取条件中的字段值 - const fieldValuesInCondition = getFieldValuesInCondition({ linkageRules, formValues: form.values }); - - // 获取条件中的变量值 - const variableValuesInCondition = getVariableValuesInCondition({ linkageRules, localVariables }); - - // 获取 value 表达式中的变量值 - const variableValuesInExpression = getVariableValuesInExpression({ action, localVariables }); - - const result = [fieldValuesInCondition, variableValuesInCondition, variableValuesInExpression] - .map((item) => JSON.stringify(item)) - .join(','); - return result; - }, - getSubscriber(action, field, rule, variables, localVariables), - { fireImmediately: true, equals: _.isEqual }, - ), - ); - }); - } - }); + // 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `reaction` 代替 + onFieldInit(`*(${fields})`, (field: any, form) => { + disposes.push( + bindLinkageRulesToFiled({ + field, + linkageRules, + formValues: form.values, + localVariables, + action, + rule, + variables, + }), + ); + }); + } }); }); @@ -268,165 +236,3 @@ export const Form: React.FC & { }), { displayName: 'Form' }, ); - -function getSubscriber( - action: any, - field: any, - rule: any, - variables: VariablesContextType, - localVariables: VariableOption[], -): (value: string, oldValue: string) => void { - return () => { - // 当条件改变触发 reaction 时,会同步收集字段状态,并保存到 field.stateOfLinkageRules 中 - collectFieldStateOfLinkageRules({ - operator: action.operator, - value: action.value, - field, - condition: rule.condition, - variables, - localVariables, - }); - - // 当条件改变时,有可能会触发多个 reaction,所以这里需要延迟一下,确保所有的 reaction 都执行完毕后, - // 再从 field.stateOfLinkageRules 中取值,因为此时 field.stateOfLinkageRules 中的值才是全的。 - setTimeout(async () => { - const fieldName = getFieldNameByOperator(action.operator); - - // 防止重复赋值 - if (!field.stateOfLinkageRules?.[fieldName]) { - return; - } - - let stateList = field.stateOfLinkageRules[fieldName]; - - stateList = await Promise.all(stateList); - stateList = stateList.filter((v) => v.condition); - - const lastState = stateList[stateList.length - 1]; - - if (fieldName === 'value') { - // value 比较特殊,它只有在匹配条件时才需要赋值,当条件不匹配时,维持现在的值; - // stateList 中肯定会有一个初始值,所以当 stateList.length > 1 时,就说明有匹配条件的情况; - if (stateList.length > 1) { - field.value = lastState.value; - } - } else { - field[fieldName] = lastState?.value; - requestAnimationFrame(() => { - field.setState((state) => { - state[fieldName] = lastState?.value; - }); - }); - //字段隐藏时清空数据 - if (fieldName === 'display' && lastState?.value === 'none') { - field.value = null; - } - } - // 在这里清空 field.stateOfLinkageRules,就可以保证:当条件再次改变时,如果该字段没有和任何条件匹配,则需要把对应的值恢复到初始值; - field.stateOfLinkageRules[fieldName] = null; - }); - }; -} - -function getFieldNameByOperator(operator: ActionType) { - switch (operator) { - case ActionType.Required: - case ActionType.InRequired: - return 'required'; - case ActionType.Visible: - case ActionType.None: - case ActionType.Hidden: - return 'display'; - case ActionType.Editable: - case ActionType.ReadOnly: - case ActionType.ReadPretty: - return 'pattern'; - case ActionType.Value: - return 'value'; - default: - return null; - } -} - -function getFieldValuesInCondition({ linkageRules, formValues }) { - return linkageRules.map((rule) => { - const run = (condition) => { - const type = Object.keys(condition)[0] || '$and'; - const conditions = condition[type]; - - return conditions - .map((condition) => { - // fix https://nocobase.height.app/T-3251 - if ('$and' in condition || '$or' in condition) { - return run(condition); - } - - const path = getTargetField(condition).join('.'); - return getValuesByPath(formValues, path); - }) - .filter(Boolean); - }; - - return run(rule.condition); - }); -} - -function getVariableValuesInCondition({ - linkageRules, - localVariables, -}: { - linkageRules: any[]; - localVariables: VariableOption[]; -}) { - return linkageRules.map((rule) => { - const type = Object.keys(rule.condition)[0] || '$and'; - const conditions = rule.condition[type]; - - return conditions - .map((condition) => { - const jsonlogic = getInnermostKeyAndValue(condition); - if (!jsonlogic) { - return null; - } - if (isVariable(jsonlogic.value)) { - return getVariableValue(jsonlogic.value, localVariables); - } - - return jsonlogic.value; - }) - .filter(Boolean); - }); -} - -function getVariableValuesInExpression({ action, localVariables }) { - const actionValue = action.value; - const mode = actionValue?.mode; - const value = actionValue?.value || actionValue?.result; - - if (mode !== 'express') { - return; - } - - if (value == null) { - return; - } - - return getVariablesFromExpression(value) - ?.map((variableString: string) => { - return getVariableValue(variableString, localVariables); - }) - .filter(Boolean); -} - -function getVariableValue(variableString: string, localVariables: VariableOption[]) { - if (!isVariable(variableString)) { - return; - } - - const variableName = getVariableName(variableString); - const ctx = { - [variableName]: localVariables.find((item) => item.name === variableName)?.ctx, - }; - - return getValuesByPath(ctx, getPath(variableString)); -} diff --git a/packages/core/client/src/schema-component/antd/form-v2/utils.tsx b/packages/core/client/src/schema-component/antd/form-v2/utils.tsx deleted file mode 100644 index 0525864870c..00000000000 --- a/packages/core/client/src/schema-component/antd/form-v2/utils.tsx +++ /dev/null @@ -1,185 +0,0 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -import { Field } from '@formily/core'; -import { evaluators } from '@nocobase/evaluators/client'; -import { uid } from '@nocobase/utils/client'; -import _ from 'lodash'; -import { ActionType } from '../../../schema-settings/LinkageRules/type'; -import { VariableOption, VariablesContextType } from '../../../variables/types'; -import { REGEX_OF_VARIABLE_IN_EXPRESSION } from '../../../variables/utils/isVariable'; -import { conditionAnalyses } from '../../common/utils/uitls'; - -interface Props { - operator; - value; - field: Field & { - [key: string]: any; - }; - condition; - variables: VariablesContextType; - localVariables: VariableOption[]; -} - -/** - * 获取字段临时状态对象 - */ -export async function getTempFieldState(condition: boolean | Promise, value: any) { - [condition, value] = await Promise.all([condition, value]); - - return { - condition, - value, - }; -} - -export const collectFieldStateOfLinkageRules = ({ - operator, - value, - field, - condition, - variables, - localVariables, -}: Props) => { - const requiredResult = field?.stateOfLinkageRules?.required || [field?.initStateOfLinkageRules?.required]; - const displayResult = field?.stateOfLinkageRules?.display || [field?.initStateOfLinkageRules?.display]; - const patternResult = field?.stateOfLinkageRules?.pattern || [field?.initStateOfLinkageRules?.pattern]; - const valueResult = field?.stateOfLinkageRules?.value || [field?.initStateOfLinkageRules?.value]; - const { evaluate } = evaluators.get('formula.js'); - - switch (operator) { - case ActionType.Required: - requiredResult.push( - getTempFieldState(conditionAnalyses({ ruleGroup: condition, variables, localVariables }), true), - ); - field.stateOfLinkageRules = { - ...field.stateOfLinkageRules, - required: requiredResult, - }; - break; - case ActionType.InRequired: - requiredResult.push( - getTempFieldState(conditionAnalyses({ ruleGroup: condition, variables, localVariables }), false), - ); - field.stateOfLinkageRules = { - ...field.stateOfLinkageRules, - required: requiredResult, - }; - break; - case ActionType.Visible: - case ActionType.None: - case ActionType.Hidden: - displayResult.push( - getTempFieldState(conditionAnalyses({ ruleGroup: condition, variables, localVariables }), operator), - ); - field.stateOfLinkageRules = { - ...field.stateOfLinkageRules, - display: displayResult, - }; - break; - case ActionType.Editable: - case ActionType.ReadOnly: - case ActionType.ReadPretty: - patternResult.push( - getTempFieldState(conditionAnalyses({ ruleGroup: condition, variables, localVariables }), operator), - ); - field.stateOfLinkageRules = { - ...field.stateOfLinkageRules, - pattern: patternResult, - }; - break; - case ActionType.Value: - { - const getValue = async () => { - if (value?.mode === 'express') { - if ((value.value || value.result) == null) { - return; - } - - // 解析如 `{{$user.name}}` 之类的变量 - const { exp, scope: expScope } = await replaceVariables(value.value || value.result, { - variables, - localVariables, - }); - - try { - const result = evaluate(exp, { now: () => new Date().toString(), ...expScope }); - return result; - } catch (error) { - console.error(error); - } - } else if (value?.mode === 'constant') { - return value?.value ?? value; - } else { - return null; - } - }; - if (isConditionEmpty(condition)) { - valueResult.push(getTempFieldState(true, getValue())); - } else { - valueResult.push( - getTempFieldState(conditionAnalyses({ ruleGroup: condition, variables, localVariables }), getValue()), - ); - } - field.stateOfLinkageRules = { - ...field.stateOfLinkageRules, - value: valueResult, - }; - } - break; - default: - return null; - } -}; - -export async function replaceVariables( - value: string, - { - variables, - localVariables, - }: { - variables: VariablesContextType; - localVariables: VariableOption[]; - }, -) { - const store = {}; - const scope = {}; - - if (value == null) { - return; - } - - const waitForParsing = value.match(REGEX_OF_VARIABLE_IN_EXPRESSION)?.map(async (item) => { - const result = await variables.parseVariable(item, localVariables); - - // 在开头加 `_` 是为了保证 id 不能以数字开头,否则在解析表达式的时候(不是解析变量)会报错 - const id = `_${uid()}`; - - scope[id] = result; - store[item] = id; - return result; - }); - - if (waitForParsing) { - await Promise.all(waitForParsing); - } - return { - exp: value.replace(REGEX_OF_VARIABLE_IN_EXPRESSION, (match) => { - return `{{${store[match] || match}}}`; - }), - scope, - }; -} - -function isConditionEmpty(rules: { $and?: any; $or?: any }) { - const type = Object.keys(rules)[0] || '$and'; - const conditions = rules[type]; - - return _.isEmpty(conditions); -} diff --git a/packages/core/client/src/schema-component/antd/grid-card/GridCard.Item.tsx b/packages/core/client/src/schema-component/antd/grid-card/GridCard.Item.tsx index 0e93250c58f..1bcba0f03a5 100644 --- a/packages/core/client/src/schema-component/antd/grid-card/GridCard.Item.tsx +++ b/packages/core/client/src/schema-component/antd/grid-card/GridCard.Item.tsx @@ -45,7 +45,7 @@ export const GridCardItem = withDynamicSchemaProps( const field = useField(); const parentRecordData = useCollectionParentRecordData(); return ( - +
{props.children} diff --git a/packages/core/client/src/schema-component/antd/grid-card/GridCard.tsx b/packages/core/client/src/schema-component/antd/grid-card/GridCard.tsx index 01d5dd4aee6..deb748099ab 100644 --- a/packages/core/client/src/schema-component/antd/grid-card/GridCard.tsx +++ b/packages/core/client/src/schema-component/antd/grid-card/GridCard.tsx @@ -81,14 +81,14 @@ const usePaginationProps = () => { const field = useField(); const { service, columnCount: _columnCount = defaultColumnCount } = useGridCardBlockContext(); const meta = service?.data?.meta; - const { count, pageSize, page } = meta || {}; - + const { count, pageSize, page, hasNext } = meta || {}; if (count) { return { total: count || 0, pageSize: pageSize || 10, current: page || 1, pageSizeOptions, + showSizeChanger: true, }; } else { return { @@ -99,7 +99,7 @@ const usePaginationProps = () => { showTitle: false, showSizeChanger: true, hideOnSinglePage: false, - total: field.value?.length < pageSize ? pageSize * page : pageSize * page + 1, + total: field.value?.length < pageSize || !hasNext ? pageSize * page : pageSize * page + 1, className: css` .ant-pagination-simple-pager { display: none !important; diff --git a/packages/core/client/src/schema-component/antd/index.ts b/packages/core/client/src/schema-component/antd/index.ts index ba15dd74b33..36b5d5bdbe8 100644 --- a/packages/core/client/src/schema-component/antd/index.ts +++ b/packages/core/client/src/schema-component/antd/index.ts @@ -62,5 +62,6 @@ export * from './variable'; export * from './unix-timestamp'; export * from './nanoid-input'; export * from './error-fallback'; +export * from './expiresRadio'; import './index.less'; diff --git a/packages/core/client/src/schema-component/antd/input-number/ReadPretty.tsx b/packages/core/client/src/schema-component/antd/input-number/ReadPretty.tsx index 61cf1b2d9cd..908bd489ff7 100644 --- a/packages/core/client/src/schema-component/antd/input-number/ReadPretty.tsx +++ b/packages/core/client/src/schema-component/antd/input-number/ReadPretty.tsx @@ -9,10 +9,10 @@ import { isValid } from '@formily/shared'; import { toFixedByStep } from '@nocobase/utils/client'; +import BigNumber from 'bignumber.js'; import { format } from 'd3-format'; import * as math from 'mathjs'; import React, { useMemo } from 'react'; -import BigNumber from 'bignumber.js'; function countDecimalPlaces(value) { const number = Number(value); @@ -175,7 +175,7 @@ export const ReadPretty: React.FC = (props) => { } return ( -
+
{addonBefore} {addonAfter} diff --git a/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx b/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx index 17452276f4c..1fd7bce53ae 100644 --- a/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx +++ b/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx @@ -46,7 +46,10 @@ ReadPretty.Input = (props: InputReadPrettyProps) => { // eslint-disable-next-line react-hooks/rules-of-hooks const compile = useCompile(); return ( -
+
{props.addonBefore} {props.prefix} @@ -98,7 +101,10 @@ ReadPretty.TextArea = (props) => { value ); return ( -
+
{props.addonBefore} {props.prefix} {content} @@ -150,7 +156,10 @@ ReadPretty.Html = (props) => { ); return ( -
+
{props.addonBefore} {props.prefix} {content} @@ -169,18 +178,19 @@ export interface URLReadPrettyProps { suffix?: React.ReactNode; addonAfter?: React.ReactNode; prefixCls?: string; + ellipsis?: boolean; } -ReadPretty.URL = (props) => { +ReadPretty.URL = (props: URLReadPrettyProps) => { // eslint-disable-next-line react-hooks/rules-of-hooks const prefixCls = usePrefixCls('description-url', props); const content = props.value && ( - + {props.value} ); return ( -
+
{props.addonBefore} {props.prefix} {content} @@ -223,12 +233,14 @@ export interface JSONTextAreaReadPrettyProps { style?: React.CSSProperties; space?: number; prefixCls?: string; + ellipsis?: boolean; } -ReadPretty.JSON = (props) => { +ReadPretty.JSON = (props: JSONTextAreaReadPrettyProps) => { // eslint-disable-next-line react-hooks/rules-of-hooks const prefixCls = usePrefixCls('json', props); - return ( + const content = props.value != null ? JSON.stringify(props.value, null, props.space ?? 2) : ''; + const JSONContent = (
 {
       )}
       style={props.style}
     >
-      {props.value != null ? JSON.stringify(props.value, null, props.space ?? 2) : ''}
+      {content}
     
); + + if (props.ellipsis) { + return ( + + {content} + + ); + } + + return JSONContent; }; diff --git a/packages/core/client/src/schema-component/antd/list/List.Designer.tsx b/packages/core/client/src/schema-component/antd/list/List.Designer.tsx index 8042c7a1af9..6933bd1c89e 100644 --- a/packages/core/client/src/schema-component/antd/list/List.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/list/List.Designer.tsx @@ -170,11 +170,12 @@ export const ListDesigner = () => { title={t('Records per page')} value={field.decoratorProps?.params?.pageSize || 20} options={[ + { label: '5', value: 5 }, { label: '10', value: 10 }, { label: '20', value: 20 }, { label: '50', value: 50 }, - { label: '80', value: 80 }, { label: '100', value: 100 }, + { label: '200', value: 200 }, ]} onChange={(pageSize) => { _.set(fieldSchema, 'x-decorator-props.params.pageSize', pageSize); diff --git a/packages/core/client/src/schema-component/antd/list/List.tsx b/packages/core/client/src/schema-component/antd/list/List.tsx index 4d74afcc22d..53355e9d2c8 100644 --- a/packages/core/client/src/schema-component/antd/list/List.tsx +++ b/packages/core/client/src/schema-component/antd/list/List.tsx @@ -51,6 +51,8 @@ const InternalList = (props) => { [fieldSchema.properties, schemaMap], ); + const pageSizeOptions = [5, 10, 20, 50, 100, 200]; + const onPaginationChange: PaginationProps['onChange'] = useCallback( (page, pageSize) => { run({ @@ -97,6 +99,8 @@ const InternalList = (props) => { total: meta?.count || 0, pageSize: meta?.pageSize || 10, current: meta?.page || 1, + showSizeChanger: true, + pageSizeOptions, } } loading={service?.loading} diff --git a/packages/core/client/src/schema-component/antd/markdown/Markdown.Void.tsx b/packages/core/client/src/schema-component/antd/markdown/Markdown.Void.tsx index 9898279885b..93f4ee10091 100644 --- a/packages/core/client/src/schema-component/antd/markdown/Markdown.Void.tsx +++ b/packages/core/client/src/schema-component/antd/markdown/Markdown.Void.tsx @@ -95,7 +95,7 @@ const MarkdownEditor = (props: MarkdownEditorProps) => {
- +
+ ); + } + return ( ); }, - [dataSource.length, others.isSubTable], + [others.isSubTable], ); const components = useMemo(() => { @@ -619,6 +643,7 @@ export const Table: any = withDynamicSchemaProps( onChange(selectedRowKeys: any[], selectedRows: any[]) { field.data = field.data || {}; field.data.selectedRowKeys = selectedRowKeys; + field.data.selectedRowData = selectedRows; setSelectedRowKeys(selectedRowKeys); onRowSelectionChange?.(selectedRowKeys, selectedRows); }, @@ -710,13 +735,13 @@ export const Table: any = withDynamicSchemaProps( const scroll = useMemo(() => { return { x: 'max-content', - y: tableHeight, + y: dataSource.length > 0 ? tableHeight : undefined, }; - }, [tableHeight, maxContent]); + }, [tableHeight, maxContent, dataSource]); const rowClassName = useCallback( (record) => (selectedRow.includes(record[rowKey]) ? highlightRow : ''), - [selectedRow, highlightRow, rowKey], + [selectedRow, highlightRow, JSON.stringify(rowKey)], ); const onExpandValue = useCallback( @@ -750,6 +775,9 @@ export const Table: any = withDynamicSchemaProps( height: 100%; display: flex; flex-direction: column; + .ant-table-expanded-row-fixed { + min-height: ${tableHeight}px; + } .ant-table-body { min-height: ${tableHeight}px; } @@ -767,7 +795,8 @@ export const Table: any = withDynamicSchemaProps( record.id} dataSource={dataSource} tableLayout="auto" {...others} diff --git a/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx b/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx index 83a75537e88..9d04b52971c 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx @@ -282,6 +282,7 @@ export const TableBlockDesigner = () => { title={t('Records per page')} value={field.decoratorProps?.params?.pageSize || 20} options={[ + { label: '5', value: 5 }, { label: '10', value: 10 }, { label: '20', value: 20 }, { label: '50', value: 50 }, diff --git a/packages/core/client/src/schema-component/antd/table-v2/TableSelectorDesigner.tsx b/packages/core/client/src/schema-component/antd/table-v2/TableSelectorDesigner.tsx index cb8562f3542..951e3d92ed2 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/TableSelectorDesigner.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/TableSelectorDesigner.tsx @@ -237,6 +237,7 @@ export const TableSelectorDesigner = () => { title={t('Records per page')} value={field.decoratorProps?.params?.pageSize || 20} options={[ + { label: '5', value: 5 }, { label: '10', value: 10 }, { label: '20', value: 20 }, { label: '50', value: 50 }, diff --git a/packages/core/client/src/schema-component/antd/table/Table.Void.Designer.tsx b/packages/core/client/src/schema-component/antd/table/Table.Void.Designer.tsx index 22adb1b7418..45d32c35974 100644 --- a/packages/core/client/src/schema-component/antd/table/Table.Void.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/table/Table.Void.Designer.tsx @@ -194,6 +194,7 @@ export const TableVoidDesigner = () => { title={'每页显示'} value={field.decoratorProps.request.params?.pageSize || 20} options={[ + { label: '5', value: 5 }, { label: '10', value: 10 }, { label: '20', value: 20 }, { label: '50', value: 50 }, diff --git a/packages/core/client/src/schema-component/antd/table/Table.Void.tsx b/packages/core/client/src/schema-component/antd/table/Table.Void.tsx index b61d56f17e7..cd4fa12c4c7 100644 --- a/packages/core/client/src/schema-component/antd/table/Table.Void.tsx +++ b/packages/core/client/src/schema-component/antd/table/Table.Void.tsx @@ -26,6 +26,8 @@ type TableVoidProps = TableProps & { ) => Result & { state?: any; setState?: any }; }; +const pageSizeOptions = [5, 10, 20, 50, 100, 200]; + const usePaginationProps = (props: TableProps & { request?: any }, service): TablePaginationConfig | false => { if (props.pagination === false) { return false; @@ -41,6 +43,7 @@ const usePaginationProps = (props: TableProps & { request?: any }, service) pagination.pageSize = pageSize; } return { + pageSizeOptions, showSizeChanger: true, ...pagination, onChange(page, pageSize) { diff --git a/packages/core/client/src/schema-component/antd/unix-timestamp/UnixTimestamp.tsx b/packages/core/client/src/schema-component/antd/unix-timestamp/UnixTimestamp.tsx index 5d19a1e1183..d461338686d 100644 --- a/packages/core/client/src/schema-component/antd/unix-timestamp/UnixTimestamp.tsx +++ b/packages/core/client/src/schema-component/antd/unix-timestamp/UnixTimestamp.tsx @@ -8,57 +8,31 @@ */ import { connect, mapReadPretty } from '@formily/react'; -import React, { useMemo } from 'react'; +import React from 'react'; import { DatePicker } from '../date-picker'; -import dayjs from 'dayjs'; - -const toValue = (value: any, accuracy) => { - if (value) { - return timestampToDate(value, accuracy); - } - return null; -}; - -function timestampToDate(timestamp, accuracy = 'millisecond') { - if (accuracy === 'second') { - timestamp *= 1000; // 如果精确度是秒级,则将时间戳乘以1000转换为毫秒级 - } - return dayjs(timestamp); -} - -function getTimestamp(date, accuracy = 'millisecond') { - if (accuracy === 'second') { - return dayjs(date).unix(); - } else { - return dayjs(date).valueOf(); // 默认返回毫秒级时间戳 - } -} interface UnixTimestampProps { - value?: number; - accuracy?: 'millisecond' | 'second'; + value?: any; onChange?: (value: number) => void; } export const UnixTimestamp = connect( (props: UnixTimestampProps) => { - const { value, onChange, accuracy = 'second' } = props; - const v = useMemo(() => toValue(value, accuracy), [value, accuracy]); + const { value, onChange } = props; return ( { if (onChange) { - onChange(getTimestamp(v, accuracy)); + onChange(v); } }} /> ); }, mapReadPretty((props) => { - const { value, accuracy = 'second' } = props; - const v = useMemo(() => toValue(value, accuracy), [value, accuracy]); - return ; + const { value } = props; + return ; }), ); diff --git a/packages/core/client/src/schema-component/antd/unix-timestamp/__tests__/UnixTimestamp.test.tsx b/packages/core/client/src/schema-component/antd/unix-timestamp/__tests__/UnixTimestamp.test.tsx index ba82c731472..1e91db5d91a 100644 --- a/packages/core/client/src/schema-component/antd/unix-timestamp/__tests__/UnixTimestamp.test.tsx +++ b/packages/core/client/src/schema-component/antd/unix-timestamp/__tests__/UnixTimestamp.test.tsx @@ -7,17 +7,15 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { screen, renderAppOptions, userEvent, waitFor, renderReadPrettyApp } from '@nocobase/test/client'; import { UnixTimestamp } from '@nocobase/client'; +import { renderAppOptions, renderReadPrettyApp, screen } from '@nocobase/test/client'; describe('UnixTimestamp', () => { it('renders without errors', async () => { const { container } = await renderAppOptions({ - Component: UnixTimestamp, - props: { - accuracy: 'millisecond', - }, - value: 0, + Component: UnixTimestamp as any, + props: {}, + value: null, }); expect(container).toMatchInlineSnapshot(`
@@ -69,78 +67,10 @@ describe('UnixTimestamp', () => { `); }); - it('millisecond', async () => { - await renderAppOptions({ - Component: UnixTimestamp, - value: 1712819630000, - props: { - accuracy: 'millisecond', - }, - }); - await waitFor(() => { - expect(screen.getByRole('textbox')).toHaveValue('2024-04-11'); - }); - }); - - it('second', async () => { - await renderAppOptions({ - Component: UnixTimestamp, - value: 1712819630, - props: { - accuracy: 'second', - }, - }); - - await waitFor(() => { - expect(screen.getByRole('textbox')).toHaveValue('2024-04-11'); - }); - }); - - it('string', async () => { - await renderAppOptions({ - Component: UnixTimestamp, - value: '2024-04-11', - props: { - accuracy: 'millisecond', - }, - }); - - await waitFor(() => { - expect(screen.getByRole('textbox')).toHaveValue('2024-04-11'); - }); - }); - - it('change', async () => { - const onChange = vitest.fn(); - await renderAppOptions({ - Component: UnixTimestamp, - value: '2024-04-11', - onChange, - props: { - accuracy: 'millisecond', - }, - }); - await userEvent.click(screen.getByRole('textbox')); - - await waitFor(() => { - expect(screen.queryByRole('table')).toBeInTheDocument(); - }); - - await userEvent.click(document.querySelector('td[title="2024-04-12"]')); - - await waitFor(() => { - expect(screen.getByRole('textbox')).toHaveValue('2024-04-12'); - }); - expect(onChange).toBeCalledWith(1712880000000); - }); - it('read pretty', async () => { const { container } = await renderReadPrettyApp({ - Component: UnixTimestamp, + Component: UnixTimestamp as any, value: '2024-04-11', - props: { - accuracy: 'millisecond', - }, }); expect(screen.getByText('2024-04-11')).toBeInTheDocument(); diff --git a/packages/core/client/src/schema-component/antd/upload/Upload.tsx b/packages/core/client/src/schema-component/antd/upload/Upload.tsx index c6ab7ad48fb..b6efb38369a 100644 --- a/packages/core/client/src/schema-component/antd/upload/Upload.tsx +++ b/packages/core/client/src/schema-component/antd/upload/Upload.tsx @@ -10,7 +10,7 @@ import { DeleteOutlined, DownloadOutlined, InboxOutlined, LoadingOutlined, PlusOutlined } from '@ant-design/icons'; import { Field } from '@formily/core'; import { connect, mapProps, mapReadPretty, useField } from '@formily/react'; -import { Upload as AntdUpload, Button, Modal, Progress, Space, Tooltip } from 'antd'; +import { Alert, Upload as AntdUpload, Button, Modal, Progress, Space, Tooltip } from 'antd'; import useUploadStyle from 'antd/es/upload/style'; import cls from 'classnames'; import { saveAs } from 'file-saver'; @@ -23,17 +23,141 @@ import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; import { useProps } from '../../hooks/useProps'; import { FILE_SIZE_LIMIT_DEFAULT, - isImage, - isPdf, + attachmentFileTypes, + getThumbnailPlaceholderURL, + matchMimetype, normalizeFile, toFileList, - toValueItem, + toValueItem as toValueItemDefault, useBeforeUpload, useUploadProps, } from './shared'; import { useStyles } from './style'; import type { ComposedUpload, DraggerProps, DraggerV2Props, UploadProps } from './type'; +attachmentFileTypes.add({ + match(file) { + return matchMimetype(file, 'image/*'); + }, + getThumbnailURL(file) { + return file.url ? `${file.url}${file.thumbnailRule || ''}` : URL.createObjectURL(file.originFileObj); + }, + Previewer({ index, list, onSwitchIndex }) { + const onDownload = useCallback( + (e) => { + e.preventDefault(); + const file = list[index]; + saveAs(file.url, `${file.title}${file.extname}`); + }, + [index, list], + ); + return ( + onSwitchIndex(null)} + onMovePrevRequest={() => onSwitchIndex((index + list.length - 1) % list.length)} + onMoveNextRequest={() => onSwitchIndex((index + 1) % list.length)} + imageTitle={list[index]?.title} + toolbarButtons={[ + , + ]} + /> + ); + }, +}); + +const iframePreviewSupportedTypes = ['application/pdf', 'audio/*', 'image/*', 'video/*']; + +function IframePreviewer({ index, list, onSwitchIndex }) { + const { t } = useTranslation(); + const file = list[index]; + const onOpen = useCallback( + (e) => { + e.preventDefault(); + e.stopPropagation(); + window.open(file.url); + }, + [file], + ); + const onDownload = useCallback( + (e) => { + e.preventDefault(); + e.stopPropagation(); + saveAs(file.url, `${file.title}${file.extname}`); + }, + [file], + ); + const onClose = useCallback(() => { + onSwitchIndex(null); + }, [onSwitchIndex]); + return ( + + {t('Open in new window')} + , + , + , + ]} + width={'85vw'} + centered={true} + > +
+ {iframePreviewSupportedTypes.some((type) => matchMimetype(file, type)) ? ( +
+ {props.children} + {/* 子表格中不能使用懒渲染。详见:https://nocobase.height.app/T-4889/description */} @@ -591,7 +615,7 @@ export const Table: any = withDynamicSchemaProps(