diff --git a/.github/workflows/build-internal-image.yml b/.github/workflows/build-internal-image.yml
new file mode 100644
index 0000000000..0601be438d
--- /dev/null
+++ b/.github/workflows/build-internal-image.yml
@@ -0,0 +1,119 @@
+name: Build Image (Internal)
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+on:
+ workflow_dispatch:
+ inputs:
+ ref_name:
+ description: 'Branch or tag name to release'
+
+jobs:
+ get-plugins:
+ uses: nocobase/nocobase/.github/workflows/get-plugins.yml@main
+ secrets: inherit
+ push-docker:
+ runs-on: ubuntu-latest
+ needs: get-plugins
+ services:
+ verdaccio:
+ image: verdaccio/verdaccio:5
+ ports:
+ - 4873:4873
+ steps:
+ - name: Set Node.js 20
+ uses: actions/setup-node@v3
+ with:
+ node-version: 20
+ - name: Get info
+ id: get-info
+ shell: bash
+ run: |
+ if [[ "${{ inputs.ref_name || github.ref_name }}" =~ "beta" ]]; then
+ echo "defaultTag=$(echo 'beta')" >> $GITHUB_OUTPUT
+ echo "proRepos=$(echo '${{ needs.get-plugins.outputs.beta-plugins }}')" >> $GITHUB_OUTPUT
+ elif [[ "${{ inputs.ref_name || github.ref_name }}" =~ "alpha" ]]; then
+ echo "defaultTag=$(echo 'alpha')" >> $GITHUB_OUTPUT
+ echo "proRepos=$(echo '${{ needs.get-plugins.outputs.alpha-plugins }}')" >> $GITHUB_OUTPUT
+ else
+ # rc
+ echo "defaultTag=$(echo 'latest')" >> $GITHUB_OUTPUT
+ echo "proRepos=$(echo '${{ needs.get-plugins.outputs.rc-plugins }}')" >> $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-info.outputs.proRepos), ',') }},${{ join(fromJSON(needs.get-plugins.outputs.custom-plugins), ',') }}
+ skip-token-revoke: true
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ ref: ${{ inputs.ref_name || github.ref_name }}
+ - name: yarn install
+ run: |
+ yarn install
+ - name: Checkout pro-plugins
+ uses: actions/checkout@v3
+ with:
+ repository: nocobase/pro-plugins
+ path: packages/pro-plugins
+ ref: ${{ inputs.ref_name || github.ref_name }}
+ token: ${{ steps.app-token.outputs.token }}
+ - name: Clone pro repos
+ shell: bash
+ run: |
+ for repo in ${{ join(fromJSON(steps.get-info.outputs.proRepos), ' ') }} ${{ join(fromJSON(needs.get-plugins.outputs.custom-plugins), ' ') }}
+ do
+ git clone -b ${{ inputs.ref_name || github.ref_name }} https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
+ done
+ - 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: Docker meta
+ id: meta
+ uses: docker/metadata-action@v4
+ with:
+ images: |
+ nocobase/nocobase
+ tags: |
+ type=ref,event=branch
+ type=ref,event=pr
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+ - 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: 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 @nocobase/plugin-notifications @nocobase/plugin-disable-pm-add $packageNames -W --production"
+ APPEND_PRESET_LOCAL_PLUGINS="notifications,disable-pm-add,${pluginNames// /,}"
+ echo "var1=$BEFORE_PACK_NOCOBASE" >> $GITHUB_OUTPUT
+ echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT
+ - 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: ${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/nocobase/nocobase:${{ steps.get-info.outputs.defaultTag }},${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/${{ steps.meta.outputs.tags }}
diff --git a/.github/workflows/manual-merge.yml b/.github/workflows/manual-merge.yml
index 061765def4..70bcbeaa5d 100644
--- a/.github/workflows/manual-merge.yml
+++ b/.github/workflows/manual-merge.yml
@@ -65,6 +65,10 @@ jobs:
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: 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: Checkout
uses: actions/checkout@v4
with:
diff --git a/.github/workflows/manual-npm-publish-license-kit.yml b/.github/workflows/manual-npm-publish-license-kit.yml
new file mode 100644
index 0000000000..d4703a82d5
--- /dev/null
+++ b/.github/workflows/manual-npm-publish-license-kit.yml
@@ -0,0 +1,619 @@
+name: Manual npm publish license-kit
+env:
+ DEBUG: napi:*
+ APP_NAME: license-kit
+ MACOSX_DEPLOYMENT_TARGET: '10.13'
+permissions:
+ contents: write
+ id-token: write
+on:
+ workflow_dispatch:
+
+# 'on':
+# push:
+# branches:
+# - main
+# tags-ignore:
+# - '**'
+# paths-ignore:
+# - '**/*.md'
+# - LICENSE
+# - '**/*.gitignore'
+# - .editorconfig
+# - docs/**
+# pull_request: null
+jobs:
+ build:
+ strategy:
+ fail-fast: false
+ matrix:
+ settings:
+ - host: macos-latest
+ target: x86_64-apple-darwin
+ build: yarn build --target x86_64-apple-darwin
+ - host: windows-latest
+ build: yarn build --target x86_64-pc-windows-msvc
+ target: x86_64-pc-windows-msvc
+ - host: windows-latest
+ build: yarn build --target i686-pc-windows-msvc
+ target: i686-pc-windows-msvc
+ - host: ubuntu-latest
+ target: x86_64-unknown-linux-gnu
+ docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
+ build: yarn build --target x86_64-unknown-linux-gnu
+ - host: ubuntu-latest
+ target: x86_64-unknown-linux-musl
+ docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
+ build: yarn build --target x86_64-unknown-linux-musl
+ - host: macos-latest
+ target: aarch64-apple-darwin
+ build: yarn build --target aarch64-apple-darwin
+ - host: ubuntu-latest
+ target: aarch64-unknown-linux-gnu
+ docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
+ build: yarn build --target aarch64-unknown-linux-gnu
+ - host: ubuntu-latest
+ target: armv7-unknown-linux-gnueabihf
+ setup: |
+ sudo apt-get update
+ sudo apt-get install gcc-arm-linux-gnueabihf -y
+ build: yarn build --target armv7-unknown-linux-gnueabihf
+ - host: ubuntu-latest
+ target: armv7-unknown-linux-musleabihf
+ build: yarn build --target armv7-unknown-linux-musleabihf
+ - host: ubuntu-latest
+ target: aarch64-linux-android
+ build: yarn build --target aarch64-linux-android
+ - host: ubuntu-latest
+ target: armv7-linux-androideabi
+ build: yarn build --target armv7-linux-androideabi
+ - host: ubuntu-latest
+ target: aarch64-unknown-linux-musl
+ docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
+ build: |-
+ set -e &&
+ rustup target add aarch64-unknown-linux-musl &&
+ yarn build --target aarch64-unknown-linux-musl
+ - host: windows-latest
+ target: aarch64-pc-windows-msvc
+ build: yarn build --target aarch64-pc-windows-msvc
+ - host: ubuntu-latest
+ target: riscv64gc-unknown-linux-gnu
+ setup: |
+ sudo apt-get update
+ sudo apt-get install gcc-riscv64-linux-gnu -y
+ build: yarn build --target riscv64gc-unknown-linux-gnu
+ name: stable - ${{ matrix.settings.target }} - node@20
+ runs-on: ${{ matrix.settings.host }}
+ 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: license-kit
+ skip-token-revoke: true
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ repository: nocobase/license-kit
+ token: ${{ steps.app-token.outputs.token }}
+ persist-credentials: true
+ ref: main
+ - name: Setup node
+ uses: actions/setup-node@v4
+ if: ${{ !matrix.settings.docker }}
+ with:
+ node-version: 20
+ cache: yarn
+ - name: Install
+ uses: dtolnay/rust-toolchain@stable
+ if: ${{ !matrix.settings.docker }}
+ with:
+ toolchain: stable
+ targets: ${{ matrix.settings.target }}
+ - name: Cache cargo
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cargo/registry/index/
+ ~/.cargo/registry/cache/
+ ~/.cargo/git/db/
+ .cargo-cache
+ target/
+ key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }}
+ - uses: goto-bus-stop/setup-zig@v2
+ if: ${{ matrix.settings.target == 'armv7-unknown-linux-gnueabihf' || matrix.settings.target == 'armv7-unknown-linux-musleabihf' }}
+ with:
+ version: 0.13.0
+ - name: Setup toolchain
+ run: ${{ matrix.settings.setup }}
+ if: ${{ matrix.settings.setup }}
+ shell: bash
+ - name: Setup node x86
+ if: matrix.settings.target == 'i686-pc-windows-msvc'
+ run: yarn config set supportedArchitectures.cpu "ia32"
+ shell: bash
+ - name: Install dependencies
+ run: yarn install
+ - name: Setup node x86
+ uses: actions/setup-node@v4
+ if: matrix.settings.target == 'i686-pc-windows-msvc'
+ with:
+ node-version: 20
+ cache: yarn
+ architecture: x86
+ - name: Install OpenSSL dev
+ if: ${{ matrix.settings.host == 'ubuntu-latest' }}
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y pkg-config libssl-dev
+ - name: Build in docker
+ uses: addnab/docker-run-action@v3
+ if: ${{ matrix.settings.docker }}
+ with:
+ image: ${{ matrix.settings.docker }}
+ options: '--user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build'
+ run: ${{ matrix.settings.build }}
+ - name: Build
+ run: ${{ matrix.settings.build }}
+ if: ${{ !matrix.settings.docker }}
+ shell: bash
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: bindings-${{ matrix.settings.target }}
+ path: ${{ env.APP_NAME }}.*.node
+ if-no-files-found: error
+ build-freebsd:
+ runs-on: ubuntu-latest
+ name: Build FreeBSD
+ 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: license-kit
+ skip-token-revoke: true
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ repository: nocobase/license-kit
+ token: ${{ steps.app-token.outputs.token }}
+ persist-credentials: true
+ ref: main
+ - name: Build
+ id: build
+ uses: cross-platform-actions/action@v0.27.0
+ env:
+ DEBUG: napi:*
+ RUSTUP_IO_THREADS: 1
+ with:
+ operating_system: freebsd
+ version: '14.2'
+ memory: 8G
+ cpu_count: 3
+ environment_variables: 'DEBUG RUSTUP_IO_THREADS'
+ shell: bash
+ run: |
+ sudo pkg install -y -f curl node libnghttp2 npm openssl
+ sudo npm install -g yarn --ignore-scripts
+ curl https://sh.rustup.rs -sSf --output rustup.sh
+ sh rustup.sh -y --profile minimal --default-toolchain beta
+ source "$HOME/.cargo/env"
+ echo "~~~~ rustc --version ~~~~"
+ rustc --version
+ echo "~~~~ node -v ~~~~"
+ node -v
+ echo "~~~~ yarn --version ~~~~"
+ yarn --version
+ pwd
+ ls -lah
+ whoami
+ env
+ freebsd-version
+ yarn install
+ yarn build
+ rm -rf node_modules
+ rm -rf target
+ rm -rf .yarn/cache
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: bindings-freebsd
+ path: ${{ env.APP_NAME }}.*.node
+ if-no-files-found: error
+ test-macOS-windows-binding:
+ name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }}
+ needs:
+ - build
+ strategy:
+ fail-fast: false
+ matrix:
+ settings:
+ - host: macos-latest
+ target: x86_64-apple-darwin
+ - host: windows-latest
+ target: x86_64-pc-windows-msvc
+ node:
+ - '18'
+ - '20'
+ runs-on: ${{ matrix.settings.host }}
+ 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: license-kit
+ skip-token-revoke: true
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ repository: nocobase/license-kit
+ token: ${{ steps.app-token.outputs.token }}
+ persist-credentials: true
+ ref: main
+ - name: Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node }}
+ cache: yarn
+ architecture: x64
+ - name: Install dependencies
+ run: yarn install
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: bindings-${{ matrix.settings.target }}
+ path: .
+ - name: List packages
+ run: ls -R .
+ shell: bash
+ - name: Test bindings
+ run: yarn test
+ test-linux-x64-gnu-binding:
+ name: Test bindings on Linux-x64-gnu - node@${{ matrix.node }}
+ needs:
+ - build
+ strategy:
+ fail-fast: false
+ matrix:
+ node:
+ - '18'
+ - '20'
+ 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: license-kit
+ skip-token-revoke: true
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ repository: nocobase/license-kit
+ token: ${{ steps.app-token.outputs.token }}
+ persist-credentials: true
+ ref: main
+ - name: Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node }}
+ cache: yarn
+ - name: Install dependencies
+ run: yarn install
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: bindings-x86_64-unknown-linux-gnu
+ path: .
+ - name: List packages
+ run: ls -R .
+ shell: bash
+ - name: Test bindings
+ run: docker run --rm -v $(pwd):/build -w /build node:${{ matrix.node }}-slim yarn test
+ test-linux-x64-musl-binding:
+ name: Test bindings on x86_64-unknown-linux-musl - node@${{ matrix.node }}
+ needs:
+ - build
+ strategy:
+ fail-fast: false
+ matrix:
+ node:
+ - '18'
+ - '20'
+ 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: license-kit
+ skip-token-revoke: true
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ repository: nocobase/license-kit
+ token: ${{ steps.app-token.outputs.token }}
+ persist-credentials: true
+ ref: main
+ - name: Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node }}
+ cache: yarn
+ - name: Install dependencies
+ run: |
+ yarn config set supportedArchitectures.libc "musl"
+ yarn install
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: bindings-x86_64-unknown-linux-musl
+ path: .
+ - name: List packages
+ run: ls -R .
+ shell: bash
+ - name: Test bindings
+ run: docker run --rm -v $(pwd):/build -w /build node:${{ matrix.node }}-alpine yarn test
+ test-linux-aarch64-gnu-binding:
+ name: Test bindings on aarch64-unknown-linux-gnu - node@${{ matrix.node }}
+ needs:
+ - build
+ strategy:
+ fail-fast: false
+ matrix:
+ node:
+ - '18'
+ - '20'
+ 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: license-kit
+ skip-token-revoke: true
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ repository: nocobase/license-kit
+ token: ${{ steps.app-token.outputs.token }}
+ persist-credentials: true
+ ref: main
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: bindings-aarch64-unknown-linux-gnu
+ path: .
+ - name: List packages
+ run: ls -R .
+ shell: bash
+ - name: Install dependencies
+ run: |
+ yarn config set supportedArchitectures.cpu "arm64"
+ yarn config set supportedArchitectures.libc "glibc"
+ yarn install
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+ with:
+ platforms: arm64
+ - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
+ - name: Setup and run tests
+ uses: addnab/docker-run-action@v3
+ with:
+ image: node:${{ matrix.node }}-slim
+ options: '--platform linux/arm64 -v ${{ github.workspace }}:/build -w /build'
+ run: |
+ set -e
+ yarn test
+ ls -la
+ test-linux-aarch64-musl-binding:
+ name: Test bindings on aarch64-unknown-linux-musl - node@${{ matrix.node }}
+ needs:
+ - build
+ 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: license-kit
+ skip-token-revoke: true
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ repository: nocobase/license-kit
+ token: ${{ steps.app-token.outputs.token }}
+ persist-credentials: true
+ ref: main
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: bindings-aarch64-unknown-linux-musl
+ path: .
+ - name: List packages
+ run: ls -R .
+ shell: bash
+ - name: Install dependencies
+ run: |
+ yarn config set supportedArchitectures.cpu "arm64"
+ yarn config set supportedArchitectures.libc "musl"
+ yarn install
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+ with:
+ platforms: arm64
+ - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
+ - name: Setup and run tests
+ uses: addnab/docker-run-action@v3
+ with:
+ image: node:lts-alpine
+ options: '--platform linux/arm64 -v ${{ github.workspace }}:/build -w /build'
+ run: |
+ set -e
+ yarn test
+ test-linux-arm-gnueabihf-binding:
+ name: Test bindings on armv7-unknown-linux-gnueabihf - node@${{ matrix.node }}
+ needs:
+ - build
+ strategy:
+ fail-fast: false
+ matrix:
+ node:
+ - '18'
+ - '20'
+ 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: license-kit
+ skip-token-revoke: true
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ repository: nocobase/license-kit
+ token: ${{ steps.app-token.outputs.token }}
+ persist-credentials: true
+ ref: main
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: bindings-armv7-unknown-linux-gnueabihf
+ path: .
+ - name: List packages
+ run: ls -R .
+ shell: bash
+ - name: Install dependencies
+ run: |
+ yarn config set supportedArchitectures.cpu "arm"
+ yarn install
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+ with:
+ platforms: arm
+ - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
+ - name: Setup and run tests
+ uses: addnab/docker-run-action@v3
+ with:
+ image: node:${{ matrix.node }}-bullseye-slim
+ options: '--platform linux/arm/v7 -v ${{ github.workspace }}:/build -w /build'
+ run: |
+ set -e
+ yarn test
+ ls -la
+ universal-macOS:
+ name: Build universal macOS binary
+ needs:
+ - build
+ runs-on: macos-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: license-kit
+ skip-token-revoke: true
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ repository: nocobase/license-kit
+ token: ${{ steps.app-token.outputs.token }}
+ persist-credentials: true
+ ref: main
+ - name: Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: yarn
+ - name: Install dependencies
+ run: yarn install
+ - name: Download macOS x64 artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: bindings-x86_64-apple-darwin
+ path: artifacts
+ - name: Download macOS arm64 artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: bindings-aarch64-apple-darwin
+ path: artifacts
+ - name: Combine binaries
+ run: yarn universal
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: bindings-universal-apple-darwin
+ path: ${{ env.APP_NAME }}.*.node
+ if-no-files-found: error
+ publish:
+ name: Publish
+ runs-on: ubuntu-latest
+ needs:
+ - build-freebsd
+ - test-macOS-windows-binding
+ - test-linux-x64-gnu-binding
+ - test-linux-x64-musl-binding
+ - test-linux-aarch64-gnu-binding
+ - test-linux-aarch64-musl-binding
+ - test-linux-arm-gnueabihf-binding
+ - universal-macOS
+ 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: license-kit
+ skip-token-revoke: true
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ repository: nocobase/license-kit
+ token: ${{ steps.app-token.outputs.token }}
+ persist-credentials: true
+ ref: main
+ - name: Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: yarn
+ - name: Install dependencies
+ run: yarn install
+ - name: Download all artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: artifacts
+ - name: Move artifacts
+ run: yarn artifacts
+ - name: List packages
+ run: ls -R ./npm
+ shell: bash
+ - name: Publish
+ run: |
+ npm config set provenance true
+ if git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+$";
+ then
+ echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
+ npm publish --access public
+ elif git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+";
+ then
+ echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
+ npm publish --tag next --access public
+ else
+ echo "Not a release, skipping publish"
+ fi
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.github/workflows/nocobase-test-backend.yml b/.github/workflows/nocobase-test-backend.yml
index f7956dddd7..c925ff3fb9 100644
--- a/.github/workflows/nocobase-test-backend.yml
+++ b/.github/workflows/nocobase-test-backend.yml
@@ -5,38 +5,40 @@ concurrency:
cancel-in-progress: true
on:
- push:
- branches:
- - main
- - next
- - develop
- paths:
- - 'package.json'
- - '**/yarn.lock'
- - 'packages/core/acl/**'
- - 'packages/core/auth/**'
- - 'packages/core/actions/**'
- - 'packages/core/database/**'
- - 'packages/core/resourcer/**'
- - 'packages/core/data-source-manager/**'
- - 'packages/core/server/**'
- - 'packages/core/utils/**'
- - 'packages/plugins/**/src/server/**'
- - '.github/workflows/nocobase-test-backend.yml'
- pull_request:
- paths:
- - 'package.json'
- - '**/yarn.lock'
- - 'packages/core/acl/**'
- - 'packages/core/auth/**'
- - 'packages/core/actions/**'
- - 'packages/core/database/**'
- - 'packages/core/resourcer/**'
- - 'packages/core/data-source-manager/**'
- - 'packages/core/server/**'
- - 'packages/core/utils/**'
- - 'packages/plugins/**/src/server/**'
- - '.github/workflows/nocobase-test-backend.yml'
+ workflow_dispatch:
+
+ # push:
+ # branches:
+ # - main
+ # - next
+ # - develop
+ # paths:
+ # - 'package.json'
+ # - '**/yarn.lock'
+ # - 'packages/core/acl/**'
+ # - 'packages/core/auth/**'
+ # - 'packages/core/actions/**'
+ # - 'packages/core/database/**'
+ # - 'packages/core/resourcer/**'
+ # - 'packages/core/data-source-manager/**'
+ # - 'packages/core/server/**'
+ # - 'packages/core/utils/**'
+ # - 'packages/plugins/**/src/server/**'
+ # - '.github/workflows/nocobase-test-backend.yml'
+ # pull_request:
+ # paths:
+ # - 'package.json'
+ # - '**/yarn.lock'
+ # - 'packages/core/acl/**'
+ # - 'packages/core/auth/**'
+ # - 'packages/core/actions/**'
+ # - 'packages/core/database/**'
+ # - 'packages/core/resourcer/**'
+ # - 'packages/core/data-source-manager/**'
+ # - 'packages/core/server/**'
+ # - 'packages/core/utils/**'
+ # - 'packages/plugins/**/src/server/**'
+ # - '.github/workflows/nocobase-test-backend.yml'
jobs:
sqlite-test:
@@ -59,7 +61,9 @@ jobs:
node-version: ${{ matrix.node_version }}
cache: 'yarn'
- name: Install project dependencies
- run: yarn install
+ run: |
+ yarn install
+ yarn add sqlite3 --no-save -W
- name: Test with Sqlite
run: yarn test --server --single-thread=false
env:
diff --git a/.github/workflows/nocobase-test-windows.yml b/.github/workflows/nocobase-test-windows.yml
index c1d378a6ba..cd20394a83 100644
--- a/.github/workflows/nocobase-test-windows.yml
+++ b/.github/workflows/nocobase-test-windows.yml
@@ -63,7 +63,9 @@ jobs:
${{ runner.os }}-yarn-
- name: Install project dependencies
- run: yarn --prefer-offline
+ run: |
+ yarn --prefer-offline
+ yarn add sqlite3 --no-save -W
- name: Test with Sqlite
run: yarn test --server --single-thread=false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f886b15050..d7ab9406f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,834 @@ 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).
+## [v1.6.25](https://github.com/nocobase/nocobase/compare/v1.6.24...v1.6.25) - 2025-04-29
+
+### 🎉 New Features
+
+- **[undefined]** add publish ci for license kit ([#6786](https://github.com/nocobase/nocobase/pull/6786)) by @jiannx
+
+- **[Data visualization: ECharts]** Add "Y-Axis inverse" setting for bar charts by @2013xile
+
+### 🚀 Improvements
+
+- **[utils]** Increase the height of the filter button field list and sort/categorize the fields ([#6779](https://github.com/nocobase/nocobase/pull/6779)) by @zhangzhonghe
+
+- **[client]** optimize subtable add button style and align paginator on the same row ([#6790](https://github.com/nocobase/nocobase/pull/6790)) by @katherinehhh
+
+- **[File manager]** Add OSS timeout option default to 10min ([#6795](https://github.com/nocobase/nocobase/pull/6795)) by @mytharcher
+
+- **[Password policy]** Change default password expiration to never expire by @2013xile
+
+- **[WeCom]** Prioritize corporate email over personal email when updating the user's email by @2013xile
+
+### 🐛 Bug Fixes
+
+- **[client]**
+ - In the collapse block, clicking the clear button in the relationship field search box should not delete the data range ([#6782](https://github.com/nocobase/nocobase/pull/6782)) by @zhangzhonghe
+
+ - association field not submitting data when displaying field from related collection ([#6798](https://github.com/nocobase/nocobase/pull/6798)) by @katherinehhh
+
+ - Prohibit moving menus before or after page tabs ([#6777](https://github.com/nocobase/nocobase/pull/6777)) by @zhangzhonghe
+
+ - Table block displays duplicate data when filtering ([#6792](https://github.com/nocobase/nocobase/pull/6792)) by @zhangzhonghe
+
+ - In the filter form, switching the field operator and then refreshing the page causes an error ([#6781](https://github.com/nocobase/nocobase/pull/6781)) by @zhangzhonghe
+
+- **[database]**
+ - Avoid error thrown when data type is not string ([#6797](https://github.com/nocobase/nocobase/pull/6797)) by @mytharcher
+
+ - add unavailableActions to sql collection and view collection ([#6765](https://github.com/nocobase/nocobase/pull/6765)) by @katherinehhh
+
+- **[create-nocobase-app]** Temporarily fix mariadb issue by downgrading to 2.5.6 ([#6762](https://github.com/nocobase/nocobase/pull/6762)) by @chenos
+
+- **[Authentication]** Disallow changing authenticator name ([#6808](https://github.com/nocobase/nocobase/pull/6808)) by @2013xile
+
+- **[Template print]** Fix: Correct permission validation logic to prevent unauthorized actions. by @sheldon66
+
+- **[File storage: S3(Pro)]** access url expiration invalid by @jiannx
+
+- **[Block: Tree]** After connecting through a foreign key, clicking to trigger filtering results in empty filter conditions by @zhangzhonghe
+
+## [v1.6.24](https://github.com/nocobase/nocobase/compare/v1.6.23...v1.6.24) - 2025-04-24
+
+### 🚀 Improvements
+
+- **[client]** Adjust upload message ([#6757](https://github.com/nocobase/nocobase/pull/6757)) by @mytharcher
+
+### 🐛 Bug Fixes
+
+- **[client]**
+ - only export action in view collection is support when writableView is false ([#6763](https://github.com/nocobase/nocobase/pull/6763)) by @katherinehhh
+
+ - unexpected association data creation when displaying association field under sub-form/sub-table in create form ([#6727](https://github.com/nocobase/nocobase/pull/6727)) by @katherinehhh
+
+ - Incorrect data retrieved for many-to-many array fields from related tables in forms ([#6744](https://github.com/nocobase/nocobase/pull/6744)) by @2013xile
+
+## [v1.6.23](https://github.com/nocobase/nocobase/compare/v1.6.22...v1.6.23) - 2025-04-23
+
+### 🚀 Improvements
+
+- **[cli]** Optimize internal logic of the `nocobase upgrade` command ([#6754](https://github.com/nocobase/nocobase/pull/6754)) by @chenos
+
+- **[Template print]** Replaced datasource action control with client role-based access control. by @sheldon66
+
+### 🐛 Bug Fixes
+
+- **[cli]** Auto-update package.json on upgrade ([#6747](https://github.com/nocobase/nocobase/pull/6747)) by @chenos
+
+- **[client]**
+ - missing filter for already associated data when adding association data ([#6750](https://github.com/nocobase/nocobase/pull/6750)) by @katherinehhh
+
+ - tree table 'Add Child' button linkage rule missing 'current record' ([#6752](https://github.com/nocobase/nocobase/pull/6752)) by @katherinehhh
+
+- **[Action: Import records]** Fix the import and export exceptions that occur when setting field permissions. ([#6677](https://github.com/nocobase/nocobase/pull/6677)) by @aaaaaajie
+
+- **[Block: Gantt]** gantt chart block overlapping months in calendar header for month view ([#6753](https://github.com/nocobase/nocobase/pull/6753)) by @katherinehhh
+
+- **[Action: Export records Pro]**
+ - pro export button losing filter parameters after sorting table column by @katherinehhh
+
+ - Fix the import and export exceptions that occur when setting field permissions. by @aaaaaajie
+
+- **[File storage: S3(Pro)]** Fix response data of uploaded file by @mytharcher
+
+- **[Workflow: Approval]** Fix preload association fields for records by @mytharcher
+
+## [v1.6.22](https://github.com/nocobase/nocobase/compare/v1.6.21...v1.6.22) - 2025-04-22
+
+### 🚀 Improvements
+
+- **[create-nocobase-app]** Upgrade dependencies and remove SQLite support ([#6708](https://github.com/nocobase/nocobase/pull/6708)) by @chenos
+
+- **[File manager]** Expose utils API ([#6705](https://github.com/nocobase/nocobase/pull/6705)) by @mytharcher
+
+- **[Workflow]** Add date types to variable types set ([#6717](https://github.com/nocobase/nocobase/pull/6717)) by @mytharcher
+
+### 🐛 Bug Fixes
+
+- **[client]**
+ - The problem of mobile top navigation bar icons being difficult to delete ([#6734](https://github.com/nocobase/nocobase/pull/6734)) by @zhangzhonghe
+
+ - After connecting through a foreign key, clicking to trigger filtering results in empty filter conditions ([#6634](https://github.com/nocobase/nocobase/pull/6634)) by @zhangzhonghe
+
+ - picker switching issue in date field of filter button ([#6695](https://github.com/nocobase/nocobase/pull/6695)) by @katherinehhh
+
+ - The issue of the collapse button in the left menu being obscured by the workflow pop-up window ([#6733](https://github.com/nocobase/nocobase/pull/6733)) by @zhangzhonghe
+
+ - missing action option constraints when reopening linkage rules ([#6723](https://github.com/nocobase/nocobase/pull/6723)) by @katherinehhh
+
+ - export button shown without export permission ([#6689](https://github.com/nocobase/nocobase/pull/6689)) by @katherinehhh
+
+ - Required fields hidden by linkage rules should not affect form submission ([#6709](https://github.com/nocobase/nocobase/pull/6709)) by @zhangzhonghe
+
+- **[server]** appVersion incorrectly generated by create-migration ([#6740](https://github.com/nocobase/nocobase/pull/6740)) by @chenos
+
+- **[build]** Fix error thrown in tar command ([#6722](https://github.com/nocobase/nocobase/pull/6722)) by @mytharcher
+
+- **[Workflow]** Fix error thrown when execute schedule event in subflow ([#6721](https://github.com/nocobase/nocobase/pull/6721)) by @mytharcher
+
+- **[Workflow: Custom action event]** Support to execute in multiple records mode by @mytharcher
+
+- **[File storage: S3(Pro)]** Add multer make logic for server-side upload by @mytharcher
+
+## [v1.6.21](https://github.com/nocobase/nocobase/compare/v1.6.20...v1.6.21) - 2025-04-17
+
+### 🚀 Improvements
+
+- **[client]** Add delay API for scenarios which open without delay ([#6681](https://github.com/nocobase/nocobase/pull/6681)) by @mytharcher
+
+- **[create-nocobase-app]** Upgrade some dependencies to latest versions ([#6673](https://github.com/nocobase/nocobase/pull/6673)) by @chenos
+
+### 🐛 Bug Fixes
+
+- **[client]**
+ - Fix error thrown when mouse hover on referenced template block in approval node configuration ([#6691](https://github.com/nocobase/nocobase/pull/6691)) by @mytharcher
+
+ - custom association field not displaying field component settings ([#6692](https://github.com/nocobase/nocobase/pull/6692)) by @katherinehhh
+
+ - Fix locale for upload component ([#6682](https://github.com/nocobase/nocobase/pull/6682)) by @mytharcher
+
+ - lazy load missing ui component will cause render error ([#6683](https://github.com/nocobase/nocobase/pull/6683)) by @gchust
+
+ - Add native Password component to HoC Input ([#6679](https://github.com/nocobase/nocobase/pull/6679)) by @mytharcher
+
+ - inherited fields shown in current collection field assignment list ([#6666](https://github.com/nocobase/nocobase/pull/6666)) by @katherinehhh
+
+- **[database]** Fixed ci build error ([#6687](https://github.com/nocobase/nocobase/pull/6687)) by @aaaaaajie
+
+- **[build]** build output is incorrect when plugin depends on some AMD libraries ([#6665](https://github.com/nocobase/nocobase/pull/6665)) by @gchust
+
+- **[Action: Import records]** fixed an error importing xlsx time field ([#6672](https://github.com/nocobase/nocobase/pull/6672)) by @aaaaaajie
+
+- **[Workflow: Manual node]** Fix manual task status constant ([#6676](https://github.com/nocobase/nocobase/pull/6676)) by @mytharcher
+
+- **[Block: iframe]** vertical scrollbar appears when iframe block is set to full height ([#6675](https://github.com/nocobase/nocobase/pull/6675)) by @katherinehhh
+
+- **[Workflow: Custom action event]** Fix test cases by @mytharcher
+
+- **[Backup manager]** timeout error occurs when trying to restore an unecrypted backup with a password by @gchust
+
+## [v1.6.20](https://github.com/nocobase/nocobase/compare/v1.6.19...v1.6.20) - 2025-04-14
+
+### 🎉 New Features
+
+- **[Departments]** Make Department, Attachment URL, and Workflow response message plugins free ([#6663](https://github.com/nocobase/nocobase/pull/6663)) by @chenos
+
+### 🐛 Bug Fixes
+
+- **[client]**
+ - The filter form should not display the "Unsaved changes" prompt ([#6657](https://github.com/nocobase/nocobase/pull/6657)) by @zhangzhonghe
+
+ - "allow multiple" option not working for relation field ([#6661](https://github.com/nocobase/nocobase/pull/6661)) by @katherinehhh
+
+ - In the filter form, when the filter button is clicked, if there are fields that have not passed validation, the filtering is still triggered ([#6659](https://github.com/nocobase/nocobase/pull/6659)) by @zhangzhonghe
+
+ - Switching to the group menu should not jump to a page that has already been hidden in menu ([#6654](https://github.com/nocobase/nocobase/pull/6654)) by @zhangzhonghe
+
+- **[File storage: S3(Pro)]**
+ - Organize language by @jiannx
+
+ - Individual baseurl and public settings, improve S3 pro storage config UX by @jiannx
+
+- **[Migration manager]** the skip auto backup option becomes invalid if environment variable popup appears during migration by @gchust
+
+## [v1.6.19](https://github.com/nocobase/nocobase/compare/v1.6.18...v1.6.19) - 2025-04-14
+
+### 🐛 Bug Fixes
+
+- **[client]**
+ - Fix the issue of preview images being obscured ([#6651](https://github.com/nocobase/nocobase/pull/6651)) by @zhangzhonghe
+
+ - In the form block, the default value of the field configuration will first be displayed as the original variable string and then disappear ([#6649](https://github.com/nocobase/nocobase/pull/6649)) by @zhangzhonghe
+
+## [v1.6.18](https://github.com/nocobase/nocobase/compare/v1.6.17...v1.6.18) - 2025-04-11
+
+### 🚀 Improvements
+
+- **[client]**
+ - Add default type fallback API for `Variable.Input` ([#6644](https://github.com/nocobase/nocobase/pull/6644)) by @mytharcher
+
+ - Optimize prompts for unconfigured pages ([#6641](https://github.com/nocobase/nocobase/pull/6641)) by @zhangzhonghe
+
+- **[Workflow: Delay node]** Support to use variable for duration ([#6621](https://github.com/nocobase/nocobase/pull/6621)) by @mytharcher
+
+- **[Workflow: Custom action event]** Add refresh settings for trigger workflow button by @mytharcher
+
+### 🐛 Bug Fixes
+
+- **[client]**
+ - subtable description overlapping with add new button ([#6646](https://github.com/nocobase/nocobase/pull/6646)) by @katherinehhh
+
+ - dashed underline caused by horizontal form layout in modal ([#6639](https://github.com/nocobase/nocobase/pull/6639)) by @katherinehhh
+
+- **[File storage: S3(Pro)]** Fix missing await for next call. by @jiannx
+
+- **[Email manager]** Fix missing await for next call. by @jiannx
+
+## [v1.6.17](https://github.com/nocobase/nocobase/compare/v1.6.16...v1.6.17) - 2025-04-09
+
+### 🚀 Improvements
+
+- **[utils]** Add duration extension for dayjs ([#6630](https://github.com/nocobase/nocobase/pull/6630)) by @mytharcher
+
+- **[client]**
+ - Support to search field in Filter component ([#6627](https://github.com/nocobase/nocobase/pull/6627)) by @mytharcher
+
+ - Add `trim` API for `Input` and `Variable.TextArea` ([#6624](https://github.com/nocobase/nocobase/pull/6624)) by @mytharcher
+
+- **[Error handler]** Support custom title in AppError component. ([#6409](https://github.com/nocobase/nocobase/pull/6409)) by @sheldon66
+
+- **[IP restriction]** Update IP restriction message content. by @sheldon66
+
+- **[File storage: S3(Pro)]** Support global variables in storage configuration by @mytharcher
+
+### 🐛 Bug Fixes
+
+- **[client]**
+ - rule with 'any' condition does not take effect when condition list is empty ([#6628](https://github.com/nocobase/nocobase/pull/6628)) by @katherinehhh
+
+ - data issue with Gantt block in tree collection ([#6617](https://github.com/nocobase/nocobase/pull/6617)) by @katherinehhh
+
+ - The relationship fields in the filter form report an error after the page is refreshed because x-data-source is not carried ([#6619](https://github.com/nocobase/nocobase/pull/6619)) by @zhangzhonghe
+
+ - variable parse failure when URL parameters contain Chinese characters ([#6618](https://github.com/nocobase/nocobase/pull/6618)) by @katherinehhh
+
+- **[Users]** Issue with parsing the user profile form schema ([#6635](https://github.com/nocobase/nocobase/pull/6635)) by @2013xile
+
+- **[Mobile]** single-select field with 'contains' filter on mobile does not support multiple selection ([#6629](https://github.com/nocobase/nocobase/pull/6629)) by @katherinehhh
+
+- **[Action: Export records]** missing filter params when exporting data after changing pagination ([#6633](https://github.com/nocobase/nocobase/pull/6633)) by @katherinehhh
+
+- **[Email manager]** fix email management permission cannot view email list by @jiannx
+
+- **[File storage: S3(Pro)]** Throw error to user when upload logo to S3 Pro storage (set to default) by @mytharcher
+
+- **[Workflow: Approval]** Fix `updatedAt` changed after migration by @mytharcher
+
+- **[Migration manager]** migration log creation time is displayed incorrectly in some environments by @gchust
+
+## [v1.6.16](https://github.com/nocobase/nocobase/compare/v1.6.15...v1.6.16) - 2025-04-03
+
+### 🐛 Bug Fixes
+
+- **[client]**
+ - x-disabled property not taking effect on form fields ([#6610](https://github.com/nocobase/nocobase/pull/6610)) by @katherinehhh
+
+ - field label display issue to prevent truncation by colon ([#6599](https://github.com/nocobase/nocobase/pull/6599)) by @katherinehhh
+
+- **[database]** When deleting one-to-many records, both `filter` and `filterByTk` are passed and `filter` includes an association field, the `filterByTk` is ignored ([#6606](https://github.com/nocobase/nocobase/pull/6606)) by @2013xile
+
+## [v1.6.15](https://github.com/nocobase/nocobase/compare/v1.6.14...v1.6.15) - 2025-04-01
+
+### 🚀 Improvements
+
+- **[database]**
+ - Add trim option for text field ([#6603](https://github.com/nocobase/nocobase/pull/6603)) by @mytharcher
+
+ - Add trim option for string field ([#6565](https://github.com/nocobase/nocobase/pull/6565)) by @mytharcher
+
+- **[File manager]** Add trim option for text fields of storages collection ([#6604](https://github.com/nocobase/nocobase/pull/6604)) by @mytharcher
+
+- **[Workflow]** Improve code ([#6589](https://github.com/nocobase/nocobase/pull/6589)) by @mytharcher
+
+- **[Workflow: Approval]** Support to use block template for approval process form by @mytharcher
+
+### 🐛 Bug Fixes
+
+- **[database]** Avoid "datetimeNoTz" field changes when value not changed in updating record ([#6588](https://github.com/nocobase/nocobase/pull/6588)) by @mytharcher
+
+- **[client]**
+ - association field (select) displaying N/A when exposing related collection fields ([#6582](https://github.com/nocobase/nocobase/pull/6582)) by @katherinehhh
+
+ - Fix `disabled` property not works when `SchemaInitializerItem` has `items` ([#6597](https://github.com/nocobase/nocobase/pull/6597)) by @mytharcher
+
+ - cascade issue: 'The value of xxx cannot be in array format' when deleting and re-selecting ([#6585](https://github.com/nocobase/nocobase/pull/6585)) by @katherinehhh
+
+- **[Collection field: Many to many (array)]** Issue of filtering by fields in an association collection with a many to many (array) field ([#6596](https://github.com/nocobase/nocobase/pull/6596)) by @2013xile
+
+- **[Public forms]** View permissions include list and get ([#6607](https://github.com/nocobase/nocobase/pull/6607)) by @chenos
+
+- **[Authentication]** token assignment in `AuthProvider` ([#6593](https://github.com/nocobase/nocobase/pull/6593)) by @2013xile
+
+- **[Workflow]** Fix sync option display incorrectly ([#6595](https://github.com/nocobase/nocobase/pull/6595)) by @mytharcher
+
+- **[Block: Map]** map management validation should not pass with space input ([#6575](https://github.com/nocobase/nocobase/pull/6575)) by @katherinehhh
+
+- **[Workflow: Approval]**
+ - Fix client variables to use in approval form by @mytharcher
+
+ - Fix branch mode when `endOnReject` configured as `true` by @mytharcher
+
+## [v1.6.14](https://github.com/nocobase/nocobase/compare/v1.6.13...v1.6.14) - 2025-03-29
+
+### 🐛 Bug Fixes
+
+- **[Calendar]** missing data on boundary dates in weekly calendar view ([#6587](https://github.com/nocobase/nocobase/pull/6587)) by @katherinehhh
+
+- **[Auth: OIDC]** Incorrect redirection occurs when the callback path is the string 'null' by @2013xile
+
+- **[Workflow: Approval]** Fix approval node configuration is incorrect after schema changed by @mytharcher
+
+## [v1.6.13](https://github.com/nocobase/nocobase/compare/v1.6.12...v1.6.13) - 2025-03-28
+
+### 🚀 Improvements
+
+- **[Async task manager]** optimize import/export buttons in Pro ([#6531](https://github.com/nocobase/nocobase/pull/6531)) by @chenos
+
+- **[Action: Export records Pro]** optimize import/export buttons in Pro by @katherinehhh
+
+- **[Migration manager]** allow skip automatic backup and restore for migration by @gchust
+
+### 🐛 Bug Fixes
+
+- **[client]** linkage conflict between same-named association fields in different sub-tables within the same form ([#6577](https://github.com/nocobase/nocobase/pull/6577)) by @katherinehhh
+
+- **[Action: Batch edit]** Click the batch edit button, configure the pop-up window, and then open it again, the pop-up window is blank ([#6578](https://github.com/nocobase/nocobase/pull/6578)) by @zhangzhonghe
+
+## [v1.6.12](https://github.com/nocobase/nocobase/compare/v1.6.11...v1.6.12) - 2025-03-27
+
+### 🐛 Bug Fixes
+
+- **[Block: Multi-step form]**
+ - the submit button has the same color in its default and highlighted by @jiannx
+
+ - fixed the bug that form reset is invalid when the field is associated with other field by @jiannx
+
+- **[Workflow: Approval]** Fix approval form values to submit by @mytharcher
+
+## [v1.6.11](https://github.com/nocobase/nocobase/compare/v1.6.10...v1.6.11) - 2025-03-27
+
+### 🚀 Improvements
+
+- **[client]**
+ - Optimize 502 error message ([#6547](https://github.com/nocobase/nocobase/pull/6547)) by @chenos
+
+ - Only support plain text file to preview ([#6563](https://github.com/nocobase/nocobase/pull/6563)) by @mytharcher
+
+- **[Collection field: Sequence]** support setting sequence as the title field for calendar block ([#6562](https://github.com/nocobase/nocobase/pull/6562)) by @katherinehhh
+
+- **[Workflow: Approval]** Support to skip validator in settings by @mytharcher
+
+### 🐛 Bug Fixes
+
+- **[client]**
+ - issue with date field display in data scope filtering ([#6564](https://github.com/nocobase/nocobase/pull/6564)) by @katherinehhh
+
+ - The 'Ellipsis overflow content' option requires a page refresh for the toggle state to take effect ([#6520](https://github.com/nocobase/nocobase/pull/6520)) by @zhangzhonghe
+
+ - Unable to open another modal within a modal ([#6535](https://github.com/nocobase/nocobase/pull/6535)) by @zhangzhonghe
+
+- **[API documentation]** API document page cannot scroll ([#6566](https://github.com/nocobase/nocobase/pull/6566)) by @zhangzhonghe
+
+- **[Workflow]** Make sure workflow key is generated before save ([#6567](https://github.com/nocobase/nocobase/pull/6567)) by @mytharcher
+
+- **[Workflow: Post-action event]** Multiple records in bulk action should trigger multiple times ([#6559](https://github.com/nocobase/nocobase/pull/6559)) by @mytharcher
+
+- **[Authentication]** Localization issue for fields of sign up page ([#6556](https://github.com/nocobase/nocobase/pull/6556)) by @2013xile
+
+- **[Public forms]** issue with public form page title displaying 'Loading...' ([#6569](https://github.com/nocobase/nocobase/pull/6569)) by @katherinehhh
+
+## [v1.6.10](https://github.com/nocobase/nocobase/compare/v1.6.9...v1.6.10) - 2025-03-25
+
+### 🐛 Bug Fixes
+
+- **[client]**
+ - Unable to use 'Current User' variable when adding a link page ([#6536](https://github.com/nocobase/nocobase/pull/6536)) by @zhangzhonghe
+
+ - field assignment with null value is ineffective ([#6549](https://github.com/nocobase/nocobase/pull/6549)) by @katherinehhh
+
+ - `yarn doc` command error ([#6540](https://github.com/nocobase/nocobase/pull/6540)) by @gchust
+
+ - Remove the 'Allow multiple selection' option from dropdown single-select fields in filter forms ([#6515](https://github.com/nocobase/nocobase/pull/6515)) by @zhangzhonghe
+
+ - Relational field's data range linkage is not effective ([#6530](https://github.com/nocobase/nocobase/pull/6530)) by @zhangzhonghe
+
+- **[Collection: Tree]** Migration issue for plugin-collection-tree ([#6537](https://github.com/nocobase/nocobase/pull/6537)) by @2013xile
+
+- **[Action: Custom request]** Unable to download UTF-8 encoded files ([#6541](https://github.com/nocobase/nocobase/pull/6541)) by @2013xile
+
+## [v1.6.9](https://github.com/nocobase/nocobase/compare/v1.6.8...v1.6.9) - 2025-03-23
+
+### 🐛 Bug Fixes
+
+- **[client]** action button transparency causing setting display issue on hover ([#6529](https://github.com/nocobase/nocobase/pull/6529)) by @katherinehhh
+
+## [v1.6.8](https://github.com/nocobase/nocobase/compare/v1.6.7...v1.6.8) - 2025-03-22
+
+### 🐛 Bug Fixes
+
+- **[server]** The upgrade command may cause workflow errors ([#6524](https://github.com/nocobase/nocobase/pull/6524)) by @gchust
+
+- **[client]** the height of the subtable in the form is set along with the form height ([#6518](https://github.com/nocobase/nocobase/pull/6518)) by @katherinehhh
+
+- **[Authentication]**
+ - X-Authenticator missing ([#6526](https://github.com/nocobase/nocobase/pull/6526)) by @chenos
+
+ - Trim authenticator options ([#6527](https://github.com/nocobase/nocobase/pull/6527)) by @2013xile
+
+- **[Block: Map]** map block key management issue causing request failures due to invisible characters ([#6521](https://github.com/nocobase/nocobase/pull/6521)) by @katherinehhh
+
+- **[Backup manager]** Restoration may cause workflow execution errors by @gchust
+
+- **[WeCom]** Resolve environment variables and secrets when retrieving notification configuration. by @2013xile
+
+## [v1.6.7](https://github.com/nocobase/nocobase/compare/v1.6.6...v1.6.7) - 2025-03-20
+
+### 🚀 Improvements
+
+- **[Workflow: mailer node]** Add secure field config description. ([#6510](https://github.com/nocobase/nocobase/pull/6510)) by @sheldon66
+
+- **[Notification: Email]** Add secure field config description. ([#6501](https://github.com/nocobase/nocobase/pull/6501)) by @sheldon66
+
+- **[Calendar]** Calendar plugin with optional settings to enable or disable quick event creation ([#6391](https://github.com/nocobase/nocobase/pull/6391)) by @Cyx649312038
+
+### 🐛 Bug Fixes
+
+- **[client]** time field submission error in Chinese locale (invalid input syntax for type time) ([#6511](https://github.com/nocobase/nocobase/pull/6511)) by @katherinehhh
+
+- **[File manager]** Unable to access files stored in COS ([#6512](https://github.com/nocobase/nocobase/pull/6512)) by @chenos
+
+- **[Block: Map]** secret key fields not triggering validation in map management ([#6509](https://github.com/nocobase/nocobase/pull/6509)) by @katherinehhh
+
+- **[WEB client]** The path in the route management table is different from the actual path ([#6483](https://github.com/nocobase/nocobase/pull/6483)) by @zhangzhonghe
+
+- **[Action: Export records Pro]** Unable to export attachments by @chenos
+
+- **[Workflow: Approval]**
+ - Fix null user caused crash by @mytharcher
+
+ - Fix error thrown when add query node result by @mytharcher
+
+## [v1.6.6](https://github.com/nocobase/nocobase/compare/v1.6.5...v1.6.6) - 2025-03-18
+
+### 🎉 New Features
+
+- **[client]** support long text fields as title fields for association field ([#6495](https://github.com/nocobase/nocobase/pull/6495)) by @katherinehhh
+
+- **[Workflow: Aggregate node]** Support to configure precision for aggregation result ([#6491](https://github.com/nocobase/nocobase/pull/6491)) by @mytharcher
+
+### 🚀 Improvements
+
+- **[File storage: S3(Pro)]** Change the text 'Access URL Base' to 'Base URL' by @zhangzhonghe
+
+### 🐛 Bug Fixes
+
+- **[evaluators]** Revert round decimal places to 9 ([#6492](https://github.com/nocobase/nocobase/pull/6492)) by @mytharcher
+
+- **[File manager]** encode url ([#6497](https://github.com/nocobase/nocobase/pull/6497)) by @chenos
+
+- **[Data source: Main]** Unable to create a MySQL view. ([#6477](https://github.com/nocobase/nocobase/pull/6477)) by @aaaaaajie
+
+- **[Workflow]** Fix legacy tasks count after workflow deleted ([#6493](https://github.com/nocobase/nocobase/pull/6493)) by @mytharcher
+
+- **[Embed NocoBase]** Page displays blank by @zhangzhonghe
+
+- **[Backup manager]**
+ - Upload files have not been restored when creating sub-app from backup template by @gchust
+
+ - MySQL database restore failure caused by GTID set overlap by @gchust
+
+- **[Workflow: Approval]**
+ - Change returned approval as todo by @mytharcher
+
+ - Fix action button missed in process table by @mytharcher
+
+## [v1.6.5](https://github.com/nocobase/nocobase/compare/v1.6.4...v1.6.5) - 2025-03-17
+
+### 🚀 Improvements
+
+- **[File manager]** Simplify file URL generating logic and API ([#6472](https://github.com/nocobase/nocobase/pull/6472)) by @mytharcher
+
+- **[File storage: S3(Pro)]** Change to a simple way to generate file URL by @mytharcher
+
+- **[Backup manager]** Allow restore backup between pre release and release version of the same version by @gchust
+
+### 🐛 Bug Fixes
+
+- **[client]**
+ - rich text field not clearing data on submission ([#6486](https://github.com/nocobase/nocobase/pull/6486)) by @katherinehhh
+
+ - The color of the icons in the upper right corner of the page does not change with the theme ([#6482](https://github.com/nocobase/nocobase/pull/6482)) by @zhangzhonghe
+
+ - Clicking the reset button on the filter form cannot clear the filtering conditions of the grid card block ([#6475](https://github.com/nocobase/nocobase/pull/6475)) by @zhangzhonghe
+
+- **[Workflow: Manual node]**
+ - Fix migration ([#6484](https://github.com/nocobase/nocobase/pull/6484)) by @mytharcher
+
+ - Change migration name to ensure rerun ([#6487](https://github.com/nocobase/nocobase/pull/6487)) by @mytharcher
+
+ - Fix workflow title field in filter ([#6480](https://github.com/nocobase/nocobase/pull/6480)) by @mytharcher
+
+ - Fix migration error when id column is not exists ([#6470](https://github.com/nocobase/nocobase/pull/6470)) by @chenos
+
+ - Avoid collection synchronized from fields ([#6478](https://github.com/nocobase/nocobase/pull/6478)) by @mytharcher
+
+- **[Workflow: Aggregate node]** Fix round on null result ([#6473](https://github.com/nocobase/nocobase/pull/6473)) by @mytharcher
+
+- **[Workflow]** Don't count tasks when workflow deleted ([#6474](https://github.com/nocobase/nocobase/pull/6474)) by @mytharcher
+
+- **[Backup manager]** Not able to start server when missing default backup settings by @gchust
+
+- **[Workflow: Approval]**
+ - Fix file association field error in process form by @mytharcher
+
+ - Fix tasks count based on hooks by @mytharcher
+
+## [v1.6.4](https://github.com/nocobase/nocobase/compare/v1.6.3...v1.6.4) - 2025-03-14
+
+### 🎉 New Features
+
+- **[client]** Cascade Selection Component Add Data Scope Setting ([#6386](https://github.com/nocobase/nocobase/pull/6386)) by @Cyx649312038
+
+### 🚀 Improvements
+
+- **[utils]** Move `md5` to utils ([#6468](https://github.com/nocobase/nocobase/pull/6468)) by @mytharcher
+
+### 🐛 Bug Fixes
+
+- **[client]** In the tree block, when unchecked, the data in the data block is not being cleared ([#6460](https://github.com/nocobase/nocobase/pull/6460)) by @zhangzhonghe
+
+- **[File manager]** Unable to delete files stored in S3. ([#6467](https://github.com/nocobase/nocobase/pull/6467)) by @chenos
+
+- **[Workflow]** Remove bind workflow settings button from data picker ([#6455](https://github.com/nocobase/nocobase/pull/6455)) by @mytharcher
+
+- **[File storage: S3(Pro)]** Resolve issue with inaccessible S3 Pro signed URLs by @chenos
+
+- **[Workflow: Approval]** Avoid page crash when no applicant in approval process table by @mytharcher
+
+## [v1.6.3](https://github.com/nocobase/nocobase/compare/v1.6.2...v1.6.3) - 2025-03-13
+
+### 🎉 New Features
+
+- **[WeCom]** Allows setting a custom tooltip for the sign-in button by @2013xile
+
+### 🐛 Bug Fixes
+
+- **[client]**
+ - Fix special character in image URL caused not showing ([#6459](https://github.com/nocobase/nocobase/pull/6459)) by @mytharcher
+
+ - incorrect page number when adding data after subtable page size change ([#6437](https://github.com/nocobase/nocobase/pull/6437)) by @katherinehhh
+
+ - The logo style is inconsistent with the previous one ([#6444](https://github.com/nocobase/nocobase/pull/6444)) by @zhangzhonghe
+
+- **[Workflow: Manual node]** Fix error thrown in migration ([#6445](https://github.com/nocobase/nocobase/pull/6445)) by @mytharcher
+
+- **[Data visualization]** Removed fields appear when adding custom filter fields ([#6450](https://github.com/nocobase/nocobase/pull/6450)) by @2013xile
+
+- **[File manager]** Fix a few issues of file manager ([#6436](https://github.com/nocobase/nocobase/pull/6436)) by @mytharcher
+
+- **[Action: Custom request]** custom request server-side permission validation error ([#6438](https://github.com/nocobase/nocobase/pull/6438)) by @katherinehhh
+
+- **[Data source manager]** switching data source in role management does not load corresponding collections ([#6431](https://github.com/nocobase/nocobase/pull/6431)) by @katherinehhh
+
+- **[Action: Batch edit]** Fix workflow can not be triggered in bulk edit submission ([#6440](https://github.com/nocobase/nocobase/pull/6440)) by @mytharcher
+
+- **[Workflow: Custom action event]** Remove `only` in E2E test case by @mytharcher
+
+- **[Workflow: Approval]**
+ - Fix association data not showing in approval form by @mytharcher
+
+ - Fix error thrown when approve on external data source by @mytharcher
+
+## [v1.6.2](https://github.com/nocobase/nocobase/compare/v1.6.1...v1.6.2) - 2025-03-12
+
+### 🐛 Bug Fixes
+
+- **[client]** date field range selection excludes the max date ([#6418](https://github.com/nocobase/nocobase/pull/6418)) by @katherinehhh
+
+- **[Notification: In-app message]** Avoid wrong receivers configuration query all users ([#6424](https://github.com/nocobase/nocobase/pull/6424)) by @sheldon66
+
+- **[Workflow: Manual node]**
+ - Fix migration which missed table prefix and schema logic ([#6425](https://github.com/nocobase/nocobase/pull/6425)) by @mytharcher
+
+ - Change version limit of migration to `<1.7.0` ([#6430](https://github.com/nocobase/nocobase/pull/6430)) by @mytharcher
+
+## [v1.6.1](https://github.com/nocobase/nocobase/compare/v1.6.0...v1.6.1) - 2025-03-11
+
+### 🐛 Bug Fixes
+
+- **[client]**
+ - When using the '$anyOf' operator, the linkage rule is invalid ([#6415](https://github.com/nocobase/nocobase/pull/6415)) by @zhangzhonghe
+
+ - Data not updating in popup windows opened via Link buttons ([#6411](https://github.com/nocobase/nocobase/pull/6411)) by @zhangzhonghe
+
+ - multi-select field value changes and option loss when deleting subtable records ([#6405](https://github.com/nocobase/nocobase/pull/6405)) by @katherinehhh
+
+- **[Notification: In-app message]** Differentiate the in-app message list background color from the message cards to enhance visual hierarchy and readability. ([#6417](https://github.com/nocobase/nocobase/pull/6417)) by @sheldon66
+
+## [v1.6.0](https://github.com/nocobase/nocobase/compare/v1.5.25...v1.6.0) - 2025-03-11
+
+## New Features
+
+### Cluster Mode
+
+The Enterprise edition supports cluster mode deployment via relevant plugins. When the application runs in cluster mode, it can leverage multiple instances and multi-core processing to improve its performance in handling concurrent access.
+
+
+
+Reference: [Deployment - Cluster Mode](https://docs.nocobase.com/welcome/getting-started/deployment/cluster-mode)
+
+### Password Policy
+
+A password policy is established for all users, including rules for password complexity, validity periods, and login security strategies, along with the management of locked accounts.
+
+
+
+Reference: [Password Policy](https://docs.nocobase.com/handbook/password-policy)
+
+### Token Policy
+
+The Token Security Policy is a function configuration designed to protect system security and enhance user experience. It includes three main configuration items: "session validity," "token effective period," and "expired token refresh limit."
+
+
+
+Reference: [Token Policy](https://docs.nocobase.com/handbook/token-policy)
+
+### IP Restriction
+
+NocoBase allows administrators to set up an IP allowlist or blacklist for user access to restrict unauthorized external network connections or block known malicious IP addresses, thereby reducing security risks. It also supports querying access denial logs to identify risky IPs.
+
+
+
+Reference: [IP Restriction](https://docs.nocobase.com/handbook/IP-restriction)
+
+### Environment Variables
+
+Centralized configuration and management of environment variables and secrets are provided for sensitive data storage, configuration reuse, environment isolation, and more.
+
+
+
+Reference: [Environment Variables](https://docs.nocobase.com/handbook/environment-variables)
+
+### Release Management
+
+This feature allows you to migrate application configurations from one environment to another.
+
+
+
+Reference: [Release Management](https://docs.nocobase.com/handbook/release-management)
+
+### Route Management
+
+- **Menu Data Separated and Renamed to Routes**: The menu data has been decoupled from the UI Schema and renamed as "routes." It is divided into two tables, desktopRoutes and mobileRoutes, which correspond to desktop and mobile routes respectively.
+- **Frontend Menu Optimization with Collapse and Responsive Support**: The frontend menu now supports collapse functionality and responsive design for an improved user experience.
+
+
+
+Reference: [Routes](https://docs.nocobase.com/handbook/routes)
+
+### Roles and Permissions
+
+- Supports configuration of action action permissions.
+ 
+- Supports configuration of tab permissions.
+
+
+
+### User Management
+
+Supports customization of user profile forms.
+
+
+
+### Workflow
+
+An entry for the workflow to-do center has been added to the global toolbar, providing real-time notifications for manual nodes and pending approval tasks.
+
+
+
+### Workflow: Custom Action Events
+
+Supports triggering custom action events both globally and in batch actions.
+
+
+
+### Workflow: Approval
+
+- Supports transferring approval responsibilities and adding extra approvers.
+ 
+- Allows approvers to modify the application content when submitting an approval.
+ 
+- Supports configuration of a basic information block within the approval interface.
+- Optimizes the style and interaction of initiating approvals and viewing pending tasks, with these improvements also integrated into the global process to-do center.
+ 
+- No longer distinguishes the location where the approval is initiated; the approval center block can both initiate and process all approvals.
+
+### Workflow: JSON Variable Mapping Node
+
+A new dedicated node has been added to map JSON data from upstream node results into variables.
+
+
+
+Reference: [JSON Variable Mapping](https://docs.nocobase.com/handbook/workflow/nodes/json-variable-mapping)
+
+### Capability Enhancements and Plugin Examples
+
+
+| Extension | Plugin Example |
+| --------------------------------- | --------------------------------------------------------------- |
+| Data Source Custom Preset Fields | @nocobase-sample/plugin-data-source-main-custom-preset-fields |
+| Calendar Register Color Field | @nocobase-sample/plugin-calendar-register-color-field |
+| Calendar Register Title Field | @nocobase-sample/plugin-calendar-register-title-field |
+| Formula Register Expression Field | @nocobase-sample/plugin-field-formula-register-expression-field |
+| Kanban Register Group Field | @nocobase-sample/plugin-kanban-register-group-field |
+| Register Filter Operator | @nocobase-sample/plugin-register-filter-operator |
+| File Storage Extension | @nocobase-sample/plugin-file-storage-demo |
+
+## Breaking Changes
+
+### Update to Token Policy
+
+In version 1.6, a new [Token Policy](https://docs.nocobase.com/handbook/token-policy) was introduced. When authentication fails, a 401 error will be returned along with a redirection to the login page. This behavior differs from previous versions. To bypass the check, refer to the following examples:
+
+Frontend Request:
+
+```javascript
+useRequest({
+ url: '/test',
+ skipAuth: true,
+});
+
+api.request({
+ url: '/test',
+ skipAuth: true,
+});
+```
+
+Backend Middleware:
+
+```javascript
+class PluginMiddlewareExampleServer extends plugin {
+ middlewareExample = (ctx, next) => {
+ if (ctx.path === '/path/to') {
+ ctx.skipAuthCheck = true;
+ }
+ await next();
+ };
+ async load() {
+ this.app.dataSourceManager.afterAddDataSource((dataSource) => {
+ dataSource.resourceManager.use(this.middlewareExample, {
+ before: 'auth',
+ });
+ });
+ }
+}
+```
+
+### Unit Test Function agent.login Changed from Synchronous to Asynchronous
+
+Due to several asynchronous operations required in the authentication process, the test function login is now asynchronous. Example:
+
+```TypeScript
+import { createMockServer } from '@nocobase/test';
+
+describe('my db suite', () => {
+ let app;
+ let agent;
+
+ beforeEach(async () => {
+ app = await createMockServer({
+ registerActions: true,
+ acl: true,
+ plugins: ['users', 'auth', 'acl'],
+ });
+ agent = await app.agent().login(1);
+ });
+
+ test('case1', async () => {
+ await agent.get('/examples');
+ await agent.get('/examples');
+ await agent.resource('examples').create();
+ });
+});
+```
+
+### New Extension API for User Center Settings Items
+
+API:
+
+```TypeScript
+type UserCenterSettingsItemOptions = SchemaSettingsItemType & { aclSnippet?: string };
+
+class Application {
+ addUserCenterSettingsItem(options: UserCenterSettingsItemOptions);
+}
+```
+
+Example:
+
+```javascript
+class PluginUserCenterSettingsExampleClient extends plugin {
+ async load() {
+ this.app.addUserCenterSettingsItem({
+ name: 'nickName',
+ Component: NickName,
+ sort: 0,
+ });
+ }
+}
+```
+
## [v1.5.25](https://github.com/nocobase/nocobase/compare/v1.5.24...v1.5.25) - 2025-03-09
### 🐛 Bug Fixes
diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md
index ba5638d66d..7c775842e9 100644
--- a/CHANGELOG.zh-CN.md
+++ b/CHANGELOG.zh-CN.md
@@ -5,6 +5,831 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
+## [v1.6.25](https://github.com/nocobase/nocobase/compare/v1.6.24...v1.6.25) - 2025-04-29
+
+### 🎉 新特性
+
+- **[undefined]** 添加 license kit 发包ci ([#6786](https://github.com/nocobase/nocobase/pull/6786)) by @jiannx
+
+- **[数据可视化:EChrats]** 条形图支持“y轴反向”设置 by @2013xile
+
+### 🚀 优化
+
+- **[utils]** 增加筛选按钮字段列表的高度,和对字段进行排序分类 ([#6779](https://github.com/nocobase/nocobase/pull/6779)) by @zhangzhonghe
+
+- **[client]** 优化子表格添加按钮样式,并将分页器与按钮排列在同一行 ([#6790](https://github.com/nocobase/nocobase/pull/6790)) by @katherinehhh
+
+- **[文件管理器]** 增加 OSS 存储引擎的超时时间配置项,且默认为 10 分钟 ([#6795](https://github.com/nocobase/nocobase/pull/6795)) by @mytharcher
+
+- **[密码策略]** 默认密码过期时间修改为不过期 by @2013xile
+
+- **[企业微信]** 更新用户邮箱时优先使用企业邮箱而不是个人邮箱 by @2013xile
+
+### 🐛 修复
+
+- **[client]**
+ - 折叠面板区块中,当点击关系字段搜索框的清空按钮后,不应该删除数据范围 ([#6782](https://github.com/nocobase/nocobase/pull/6782)) by @zhangzhonghe
+
+ - 关系字段,在显示关系表下的字段数据时不提交数据 ([#6798](https://github.com/nocobase/nocobase/pull/6798)) by @katherinehhh
+
+ - 禁止将菜单移动到页面 tab 的前面和后面 ([#6777](https://github.com/nocobase/nocobase/pull/6777)) by @zhangzhonghe
+
+ - 表格区块在筛选时重复显示数据 ([#6792](https://github.com/nocobase/nocobase/pull/6792)) by @zhangzhonghe
+
+ - 筛选表单中,切换字段操作符后,刷新页面会报错 ([#6781](https://github.com/nocobase/nocobase/pull/6781)) by @zhangzhonghe
+
+- **[database]**
+ - 避免文本类型输入数据不是字符串时的报错 ([#6797](https://github.com/nocobase/nocobase/pull/6797)) by @mytharcher
+
+ - 补充sql collection和view collection 的unavailableActions ([#6765](https://github.com/nocobase/nocobase/pull/6765)) by @katherinehhh
+
+- **[create-nocobase-app]** 回退 mariadb 版本至 2.5.6,解决兼容性问题 ([#6762](https://github.com/nocobase/nocobase/pull/6762)) by @chenos
+
+- **[用户认证]** 不允许修改认证器标识 ([#6808](https://github.com/nocobase/nocobase/pull/6808)) by @2013xile
+
+- **[模板打印]** 修复:修正权限校验逻辑,防止未授权操作。 by @sheldon66
+
+- **[文件存储:S3 (Pro)]** 访问地址有效期无效 by @jiannx
+
+- **[区块:树]** 通过外键连接后,点击触发筛选,筛选条件为空 by @zhangzhonghe
+
+## [v1.6.24](https://github.com/nocobase/nocobase/compare/v1.6.23...v1.6.24) - 2025-04-24
+
+### 🚀 优化
+
+- **[client]** 调整上传文件的提示信息 ([#6757](https://github.com/nocobase/nocobase/pull/6757)) by @mytharcher
+
+### 🐛 修复
+
+- **[client]**
+ - 视图表,无编辑权限时允许显示导出按钮 ([#6763](https://github.com/nocobase/nocobase/pull/6763)) by @katherinehhh
+
+ - 新增表单中显示关系字段子表格/子表单时关系数据也被新增 ([#6727](https://github.com/nocobase/nocobase/pull/6727)) by @katherinehhh
+
+ - 在表单中获取关联表中的多对多数组字段数据不正确 ([#6744](https://github.com/nocobase/nocobase/pull/6744)) by @2013xile
+
+## [v1.6.23](https://github.com/nocobase/nocobase/compare/v1.6.22...v1.6.23) - 2025-04-23
+
+### 🚀 优化
+
+- **[cli]** 优化 `nocobase upgrade` 命令的内部实现逻辑 ([#6754](https://github.com/nocobase/nocobase/pull/6754)) by @chenos
+
+- **[模板打印]** 用客户端角色访问控制替换了数据源操作权限控制。 by @sheldon66
+
+### 🐛 修复
+
+- **[cli]** 升级时自动更新项目的 package.json ([#6747](https://github.com/nocobase/nocobase/pull/6747)) by @chenos
+
+- **[client]**
+ - 添加关联表格时未过滤已关联的数据 ([#6750](https://github.com/nocobase/nocobase/pull/6750)) by @katherinehhh
+
+ - 树表格中添加子记录按钮的联动规则缺失「当前记录」变量 ([#6752](https://github.com/nocobase/nocobase/pull/6752)) by @katherinehhh
+
+- **[操作:导入记录]** 修复设置字段权限时出现的导入导出异常。 ([#6677](https://github.com/nocobase/nocobase/pull/6677)) by @aaaaaajie
+
+- **[区块:甘特图]** 甘特图区块设置月份视图时,日历头部月份重叠 ([#6753](https://github.com/nocobase/nocobase/pull/6753)) by @katherinehhh
+
+- **[操作:导出记录 Pro]**
+ - pro导出按钮在点击表格排序后丢失过滤参数 by @katherinehhh
+
+ - 修复设置字段权限时出现的导入导出异常。 by @aaaaaajie
+
+- **[文件存储:S3 (Pro)]** 修复已上传文件的响应数据 by @mytharcher
+
+- **[工作流:审批]** 修复预加载审批记录数据的关系字段 by @mytharcher
+
+## [v1.6.22](https://github.com/nocobase/nocobase/compare/v1.6.21...v1.6.22) - 2025-04-22
+
+### 🚀 优化
+
+- **[create-nocobase-app]** 更新依赖,移除 SQLite 支持 ([#6708](https://github.com/nocobase/nocobase/pull/6708)) by @chenos
+
+- **[文件管理器]** 暴露公共包 API ([#6705](https://github.com/nocobase/nocobase/pull/6705)) by @mytharcher
+
+- **[工作流]** 为变量的类型集合增加日期相关类型 ([#6717](https://github.com/nocobase/nocobase/pull/6717)) by @mytharcher
+
+### 🐛 修复
+
+- **[client]**
+ - 移动端顶部的导航栏图标很难被删除的问题 ([#6734](https://github.com/nocobase/nocobase/pull/6734)) by @zhangzhonghe
+
+ - 通过外键连接后,点击触发筛选,筛选条件为空 ([#6634](https://github.com/nocobase/nocobase/pull/6634)) by @zhangzhonghe
+
+ - 筛选按钮中日期字段,切换picker 异常 ([#6695](https://github.com/nocobase/nocobase/pull/6695)) by @katherinehhh
+
+ - 左侧菜单的收起按钮会被绑定工作流弹窗遮挡的问题 ([#6733](https://github.com/nocobase/nocobase/pull/6733)) by @zhangzhonghe
+
+ - 重新打开联动规则时缺少操作选项约束 ([#6723](https://github.com/nocobase/nocobase/pull/6723)) by @katherinehhh
+
+ - 未设置导出权限时仍显示导出按钮 ([#6689](https://github.com/nocobase/nocobase/pull/6689)) by @katherinehhh
+
+ - 被联动规则隐藏的必填字段,不应该影响表单的提交 ([#6709](https://github.com/nocobase/nocobase/pull/6709)) by @zhangzhonghe
+
+- **[server]** create-migration 命令生成的 appVersion 不准确 ([#6740](https://github.com/nocobase/nocobase/pull/6740)) by @chenos
+
+- **[build]** 修复 tar 命令报错的问题 ([#6722](https://github.com/nocobase/nocobase/pull/6722)) by @mytharcher
+
+- **[工作流]** 修复子流程执行定时任务报错的问题 ([#6721](https://github.com/nocobase/nocobase/pull/6721)) by @mytharcher
+
+- **[工作流:自定义操作事件]** 支持多行记录模式的手动执行 by @mytharcher
+
+- **[文件存储:S3 (Pro)]** 增加 multer 逻辑用于服务端上传 by @mytharcher
+
+## [v1.6.21](https://github.com/nocobase/nocobase/compare/v1.6.20...v1.6.21) - 2025-04-17
+
+### 🚀 优化
+
+- **[client]** 为弹窗组件增加 delay API ([#6681](https://github.com/nocobase/nocobase/pull/6681)) by @mytharcher
+
+- **[create-nocobase-app]** 升级部分依赖的版本 ([#6673](https://github.com/nocobase/nocobase/pull/6673)) by @chenos
+
+### 🐛 修复
+
+- **[client]**
+ - 修复审批节点配置中引用模板区块的添加按钮报错问题 ([#6691](https://github.com/nocobase/nocobase/pull/6691)) by @mytharcher
+
+ - 自定义的关系字段没有显示关系字段组件 ([#6692](https://github.com/nocobase/nocobase/pull/6692)) by @katherinehhh
+
+ - 修复上传组件语言问题 ([#6682](https://github.com/nocobase/nocobase/pull/6682)) by @mytharcher
+
+ - 懒加载组件不存在时界面报错 ([#6683](https://github.com/nocobase/nocobase/pull/6683)) by @gchust
+
+ - 补全原生的 Password 组件到封装过的输入组件 ([#6679](https://github.com/nocobase/nocobase/pull/6679)) by @mytharcher
+
+ - 字段赋值本表字段列表中显示了继承表字段,应只显示本表字段 ([#6666](https://github.com/nocobase/nocobase/pull/6666)) by @katherinehhh
+
+- **[database]** 修复 CI 编译错误 ([#6687](https://github.com/nocobase/nocobase/pull/6687)) by @aaaaaajie
+
+- **[build]** 插件依赖 AMD 库时构建产物不正确 ([#6665](https://github.com/nocobase/nocobase/pull/6665)) by @gchust
+
+- **[操作:导入记录]** 修复导入包含时间字段的 xlsx 错误 ([#6672](https://github.com/nocobase/nocobase/pull/6672)) by @aaaaaajie
+
+- **[工作流:人工处理节点]** 修复人工节点任务状态常量 ([#6676](https://github.com/nocobase/nocobase/pull/6676)) by @mytharcher
+
+- **[区块:iframe]** iframe 区块设置全高时页面出现滚动条 ([#6675](https://github.com/nocobase/nocobase/pull/6675)) by @katherinehhh
+
+- **[工作流:自定义操作事件]** 修复测试用例 by @mytharcher
+
+- **[备份管理器]** 还原时若备份未设置密码,但用户输入了密码,还原会出现超时报错 by @gchust
+
+## [v1.6.20](https://github.com/nocobase/nocobase/compare/v1.6.19...v1.6.20) - 2025-04-14
+
+### 🎉 新特性
+
+- **[部门]** 商业插件部门、附件 URL、工作流响应消息改为免费提供 ([#6663](https://github.com/nocobase/nocobase/pull/6663)) by @chenos
+
+### 🐛 修复
+
+- **[client]**
+ - 筛选表单不应该显示“未保存修改”提示 ([#6657](https://github.com/nocobase/nocobase/pull/6657)) by @zhangzhonghe
+
+ - 筛选表单中关系字段的“允许多选”设置项不生效 ([#6661](https://github.com/nocobase/nocobase/pull/6661)) by @katherinehhh
+
+ - 筛选表单中,当点击筛选按钮时,如果有字段未校验通过,依然会触发筛选的问题 ([#6659](https://github.com/nocobase/nocobase/pull/6659)) by @zhangzhonghe
+
+ - 切换到分组菜单时,不应该跳转到已经在菜单中被隐藏的页面 ([#6654](https://github.com/nocobase/nocobase/pull/6654)) by @zhangzhonghe
+
+- **[文件存储:S3 (Pro)]**
+ - 整理语言文案 by @jiannx
+
+ - baseurl 和 public 设置不再互相关联,改进 S3 pro 存储的配置交互体验 by @jiannx
+
+- **[迁移管理]** 迁移时若弹出环境变量弹窗,跳过自动备份选项会失效 by @gchust
+
+## [v1.6.19](https://github.com/nocobase/nocobase/compare/v1.6.18...v1.6.19) - 2025-04-14
+
+### 🐛 修复
+
+- **[client]**
+ - 修复预览图片被遮挡的问题 ([#6651](https://github.com/nocobase/nocobase/pull/6651)) by @zhangzhonghe
+
+ - 表单区块中,字段配置的默认值会先显示为原始变量字符串然后再消失 ([#6649](https://github.com/nocobase/nocobase/pull/6649)) by @zhangzhonghe
+
+## [v1.6.18](https://github.com/nocobase/nocobase/compare/v1.6.17...v1.6.18) - 2025-04-11
+
+### 🚀 优化
+
+- **[client]**
+ - 为 `Variable.Input` 组件增加默认退避类型的 API ([#6644](https://github.com/nocobase/nocobase/pull/6644)) by @mytharcher
+
+ - 优化未配置页面时的提示 ([#6641](https://github.com/nocobase/nocobase/pull/6641)) by @zhangzhonghe
+
+- **[工作流:延时节点]** 支持延迟时间使用变量 ([#6621](https://github.com/nocobase/nocobase/pull/6621)) by @mytharcher
+
+- **[工作流:自定义操作事件]** 为触发工作流按钮增加刷新配置项 by @mytharcher
+
+### 🐛 修复
+
+- **[client]**
+ - 子表格中描述信息与操作按钮遮挡 ([#6646](https://github.com/nocobase/nocobase/pull/6646)) by @katherinehhh
+
+ - 弹窗表单在 horizontal 布局下初始宽度计算错误,导致出现提示和 下划虚线 ([#6639](https://github.com/nocobase/nocobase/pull/6639)) by @katherinehhh
+
+- **[文件存储:S3 (Pro)]** 修复next调用缺少await by @jiannx
+
+- **[邮件管理]** 修复next调用缺少await by @jiannx
+
+## [v1.6.17](https://github.com/nocobase/nocobase/compare/v1.6.16...v1.6.17) - 2025-04-09
+
+### 🚀 优化
+
+- **[utils]** 为 dayjs 包增加时长扩展 ([#6630](https://github.com/nocobase/nocobase/pull/6630)) by @mytharcher
+
+- **[client]**
+ - 支持筛选组件中对字段进行搜索 ([#6627](https://github.com/nocobase/nocobase/pull/6627)) by @mytharcher
+
+ - 为 `Input` 和 `Variable.TextArea` 组件增加 `trim` API ([#6624](https://github.com/nocobase/nocobase/pull/6624)) by @mytharcher
+
+- **[错误处理器]** 在 AppError 组件中支持自定义标题。 ([#6409](https://github.com/nocobase/nocobase/pull/6409)) by @sheldon66
+
+- **[IP 限制]** 更新 IP 限制消息内容。 by @sheldon66
+
+- **[文件存储:S3 (Pro)]** 支持存储引擎的配置中使用全局变量 by @mytharcher
+
+### 🐛 修复
+
+- **[client]**
+ - 联动规则条件设置为任意且无条件内容时属性设置不生效 ([#6628](https://github.com/nocobase/nocobase/pull/6628)) by @katherinehhh
+
+ - 树表使用甘特图区块时数据显示异常 ([#6617](https://github.com/nocobase/nocobase/pull/6617)) by @katherinehhh
+
+ - 筛选表单中的关系字段在刷新页面后,由于没有携带 x-data-source 而报错 ([#6619](https://github.com/nocobase/nocobase/pull/6619)) by @zhangzhonghe
+
+ - 链接中中文参数变量值解析失败 ([#6618](https://github.com/nocobase/nocobase/pull/6618)) by @katherinehhh
+
+- **[用户]** 用户个人资料表单 schema 的解析问题 ([#6635](https://github.com/nocobase/nocobase/pull/6635)) by @2013xile
+
+- **[移动端]** 下拉单选字段在移动端设置筛选符为包含时组件未支持多选 ([#6629](https://github.com/nocobase/nocobase/pull/6629)) by @katherinehhh
+
+- **[操作:导出记录]** 筛选数据后切换分页再导出时筛选参数丢失 ([#6633](https://github.com/nocobase/nocobase/pull/6633)) by @katherinehhh
+
+- **[邮件管理]** 邮件管理权限无法查看邮件列表 by @jiannx
+
+- **[文件存储:S3 (Pro)]** 当用户上传 logo 失败时提示错误(设置为默认存储的 S3 Pro) by @mytharcher
+
+- **[工作流:审批]** 修复更新时间在迁移后变化 by @mytharcher
+
+- **[迁移管理]** 部分服务器环境下迁移日志创建日期显示不正确 by @gchust
+
+## [v1.6.16](https://github.com/nocobase/nocobase/compare/v1.6.15...v1.6.16) - 2025-04-03
+
+### 🐛 修复
+
+- **[client]**
+ - 表单字段设置不可编辑不起作用 ([#6610](https://github.com/nocobase/nocobase/pull/6610)) by @katherinehhh
+
+ - 表单字段标题因冒号导致的截断问题 ([#6599](https://github.com/nocobase/nocobase/pull/6599)) by @katherinehhh
+
+- **[database]** 删除一对多记录时,同时传递 `filter` 和 `filterByTk` 参数,`filter` 包含关系字段时,`filterByTk` 参数失效 ([#6606](https://github.com/nocobase/nocobase/pull/6606)) by @2013xile
+
+## [v1.6.15](https://github.com/nocobase/nocobase/compare/v1.6.14...v1.6.15) - 2025-04-01
+
+### 🚀 优化
+
+- **[database]**
+ - 为多行文本类型字段增加去除首尾空白字符的选项 ([#6603](https://github.com/nocobase/nocobase/pull/6603)) by @mytharcher
+
+ - 为单行文本增加自动去除首尾空白字符的选项 ([#6565](https://github.com/nocobase/nocobase/pull/6565)) by @mytharcher
+
+- **[文件管理器]** 为存储引擎表的文本字段增加去除首尾空白字符的选项 ([#6604](https://github.com/nocobase/nocobase/pull/6604)) by @mytharcher
+
+- **[工作流]** 优化代码 ([#6589](https://github.com/nocobase/nocobase/pull/6589)) by @mytharcher
+
+- **[工作流:审批]** 支持审批表单使用区块模板 by @mytharcher
+
+### 🐛 修复
+
+- **[database]** 避免“日期时间(无时区)”字段在值未变动的更新时触发值改变 ([#6588](https://github.com/nocobase/nocobase/pull/6588)) by @mytharcher
+
+- **[client]**
+ - 关系字段(select)放出关系表字段时默认显示 N/A ([#6582](https://github.com/nocobase/nocobase/pull/6582)) by @katherinehhh
+
+ - 修复 `SchemaInitializerItem` 配置了 `items` 时 `disabled` 属性无效的问题 ([#6597](https://github.com/nocobase/nocobase/pull/6597)) by @mytharcher
+
+ - 级联组件删除后重新选择时出现 'The value of xxx cannot be in array format' ([#6585](https://github.com/nocobase/nocobase/pull/6585)) by @katherinehhh
+
+- **[数据表字段:多对多 (数组)]** 主表筛选带有多对多(数组)字段的关联表中的字段报错的问题 ([#6596](https://github.com/nocobase/nocobase/pull/6596)) by @2013xile
+
+- **[公开表单]** 查看权限包括 list 和 get ([#6607](https://github.com/nocobase/nocobase/pull/6607)) by @chenos
+
+- **[用户认证]** `AuthProvider` 中的 token 赋值 ([#6593](https://github.com/nocobase/nocobase/pull/6593)) by @2013xile
+
+- **[工作流]** 修复同步选项展示问题 ([#6595](https://github.com/nocobase/nocobase/pull/6595)) by @mytharcher
+
+- **[区块:地图]** 地图管理必填校验不应通过空格输入 ([#6575](https://github.com/nocobase/nocobase/pull/6575)) by @katherinehhh
+
+- **[工作流:审批]**
+ - 修复审批表单中的前端变量 by @mytharcher
+
+ - 修复分支模式下配置拒绝则结束时的流程问题 by @mytharcher
+
+## [v1.6.14](https://github.com/nocobase/nocobase/compare/v1.6.13...v1.6.14) - 2025-03-29
+
+### 🐛 修复
+
+- **[日历]** 日历区块以周为视图时,边界日期不显示数据 ([#6587](https://github.com/nocobase/nocobase/pull/6587)) by @katherinehhh
+
+- **[认证:OIDC]** 回调路径是字符串'null'时导致跳转不正确 by @2013xile
+
+- **[工作流:审批]** 修复审批节点界面配置变更后数据未同步的问题 by @mytharcher
+
+## [v1.6.13](https://github.com/nocobase/nocobase/compare/v1.6.12...v1.6.13) - 2025-03-28
+
+### 🚀 优化
+
+- **[异步任务管理器]** 优化 Pro 导入导出按钮异步逻辑 ([#6531](https://github.com/nocobase/nocobase/pull/6531)) by @chenos
+
+- **[操作:导出记录 Pro]** 优化 Pro 导入导出按钮 by @katherinehhh
+
+- **[迁移管理]** 允许执行迁移时跳过自动备份还原 by @gchust
+
+### 🐛 修复
+
+- **[client]** 同一表单中不同关系字段的同名关系字段的联动互相影响 ([#6577](https://github.com/nocobase/nocobase/pull/6577)) by @katherinehhh
+
+- **[操作:批量编辑]** 点击批量编辑按钮,配置完弹窗再打开,弹窗是空白的 ([#6578](https://github.com/nocobase/nocobase/pull/6578)) by @zhangzhonghe
+
+## [v1.6.12](https://github.com/nocobase/nocobase/compare/v1.6.11...v1.6.12) - 2025-03-27
+
+### 🐛 修复
+
+- **[区块:分步表单]**
+ - 提交按钮默认和高亮情况下颜色一样 by @jiannx
+
+ - 修复当字段与其他表单字段存在关联时,表单重置无效 by @jiannx
+
+- **[工作流:审批]** 修复审批表单提交值的问题 by @mytharcher
+
+## [v1.6.11](https://github.com/nocobase/nocobase/compare/v1.6.10...v1.6.11) - 2025-03-27
+
+### 🚀 优化
+
+- **[client]**
+ - 优化 502 错误提示 ([#6547](https://github.com/nocobase/nocobase/pull/6547)) by @chenos
+
+ - 仅支持纯文本文件预览 ([#6563](https://github.com/nocobase/nocobase/pull/6563)) by @mytharcher
+
+- **[数据表字段:自动编码]** 支持使用 sequence 作为日历区块的标题字段 ([#6562](https://github.com/nocobase/nocobase/pull/6562)) by @katherinehhh
+
+- **[工作流:审批]** 支持审批处理按钮跳过表单验证的设置 by @mytharcher
+
+### 🐛 修复
+
+- **[client]**
+ - 数据范围中筛选日期字段显示异常 ([#6564](https://github.com/nocobase/nocobase/pull/6564)) by @katherinehhh
+
+ - 选项“省略超出长度的内容”需要刷新页面,开关的状态才生效 ([#6520](https://github.com/nocobase/nocobase/pull/6520)) by @zhangzhonghe
+
+ - 在弹窗中无法再次打开弹窗 ([#6535](https://github.com/nocobase/nocobase/pull/6535)) by @zhangzhonghe
+
+- **[API 文档]** API 文档页面不能滚动 ([#6566](https://github.com/nocobase/nocobase/pull/6566)) by @zhangzhonghe
+
+- **[工作流]** 确保创建工作流之前 key 已生成 ([#6567](https://github.com/nocobase/nocobase/pull/6567)) by @mytharcher
+
+- **[工作流:操作后事件]** 多行记录的批量操作需要触发多次 ([#6559](https://github.com/nocobase/nocobase/pull/6559)) by @mytharcher
+
+- **[用户认证]** 注册页面字段的本地化问题 ([#6556](https://github.com/nocobase/nocobase/pull/6556)) by @2013xile
+
+- **[公开表单]** 公开表单页面标题不应该显示 Loading... ([#6569](https://github.com/nocobase/nocobase/pull/6569)) by @katherinehhh
+
+## [v1.6.10](https://github.com/nocobase/nocobase/compare/v1.6.9...v1.6.10) - 2025-03-25
+
+### 🐛 修复
+
+- **[client]**
+ - 添加链接页面时,无法使用“当前用户”变量 ([#6536](https://github.com/nocobase/nocobase/pull/6536)) by @zhangzhonghe
+
+ - 字段赋值对字段进行“空值”赋值无效 ([#6549](https://github.com/nocobase/nocobase/pull/6549)) by @katherinehhh
+
+ - `yarn doc` 命令报错 ([#6540](https://github.com/nocobase/nocobase/pull/6540)) by @gchust
+
+ - 筛选表单中,移除下拉单选字段的“允许多选”选项 ([#6515](https://github.com/nocobase/nocobase/pull/6515)) by @zhangzhonghe
+
+ - 关系字段的数据范围联动不生效 ([#6530](https://github.com/nocobase/nocobase/pull/6530)) by @zhangzhonghe
+
+- **[数据表:树]** 树表插件的迁移脚本问题 ([#6537](https://github.com/nocobase/nocobase/pull/6537)) by @2013xile
+
+- **[操作:自定义请求]** 无法下载utf8编码的文件 ([#6541](https://github.com/nocobase/nocobase/pull/6541)) by @2013xile
+
+## [v1.6.9](https://github.com/nocobase/nocobase/compare/v1.6.8...v1.6.9) - 2025-03-23
+
+### 🐛 修复
+
+- **[client]** 操作按钮透明状态导致 hover 时按钮 setting 显示异常 ([#6529](https://github.com/nocobase/nocobase/pull/6529)) by @katherinehhh
+
+## [v1.6.8](https://github.com/nocobase/nocobase/compare/v1.6.7...v1.6.8) - 2025-03-22
+
+### 🐛 修复
+
+- **[server]** Upgrade 命令可能造成工作流报错 ([#6524](https://github.com/nocobase/nocobase/pull/6524)) by @gchust
+
+- **[client]** 表单中的子表格高度会随主表单高度一同设置 ([#6518](https://github.com/nocobase/nocobase/pull/6518)) by @katherinehhh
+
+- **[用户认证]**
+ - X-Authenticator 缺失 ([#6526](https://github.com/nocobase/nocobase/pull/6526)) by @chenos
+
+ - 移除认证器配置项前后的空格、换行符 ([#6527](https://github.com/nocobase/nocobase/pull/6527)) by @2013xile
+
+- **[区块:地图]** 地图区块 密钥管理中不可见字符导致的密钥请求失败的问题 ([#6521](https://github.com/nocobase/nocobase/pull/6521)) by @katherinehhh
+
+- **[备份管理器]** 还原过程中可能引起工作流执行报错 by @gchust
+
+- **[企业微信]** 获取通知配置时需要解析环境变量和密钥 by @2013xile
+
+## [v1.6.7](https://github.com/nocobase/nocobase/compare/v1.6.6...v1.6.7) - 2025-03-20
+
+### 🚀 优化
+
+- **[工作流:邮件发送节点]** 增加安全字段配置描述。 ([#6510](https://github.com/nocobase/nocobase/pull/6510)) by @sheldon66
+
+- **[通知:电子邮件]** 增加安全字段配置描述。 ([#6501](https://github.com/nocobase/nocobase/pull/6501)) by @sheldon66
+
+- **[日历]** 日历插件添加开启或关闭快速创建事件可选设置 ([#6391](https://github.com/nocobase/nocobase/pull/6391)) by @Cyx649312038
+
+### 🐛 修复
+
+- **[client]** 时间字段在中文语言下提交时报错 invalid input syntax for type time ([#6511](https://github.com/nocobase/nocobase/pull/6511)) by @katherinehhh
+
+- **[文件管理器]** COS 存储的文件无法访问 ([#6512](https://github.com/nocobase/nocobase/pull/6512)) by @chenos
+
+- **[区块:地图]** 地图管理中密钥必填校验失败 ([#6509](https://github.com/nocobase/nocobase/pull/6509)) by @katherinehhh
+
+- **[WEB 客户端]** 路由管理表格中的路径与实际路径不一样 ([#6483](https://github.com/nocobase/nocobase/pull/6483)) by @zhangzhonghe
+
+- **[操作:导出记录 Pro]** 无法导出附件 by @chenos
+
+- **[工作流:审批]**
+ - 修复空用户造成页面崩溃 by @mytharcher
+
+ - 修复审批人界面配置添加查询节点时的页面崩溃 by @mytharcher
+
+## [v1.6.6](https://github.com/nocobase/nocobase/compare/v1.6.5...v1.6.6) - 2025-03-18
+
+### 🎉 新特性
+
+- **[client]** 支持长文本字段作为关系字段的标题字段 ([#6495](https://github.com/nocobase/nocobase/pull/6495)) by @katherinehhh
+
+- **[工作流:聚合查询节点]** 支持为聚合结果配置精度选项 ([#6491](https://github.com/nocobase/nocobase/pull/6491)) by @mytharcher
+
+### 🚀 优化
+
+- **[文件存储:S3 (Pro)]** 将文案“访问 URL 基础”改为“基础 URL” by @zhangzhonghe
+
+### 🐛 修复
+
+- **[evaluators]** 将表达式计算保留小数调整回 9 位 ([#6492](https://github.com/nocobase/nocobase/pull/6492)) by @mytharcher
+
+- **[文件管理器]** URL 转义 ([#6497](https://github.com/nocobase/nocobase/pull/6497)) by @chenos
+
+- **[数据源:主数据库]** 无法创建 MySQL 视图 ([#6477](https://github.com/nocobase/nocobase/pull/6477)) by @aaaaaajie
+
+- **[工作流]** 修复历史遗留任务数量工作流删除后统计错误 ([#6493](https://github.com/nocobase/nocobase/pull/6493)) by @mytharcher
+
+- **[嵌入 NocoBase]** 页面显示空白 by @zhangzhonghe
+
+- **[备份管理器]**
+ - 通过多应用模板创建子应用时备份中的上传文件未被正确还原 by @gchust
+
+ - 还原 MySQL 数据库备份时由于 GTID 集合重叠导致的失败 by @gchust
+
+- **[工作流:审批]**
+ - 将退回的审批单据列入待办 by @mytharcher
+
+ - 修复审批过程表格中发起人查看按钮消失的问题 by @mytharcher
+
+## [v1.6.5](https://github.com/nocobase/nocobase/compare/v1.6.4...v1.6.5) - 2025-03-17
+
+### 🚀 优化
+
+- **[文件管理器]** 简化生成文件 URL 的逻辑和 API ([#6472](https://github.com/nocobase/nocobase/pull/6472)) by @mytharcher
+
+- **[文件存储:S3 (Pro)]** 优化生成文件 URL 的方法 by @mytharcher
+
+- **[备份管理器]** 允许在相同版本的预发布和发布版本之间恢复备份 by @gchust
+
+### 🐛 修复
+
+- **[client]**
+ - 富文本字段清空后提交时数据未删除 ([#6486](https://github.com/nocobase/nocobase/pull/6486)) by @katherinehhh
+
+ - 页面右上角图标的颜色不会随主题变化 ([#6482](https://github.com/nocobase/nocobase/pull/6482)) by @zhangzhonghe
+
+ - 点击筛选表单的重置按钮无法清除网格卡片区块的筛选条件 ([#6475](https://github.com/nocobase/nocobase/pull/6475)) by @zhangzhonghe
+
+- **[工作流:人工处理节点]**
+ - 修复迁移脚本 ([#6484](https://github.com/nocobase/nocobase/pull/6484)) by @mytharcher
+
+ - 修改迁移脚本确保执行 ([#6487](https://github.com/nocobase/nocobase/pull/6487)) by @mytharcher
+
+ - 修复区块的筛选组件中工作流标题项 ([#6480](https://github.com/nocobase/nocobase/pull/6480)) by @mytharcher
+
+ - 修复 id 列不存在时迁移脚本报错 ([#6470](https://github.com/nocobase/nocobase/pull/6470)) by @chenos
+
+ - 避免历史表被关系字段同步出来 ([#6478](https://github.com/nocobase/nocobase/pull/6478)) by @mytharcher
+
+- **[工作流:聚合查询节点]** 修复对聚合结果为 null 时取整报错 ([#6473](https://github.com/nocobase/nocobase/pull/6473)) by @mytharcher
+
+- **[工作流]** 不统计已删除的工作流的待办 ([#6474](https://github.com/nocobase/nocobase/pull/6474)) by @mytharcher
+
+- **[备份管理器]** 默认的备份设置不存在时服务器无法启动 by @gchust
+
+- **[工作流:审批]**
+ - 修复审批表单中文件字段报错问题 by @mytharcher
+
+ - 基于钩子事件修复待办任务数量 by @mytharcher
+
+## [v1.6.4](https://github.com/nocobase/nocobase/compare/v1.6.3...v1.6.4) - 2025-03-14
+
+### 🎉 新特性
+
+- **[client]** 级联选择组件添加数据范围设置 ([#6386](https://github.com/nocobase/nocobase/pull/6386)) by @Cyx649312038
+
+### 🚀 优化
+
+- **[utils]** 将 `md5` 方法移到通用包 ([#6468](https://github.com/nocobase/nocobase/pull/6468)) by @mytharcher
+
+### 🐛 修复
+
+- **[client]** 在树区块中,取消选中时,数据区块的数据没有被清空 ([#6460](https://github.com/nocobase/nocobase/pull/6460)) by @zhangzhonghe
+
+- **[文件管理器]** 无法删除 s3 文件存储的文件 ([#6467](https://github.com/nocobase/nocobase/pull/6467)) by @chenos
+
+- **[工作流]** 在数据选择器中移除绑定工作流的配置按钮 ([#6455](https://github.com/nocobase/nocobase/pull/6455)) by @mytharcher
+
+- **[文件存储:S3 (Pro)]** 修复 s3 pro 的签名 url 无法访问的问题 by @chenos
+
+- **[工作流:审批]** 避免审批流程表格中由于没有发起人时的页面崩溃 by @mytharcher
+
+## [v1.6.3](https://github.com/nocobase/nocobase/compare/v1.6.2...v1.6.3) - 2025-03-13
+
+### 🎉 新特性
+
+- **[企业微信]** 支持自定义登录按钮提示 by @2013xile
+
+### 🐛 修复
+
+- **[client]**
+ - 修复图片中特殊字符导致不显示的问题 ([#6459](https://github.com/nocobase/nocobase/pull/6459)) by @mytharcher
+
+ - 子表格切换分页数后新增数据页码显示错误 ([#6437](https://github.com/nocobase/nocobase/pull/6437)) by @katherinehhh
+
+ - Logo 的样式与之前的不一致 ([#6444](https://github.com/nocobase/nocobase/pull/6444)) by @zhangzhonghe
+
+- **[工作流:人工处理节点]** 修复迁移脚本报错 ([#6445](https://github.com/nocobase/nocobase/pull/6445)) by @mytharcher
+
+- **[数据可视化]** 添加自定义筛选字段时会出现已移除字段 ([#6450](https://github.com/nocobase/nocobase/pull/6450)) by @2013xile
+
+- **[文件管理器]** 修复文件管理一些问题 ([#6436](https://github.com/nocobase/nocobase/pull/6436)) by @mytharcher
+
+- **[操作:自定义请求]** 自定义请求的服务端权限校验错误 ([#6438](https://github.com/nocobase/nocobase/pull/6438)) by @katherinehhh
+
+- **[数据源管理]** 角色管理中切换数据源没有加载对应数据表 ([#6431](https://github.com/nocobase/nocobase/pull/6431)) by @katherinehhh
+
+- **[操作:批量编辑]** 修复批量编辑提交时未能触发工作流的问题 ([#6440](https://github.com/nocobase/nocobase/pull/6440)) by @mytharcher
+
+- **[工作流:自定义操作事件]** 移除 E2E 测试中的 `only` by @mytharcher
+
+- **[工作流:审批]**
+ - 修复审批表单中关系数据未展示的问题 by @mytharcher
+
+ - 修复外部数据源审批时的报错 by @mytharcher
+
+## [v1.6.2](https://github.com/nocobase/nocobase/compare/v1.6.1...v1.6.2) - 2025-03-12
+
+### 🐛 修复
+
+- **[client]** 表单日期字段日期范围,最大日期可选范围少一天 ([#6418](https://github.com/nocobase/nocobase/pull/6418)) by @katherinehhh
+
+- **[通知:站内信]** 避免错误的接收人配置导致查询出全部用户 ([#6424](https://github.com/nocobase/nocobase/pull/6424)) by @sheldon66
+
+- **[工作流:人工处理节点]**
+ - 修复遗漏表前缀和 schema 的迁移脚本 ([#6425](https://github.com/nocobase/nocobase/pull/6425)) by @mytharcher
+
+ - 调整迁移脚本版本范围限制为 `<1.7.0` ([#6430](https://github.com/nocobase/nocobase/pull/6430)) by @mytharcher
+
+## [v1.6.1](https://github.com/nocobase/nocobase/compare/v1.6.0...v1.6.1) - 2025-03-11
+
+### 🐛 修复
+
+- **[client]**
+ - 使用“$anyOf”操作符时,联动规则无效 ([#6415](https://github.com/nocobase/nocobase/pull/6415)) by @zhangzhonghe
+
+ - 使用链接按钮打开的弹窗,数据不更新 ([#6411](https://github.com/nocobase/nocobase/pull/6411)) by @zhangzhonghe
+
+ - 子表格删除记录的时候多选字段值错误且选项缺失 ([#6405](https://github.com/nocobase/nocobase/pull/6405)) by @katherinehhh
+
+- **[通知:站内信]** 在站内信列表中,将背景颜色与消息卡片的颜色区分开,以提升视觉层次感和可读性。 ([#6417](https://github.com/nocobase/nocobase/pull/6417)) by @sheldon66
+
+## [v1.6.0](https://github.com/nocobase/nocobase/compare/v1.5.25...v1.6.0) - 2025-03-11
+
+## 新特性
+
+### 集群模式
+
+企业版可通过相关插件支持集群模式部署,应用以集群模式运行时,可以通过多个实例和使用多核模式来提高应用的对并发访问处理的性能。
+
+
+
+参考文档:[集群部署](https://docs-cn.nocobase.com/welcome/getting-started/deployment/cluster-mode)
+
+### 密码策略
+
+为所有用户设置密码规则,密码有效期和密码登录安全策略,管理锁定用户。
+
+
+
+参考文档:[密码策略和用户锁定](https://docs-cn.nocobase.com/handbook/password-policy)
+
+### Token 安全策略
+
+Token 安全策略是一种用于保护系统安全和体验的功能配置。它包括了三个主要配置项:“会话有效期”、“Token 有效周期” 和 “过期 Token 刷新时限” 。
+
+
+
+参考文档:[Token 安全策略](https://docs-cn.nocobase.com/handbook/token-policy)
+
+### IP 限制
+
+NocoBase 支持管理员对用户访问 IP 设置白名单或黑名单,以限制未授权的外部网络连接或阻止已知的恶意 IP 地址,降低安全风险。同时支持管理员查询访问拒绝日志,识别风险 IP。
+
+
+
+参考文档:[IP 限制](https://docs-cn.nocobase.com/handbook/IP-restriction)
+
+### 变量和密钥
+
+集中配置和管理环境变量和密钥,用于敏感数据存储、配置数据重用、环境配置隔离等。
+
+
+
+参考文档:[变量和密钥](https://docs-cn.nocobase.com/handbook/environment-variables)
+
+### 迁移管理
+
+用于将应用配置从一个应用环境迁移到另一个应用环境。
+
+
+
+参考文档:[迁移管理](https://docs-cn.nocobase.com/handbook/migration-manager)[发布管理](https://docs-cn.nocobase.com/handbook/release-management)
+
+### 路由管理
+
+* **菜单数据独立并改名为路由**:菜单数据从 UI Schema 中拆分出来,改名为**路由**,分为 `desktopRoutes` 和 `mobileRoutes` 两张表,分别对应桌面端路由和移动端路由。
+* **菜单前端优化,支持折叠与响应式**:菜单在前端实现了**折叠**与**响应式**适配,提升了使用体验。
+
+
+
+参考文档:[路由管理](https://docs-cn.nocobase.com/handbook/routes)
+
+### 角色和权限
+
+* 支持配置更多的操作按钮权限,包括弹窗、链接、扫码、触发工作流
+ 
+* 支持配置标签页权限
+
+ 
+
+### 用户管理
+
+支持配置用户个人资料表单
+
+
+
+### 工作流
+
+在全局工具栏中增加流程待办中心入口,并实时提示人工节点、审批的相关待办任务数量。
+
+
+
+### 工作流:自定义操作事件
+
+支持全局和批量数据触发自定义操作事件。
+
+
+
+### 工作流:审批
+
+* 支持转签、加签。
+* 支持审批人在提交审批时修改申请内容。
+* 支持在审批界面中配置审批基础信息区块。
+* 优化审批发起和待办区块的样式和交互,同时也在全局的流程待办中心中内置。
+* 不再区分发起审批的位置,审批中心区块可以发起和处理所有审批。
+
+### 工作流:JSON 变量映射节点
+
+新增用于将上游节点结果中的 JSON 数据映射为变量的专用节点。
+
+
+
+参考文档:[JSON 变量映射](https://docs-cn.nocobase.com/handbook/workflow/nodes/json-variable-mapping)
+
+### 扩展能力提升及插件示例
+
+
+| 扩展项 | 插件示例 |
+| ---------------------- | --------------------------------------------------------------- |
+| 数据表预置字段扩展 | @nocobase-sample/plugin-data-source-main-custom-preset-fields |
+| 日历颜色字段可选项扩展 | @nocobase-sample/plugin-calendar-register-color-field |
+| 日历标题字段可选项扩展 | @nocobase-sample/plugin-calendar-register-title-field |
+| 公式可选项字段扩展 | @nocobase-sample/plugin-field-formula-register-expression-field |
+| 看板分组字段扩展 | @nocobase-sample/plugin-kanban-register-group-field |
+| 筛选操作符扩展 | @nocobase-sample/plugin-register-filter-operator |
+| 文件存储扩展 | @nocobase-sample/plugin-file-storage-demo |
+
+## 不兼容变更
+
+### Token 安全策略更新
+
+1.6 版本新增了 [Token 安全策略](https://docs-cn.nocobase.com/handbook/token-policy),Auth 认证检查未通过时,将返回 401 错误并跳转至登录页。此行为与之前版本有所不同。如需跳过检查,可参考以下示例进行处理:
+
+前端请求
+
+```javascript
+useRequest({
+ url: '/test',
+ skipAuth: true,
+});
+
+api.request({
+ url: '/test',
+ skipAuth: true,
+});
+```
+
+后端中间件
+
+```javascript
+class PluginMiddlewareExampleServer extends plugin {
+ middlewareExample = (ctx, next) => {
+ if (ctx.path === '/path/to') {
+ ctx.skipAuthCheck = true;
+ }
+ await next();
+ };
+ async load() {
+ this.app.dataSourceManager.afterAddDataSource((dataSource) => {
+ dataSource.resourceManager.use(this.middlewareExample, {
+ before: 'auth',
+ });
+ });
+ }
+}
+```
+
+### 单元测试函数 agent.login 由同步改为异步
+
+由于认证流程需要进行一些异步操作,测试函数 login 改为异步, 示例:
+
+```TypeScript
+import { createMockServer } from '@nocobase/test';
+
+describe('my db suite', () => {
+ let app;
+ let agent;
+
+ beforeEach(async () => {
+ app = await createMockServer({
+ registerActions: true,
+ acl: true,
+ plugins: ['users', 'auth', 'acl'],
+ });
+ agent = await app.agent().login(1);
+ });
+
+ test('case1', async () => {
+ await agent.get('/examples');
+ await agent.get('/examples');
+ await agent.resource('examples').create();
+ });
+});
+```
+
+### 提供全新的用户中心设置项的扩展 API
+
+API
+
+```ts
+type UserCenterSettingsItemOptions = SchemaSettingsItemType & { aclSnippet?: string };
+
+class Application {
+ addUserCenterSettingsItem(options: UserCenterSettingsItemOptions);
+}
+```
+
+示例
+
+```javascript
+class PluginUserCenterSettingsExampleClient extends plugin {
+ async load() {
+ this.app.addUserCenterSettingsItem({
+ name: 'nickName',
+ Component: NickName,
+ sort: 0,
+ });
+ }
+}
+```
+
## [v1.5.25](https://github.com/nocobase/nocobase/compare/v1.5.24...v1.5.25) - 2025-03-09
### 🐛 修复
diff --git a/LICENSE.txt b/LICENSE.txt
index b3c87c1e83..babf2bf053 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,4 +1,4 @@
-Updated Date: February 20, 2025
+Updated Date: April 1, 2025
NocoBase License Agreement
@@ -88,7 +88,7 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr
6.6 Can sell plugins developed for Software in the Marketplace.
-6.7 The User with an Enterprise Edition License can sell Upper Layer Application to their clients.
+6.7 The User with a Professional or Enterprise Edition License can sell Upper Layer Application to their clients.
6.8 Not restricted by the AGPL-3.0 agreement.
@@ -106,9 +106,9 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr
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 for the User withot an Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license.
+7.5 It is not allowed for the User withot a Professional or Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license.
-7.6 It is not allowed for the User with an Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license with access to further development and configuration.
+7.6 It is not allowed for the User with a Professional or Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license with access to further development and configuration.
7.7 It is not allowed to publicly sell plugins developed for Software outside of the Marketplace.
diff --git a/README.ja-JP.md b/README.ja-JP.md
index 2fff33bddd..49e5c1be42 100644
--- a/README.ja-JP.md
+++ b/README.ja-JP.md
@@ -2,14 +2,10 @@
https://github.com/user-attachments/assets/cf08bfe5-e6e6-453c-8b96-350a6a8bed17
-## ご協力ありがとうございます!
+
-
-
-## リリースノート
-
-リリースノートは[ブログ](https://www.nocobase.com/ja/blog/timeline)で随時更新され、週ごとにまとめて公開しています。
+
## NocoBaseはなに?
@@ -28,6 +24,16 @@ https://docs-cn.nocobase.com/
コミュニティ:
https://forum.nocobase.com/
+チュートリアル:
+https://www.nocobase.com/ja/tutorials
+
+顧客のストーリー:
+https://www.nocobase.com/ja/blog/tags/customer-stories
+
+## リリースノート
+
+リリースノートは[ブログ](https://www.nocobase.com/ja/blog/timeline)で随時更新され、週ごとにまとめて公開しています。
+
## 他の製品との違い
### 1. データモデル駆動
diff --git a/README.md b/README.md
index 1314bec051..a2cfdb2bc4 100644
--- a/README.md
+++ b/README.md
@@ -2,19 +2,14 @@ English | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md)
https://github.com/user-attachments/assets/a50c100a-4561-4e06-b2d2-d48098659ec0
-## We'd love your support!
-
+
-
-
-## Release Notes
-
-Our [blog](https://www.nocobase.com/en/blog/timeline) is regularly updated with release notes and provides a weekly summary.
+
## What is NocoBase
-NocoBase is a scalability-first, open-source no-code development platform.
+NocoBase is an extensibility-first, open-source no-code development platform.
Instead of investing years of time and millions of dollars in research and development, deploy NocoBase in a few minutes and you'll have a private, controllable, and extremely scalable no-code development platform!
Homepage:
@@ -29,6 +24,17 @@ https://docs.nocobase.com/
Forum:
https://forum.nocobase.com/
+Tutorials:
+https://www.nocobase.com/en/tutorials
+
+Use Cases:
+https://www.nocobase.com/en/blog/tags/customer-stories
+
+
+## Release Notes
+
+Our [blog](https://www.nocobase.com/en/blog/timeline) is regularly updated with release notes and provides a weekly summary.
+
## Distinctive features
### 1. Data model-driven
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 66e9c281d7..728695efc7 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -2,13 +2,10 @@
https://github.com/nocobase/nocobase/assets/1267426/29623e45-9a48-4598-bb9e-9dd173ade553
-## 感谢支持
+
-
-
-## 发布日志
-我们的[博客](https://www.nocobase.com/cn/blog/timeline)会及时更新发布日志,并每周进行汇总。
+
## NocoBase 是什么
@@ -27,6 +24,15 @@ https://docs-cn.nocobase.com/
社区:
https://forum.nocobase.com/
+教程:
+https://www.nocobase.com/cn/tutorials
+
+用户故事:
+https://www.nocobase.com/cn/blog/tags/customer-stories
+
+## 发布日志
+我们的[博客](https://www.nocobase.com/cn/blog/timeline)会及时更新发布日志,并每周进行汇总。
+
## 与众不同之处
### 1. 数据模型驱动
diff --git a/docker/nocobase-full/nocobase.conf b/docker/nocobase-full/nocobase.conf
index 61ddc70335..7de9bbb630 100644
--- a/docker/nocobase-full/nocobase.conf
+++ b/docker/nocobase-full/nocobase.conf
@@ -17,7 +17,7 @@ server {
server_name _;
root /app/nocobase/packages/app/client/dist;
index index.html;
- client_max_body_size 20M;
+ client_max_body_size 0;
access_log /var/log/nginx/nocobase.log apm;
diff --git a/docker/nocobase/Dockerfile b/docker/nocobase/Dockerfile
index 5de0d6030a..9c0a1bbfff 100644
--- a/docker/nocobase/Dockerfile
+++ b/docker/nocobase/Dockerfile
@@ -6,9 +6,13 @@ WORKDIR /app
RUN cd /app \
&& yarn config set network-timeout 600000 -g \
- && npx -y create-nocobase-app@${CNA_VERSION} my-nocobase-app -a -e APP_ENV=production \
+ && npx -y create-nocobase-app@${CNA_VERSION} my-nocobase-app --skip-dev-dependencies -a -e APP_ENV=production \
&& cd /app/my-nocobase-app \
- && yarn install --production
+ && yarn install --production \
+ && rm -rf yarn.lock \
+ && find node_modules -type f -name "yarn.lock" -delete \
+ && find node_modules -type f -name "bower.json" -delete \
+ && find node_modules -type f -name "composer.json" -delete
RUN cd /app \
&& rm -rf nocobase.tar.gz \
diff --git a/docker/nocobase/nocobase.conf b/docker/nocobase/nocobase.conf
index 7c0c4d2c41..d178440bf9 100644
--- a/docker/nocobase/nocobase.conf
+++ b/docker/nocobase/nocobase.conf
@@ -17,7 +17,7 @@ server {
server_name _;
root /app/nocobase/node_modules/@nocobase/app/dist/client;
index index.html;
- client_max_body_size 1000M;
+ client_max_body_size 0;
access_log /var/log/nginx/nocobase.log apm;
gzip on;
diff --git a/lerna.json b/lerna.json
index b15df1c889..9486c51939 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "1.6.0-beta.19",
+ "version": "1.7.0-beta.26",
"npmClient": "yarn",
"useWorkspaces": true,
"npmClientArgs": ["--ignore-engines"],
diff --git a/package.json b/package.json
index 02fefd9951..2520b58a91 100644
--- a/package.json
+++ b/package.json
@@ -52,7 +52,9 @@
"react": "^18.0.0",
"react-dom": "^18.0.0",
"nwsapi": "2.2.7",
- "antd": "5.12.8",
+ "antd": "5.24.2",
+ "@formily/antd-v5": "1.2.3",
+ "dayjs": "1.11.13",
"@ant-design/icons": "^5.6.1"
},
"config": {
@@ -81,6 +83,7 @@
"ghooks": "^2.0.4",
"lint-staged": "^13.2.3",
"patch-package": "^8.0.0",
+ "pm2": "^6.0.5",
"pretty-format": "^24.0.0",
"pretty-quick": "^3.1.0",
"react": "^18.0.0",
@@ -93,4 +96,4 @@
"yarn": "1.22.19"
},
"dependencies": {}
-}
+}
\ No newline at end of file
diff --git a/packages/core/acl/package.json b/packages/core/acl/package.json
index f3a025fe1b..3a0f35febf 100644
--- a/packages/core/acl/package.json
+++ b/packages/core/acl/package.json
@@ -1,13 +1,13 @@
{
"name": "@nocobase/acl",
- "version": "1.6.0-beta.19",
+ "version": "1.7.0-beta.26",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
- "@nocobase/resourcer": "1.6.0-beta.19",
- "@nocobase/utils": "1.6.0-beta.19",
+ "@nocobase/resourcer": "1.7.0-beta.26",
+ "@nocobase/utils": "1.7.0-beta.26",
"minimatch": "^5.1.1"
},
"repository": {
diff --git a/packages/core/acl/src/__tests__/acl-role.test.ts b/packages/core/acl/src/__tests__/acl-role.test.ts
new file mode 100644
index 0000000000..88b914361e
--- /dev/null
+++ b/packages/core/acl/src/__tests__/acl-role.test.ts
@@ -0,0 +1,579 @@
+/**
+ * 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 { ACL } from '..';
+describe('multiple roles merge', () => {
+ let acl: ACL;
+ beforeEach(() => {
+ acl = new ACL();
+ });
+ describe('filter merge', () => {
+ test('should allow all(params:{}) when filter1 = undefined, filter2 is not exists', () => {
+ acl.setAvailableAction('edit', {
+ type: 'old-data',
+ });
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:view': {
+ filter: undefined,
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {},
+ });
+ const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'view',
+ params: {},
+ });
+ });
+ test('should allow all(params:{}) when filter1 = undefined, filter2 = {}', () => {
+ acl.setAvailableAction('edit', {
+ type: 'old-data',
+ });
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:view': {
+ filter: undefined,
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:view': {
+ filter: {},
+ },
+ },
+ });
+ const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'view',
+ params: {},
+ });
+ });
+ test('should allow all(params={}) when filter1 = {}, filter2 = {}', () => {
+ acl.setAvailableAction('edit', {
+ type: 'old-data',
+ });
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:view': {
+ filter: {},
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:view': {
+ filter: {},
+ },
+ },
+ });
+ const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'view',
+ params: {},
+ });
+ });
+ test('should union filter(params.filter={$or:[{id:1}, {id:2}]}) when filter1 = {id: 1}, filter2 = {id: 2}', () => {
+ acl.setAvailableAction('edit', {
+ type: 'old-data',
+ });
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:view': {
+ filter: { id: 1 },
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:view': {
+ filter: { id: 2 },
+ },
+ },
+ });
+ const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'view',
+ params: {
+ filter: {
+ $or: expect.arrayContaining([{ id: 1 }, { id: 2 }]),
+ },
+ },
+ });
+ });
+
+ test('should union filter(filter={$or:[{id:1}, {name: zhangsan}]}) when filter1 = {id: 1}, filter2 = {name: zhangsan}', () => {
+ acl.setAvailableAction('edit', {
+ type: 'old-data',
+ });
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:view': {
+ filter: { id: 1 },
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:view': {
+ filter: { name: 'zhangsan' },
+ },
+ },
+ });
+ const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'view',
+ params: {
+ filter: {
+ $or: expect.arrayContaining([{ id: 1 }, { name: 'zhangsan' }]),
+ },
+ },
+ });
+ });
+
+ test('should union filter(filter={$or:[{id:1}, {name: zhangsan}]}) when filter1 = {id: 1}, filter2 = { $or: [{name: zhangsan}]', () => {
+ acl.setAvailableAction('edit', {
+ type: 'old-data',
+ });
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:view': {
+ filter: { id: 1 },
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:view': {
+ filter: { name: 'zhangsan' },
+ },
+ },
+ });
+ const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'view',
+ params: {
+ filter: {
+ $or: expect.arrayContaining([{ id: 1 }, { name: 'zhangsan' }]),
+ },
+ },
+ });
+ });
+ });
+
+ describe('feilds merge', () => {
+ test('should allow all(params={}) when fields1 = undefined, fields2 is not exists', () => {
+ acl.setAvailableAction('edit', {
+ type: 'old-data',
+ });
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:view': {
+ fields: [],
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:view': {},
+ },
+ });
+ const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'view',
+ params: {},
+ });
+ });
+ test('should allow all(params={}) when fields1 = undefined, fields2 is not exists', () => {
+ acl.setAvailableAction('edit', {
+ type: 'old-data',
+ });
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:view': {
+ fields: undefined,
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:view': {},
+ },
+ });
+ const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'view',
+ params: {},
+ });
+ });
+ test('should allow all(params={}) when fields1 = [], fields2 =[]', () => {
+ acl.setAvailableAction('edit', {
+ type: 'old-data',
+ });
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:view': {
+ fields: [],
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:view': {
+ fields: [],
+ },
+ },
+ });
+ const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'view',
+ params: {},
+ });
+ });
+ test('should union fields(params={ fields: [a,b]}) when fields1 = [a], fields2 =[b]', () => {
+ acl.setAvailableAction('edit', {
+ type: 'old-data',
+ });
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:view': {
+ fields: ['a'],
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:view': {
+ fields: ['b'],
+ },
+ },
+ });
+ const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'view',
+ params: {
+ fields: expect.arrayContaining(['a', 'b']),
+ },
+ });
+ });
+ test('should union no repeat fields(params={ fields: [a,b,c]}) when fields1 = [a,b], fields2 =[b,c]', () => {
+ acl.setAvailableAction('edit', {
+ type: 'old-data',
+ });
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:view': {
+ fields: ['a', 'b'],
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:view': {
+ fields: ['b', 'c'],
+ },
+ },
+ });
+ const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'view',
+ params: {
+ fields: expect.arrayContaining(['a', 'b', 'c']),
+ },
+ });
+ expect(canResult.params.fields.length).toStrictEqual(3);
+ });
+ });
+
+ describe('whitelist', () => {
+ test('should union whitelist(params={ fields: [a,b,c]}) when fields1 = [a,b], fields2 =[c]', () => {
+ acl.setAvailableAction('update');
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:update': {
+ whitelist: ['a', 'b'],
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:update': {
+ whitelist: ['c'],
+ },
+ },
+ });
+ const canResult = acl.can({ resource: 'posts', action: 'update', roles: ['role1', 'role2'] });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'update',
+ params: {
+ whitelist: expect.arrayContaining(['a', 'b', 'c']),
+ },
+ });
+ });
+ });
+
+ describe('appends', () => {
+ test('should union appends(params={ appends: [a,b,c]}) when appends = [a,b], appends =[c]', () => {
+ acl.setAvailableAction('update');
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:update': {
+ appends: ['a', 'b'],
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:update': {
+ appends: ['c'],
+ },
+ },
+ });
+ const canResult = acl.can({ resource: 'posts', action: 'update', roles: ['role1', 'role2'] });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'update',
+ params: {
+ appends: expect.arrayContaining(['a', 'b', 'c']),
+ },
+ });
+ });
+ test('should union appends(params={ appends: [a,b]}) when appends = [a,b], appends =[]', () => {
+ acl.setAvailableAction('update');
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:update': {
+ appends: ['a', 'b'],
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:update': {
+ appends: [],
+ },
+ },
+ });
+ const canResult = acl.can({ resource: 'posts', action: 'update', roles: ['role1', 'role2'] });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'update',
+ params: {
+ appends: expect.arrayContaining(['a', 'b']),
+ },
+ });
+ });
+ });
+
+ describe('filter & fields merge', () => {
+ test('should allow all(params={}) when actions1 = {filter: {}}, actions2 = {fields: []}', () => {
+ acl.setAvailableAction('view', {
+ type: 'old-data',
+ });
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:view': {
+ filter: {},
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:view': {
+ fields: [],
+ },
+ },
+ });
+ const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'view',
+ params: {},
+ });
+ });
+ test('should allow all(params={}) when actions1 = {filter: {}}, actions2 = {fields: [a]}', () => {
+ acl.setAvailableAction('view', {
+ type: 'old-data',
+ });
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:view': {
+ filter: {},
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:view': {
+ fields: ['a'],
+ },
+ },
+ });
+ const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'view',
+ params: {},
+ });
+ });
+ test('should allow all(params={}) when actions1 = {filter: {a:1}}, actions2 = {fields: []}', () => {
+ acl.setAvailableAction('view', {
+ type: 'old-data',
+ });
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:view': {
+ filter: { a: 1 },
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:view': {
+ fields: [],
+ },
+ },
+ });
+ const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'view',
+ params: {},
+ });
+ });
+
+ test('should allow all(params={}) when actions1 = {filter: {a:1}}, actions2 = {fields: [a]}', () => {
+ acl.setAvailableAction('view', {
+ type: 'old-data',
+ });
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:view': {
+ filter: { a: 1 },
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:view': {
+ fields: ['a'],
+ },
+ },
+ });
+ const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'view',
+ params: {},
+ });
+ });
+
+ test('should union filter&fields(params={ filter:{ $or:[{a:1},{a:2}]}, fields:[a,b]}) when actions1={filter:{a:1}, fields:[a]}, actions2={filter: {a:1}},fields:[b]}', () => {
+ acl.setAvailableAction('view', {
+ type: 'old-data',
+ });
+ acl.define({
+ role: 'role1',
+ actions: {
+ 'posts:view': {
+ filter: { a: 1 },
+ fields: ['a'],
+ },
+ },
+ });
+ acl.define({
+ role: 'role2',
+ actions: {
+ 'posts:view': {
+ filter: { a: 2 },
+ fields: ['b'],
+ },
+ },
+ });
+ const canResult = acl.can({ roles: ['role1', 'role2'], resource: 'posts', action: 'view' });
+ expect(canResult).toStrictEqual({
+ role: 'role1',
+ resource: 'posts',
+ action: 'view',
+ params: expect.objectContaining({
+ filter: { $or: expect.arrayContaining([{ a: 1 }, { a: 2 }]) },
+ fields: expect.arrayContaining(['a', 'b']),
+ }),
+ });
+ });
+ });
+});
diff --git a/packages/core/acl/src/acl-role.ts b/packages/core/acl/src/acl-role.ts
index e2f4b53a03..a430a2033a 100644
--- a/packages/core/acl/src/acl-role.ts
+++ b/packages/core/acl/src/acl-role.ts
@@ -7,11 +7,11 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
+import { default as _, default as lodash } from 'lodash';
+import minimatch from 'minimatch';
import { ACL, DefineOptions } from './acl';
import { ACLAvailableStrategy, AvailableStrategyOptions } from './acl-available-strategy';
import { ACLResource } from './acl-resource';
-import lodash from 'lodash';
-import minimatch from 'minimatch';
export interface RoleActionParams {
fields?: string[];
@@ -185,12 +185,12 @@ export class ACLRole {
}
}
- return {
+ return _.cloneDeep({
role: this.name,
strategy: this.strategy,
actions,
snippets: Array.from(this.snippets),
- };
+ });
}
protected getResourceActionFromPath(path: string) {
diff --git a/packages/core/acl/src/acl.ts b/packages/core/acl/src/acl.ts
index 00e86e5ceb..1c2ef37895 100644
--- a/packages/core/acl/src/acl.ts
+++ b/packages/core/acl/src/acl.ts
@@ -19,6 +19,7 @@ import { AllowManager, ConditionFunc } from './allow-manager';
import FixedParamsManager, { Merger } from './fixed-params-manager';
import SnippetManager, { SnippetOptions } from './snippet-manager';
import { NoPermissionError } from './errors/no-permission-error';
+import { mergeAclActionParams, removeEmptyParams } from './utils';
interface CanResult {
role: string;
@@ -54,11 +55,12 @@ export interface ListenerContext {
type Listener = (ctx: ListenerContext) => void;
interface CanArgs {
- role: string;
+ role?: string;
resource: string;
action: string;
rawResourceName?: string;
ctx?: any;
+ roles?: string[];
}
export class ACL extends EventEmitter {
@@ -169,6 +171,10 @@ export class ACL extends EventEmitter {
return this.roles.get(name);
}
+ getRoles(names: string[]): ACLRole[] {
+ return names.map((name) => this.getRole(name)).filter((x) => Boolean(x));
+ }
+
removeRole(name: string) {
return this.roles.delete(name);
}
@@ -202,6 +208,36 @@ export class ACL extends EventEmitter {
}
can(options: CanArgs): CanResult | null {
+ if (options.role) {
+ return lodash.cloneDeep(this.getCanByRole(options));
+ }
+ if (options.roles?.length) {
+ return lodash.cloneDeep(this.getCanByRoles(options));
+ }
+
+ return null;
+ }
+
+ private getCanByRoles(options: CanArgs) {
+ let canResult: CanResult | null = null;
+
+ for (const role of options.roles) {
+ const result = this.getCanByRole({
+ role,
+ ...options,
+ });
+ if (!canResult) {
+ canResult = result;
+ canResult && removeEmptyParams(canResult.params);
+ } else if (canResult && result) {
+ canResult.params = mergeAclActionParams(canResult.params, result.params);
+ }
+ }
+
+ return canResult;
+ }
+
+ private getCanByRole(options: CanArgs) {
const { role, resource, action, rawResourceName } = options;
const aclRole = this.roles.get(role);
@@ -351,9 +387,12 @@ export class ACL extends EventEmitter {
}
ctx.can = (options: Omit) => {
- const canResult = acl.can({ role: roleName, ...options });
-
- return canResult;
+ const roles = ctx.state.currentRoles || [roleName];
+ const can = acl.can({ roles, ...options });
+ if (!can) {
+ return null;
+ }
+ return can;
};
ctx.permission = {
@@ -370,7 +409,7 @@ export class ACL extends EventEmitter {
* @internal
*/
async getActionParams(ctx) {
- const roleName = ctx.state.currentRole || 'anonymous';
+ const roleNames = ctx.state.currentRoles?.length ? ctx.state.currentRoles : 'anonymous';
const { resourceName: rawResourceName, actionName } = ctx.action;
let resourceName = rawResourceName;
@@ -386,11 +425,11 @@ export class ACL extends EventEmitter {
}
ctx.can = (options: Omit) => {
- const can = this.can({ role: roleName, ...options });
- if (!can) {
- return null;
+ const can = this.can({ roles: roleNames, ...options });
+ if (can) {
+ return lodash.cloneDeep(can);
}
- return lodash.cloneDeep(can);
+ return null;
};
ctx.permission = {
@@ -421,6 +460,23 @@ export class ACL extends EventEmitter {
}
}
+ // 检查 $or 条件中的 createdById
+ if (params?.filter?.$or?.length) {
+ const checkCreatedById = (items) => {
+ return items.some(
+ (x) =>
+ 'createdById' in x || x.$or?.some((y) => 'createdById' in y) || x.$and?.some((y) => 'createdById' in y),
+ );
+ };
+
+ if (checkCreatedById(params.filter.$or)) {
+ const collection = ctx.db.getCollection(resourceName);
+ if (!collection || !collection.getField('createdById')) {
+ throw new NoPermissionError('createdById field not found');
+ }
+ }
+ }
+
return params;
}
diff --git a/packages/core/acl/src/index.ts b/packages/core/acl/src/index.ts
index 269a8f6678..7def1f2dca 100644
--- a/packages/core/acl/src/index.ts
+++ b/packages/core/acl/src/index.ts
@@ -14,3 +14,4 @@ export * from './acl-resource';
export * from './acl-role';
export * from './skip-middleware';
export * from './errors';
+export * from './utils';
diff --git a/packages/core/acl/src/utils/acl-role.ts b/packages/core/acl/src/utils/acl-role.ts
new file mode 100644
index 0000000000..8d3be6ba97
--- /dev/null
+++ b/packages/core/acl/src/utils/acl-role.ts
@@ -0,0 +1,269 @@
+/**
+ * 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 { assign } from '@nocobase/utils';
+import _ from 'lodash';
+import { ACLRole } from '../acl-role';
+
+export function mergeRole(roles: ACLRole[]) {
+ const result: Record = {
+ roles: [],
+ strategy: {},
+ actions: null,
+ snippets: [],
+ resources: null,
+ };
+ const allSnippets: string[][] = [];
+ for (const role of roles) {
+ const jsonRole = role.toJSON();
+ result.roles = mergeRoleNames(result.roles, jsonRole.role);
+ result.strategy = mergeRoleStrategy(result.strategy, jsonRole.strategy);
+ result.actions = mergeRoleActions(result.actions, jsonRole.actions);
+ result.resources = mergeRoleResources(result.resources, [...role.resources.keys()]);
+ if (_.isArray(jsonRole.snippets)) {
+ allSnippets.push(jsonRole.snippets);
+ }
+ }
+ result.snippets = mergeRoleSnippets(allSnippets);
+ adjustActionByStrategy(roles, result);
+ return result;
+}
+
+/**
+ * When merging permissions from multiple roles, if strategy.actions allows certain actions, then those actions have higher priority.
+ * For example, [
+ * {
+ * actions: {
+ * 'users:view': {...},
+ * 'users:create': {...}
+ * },
+ * strategy: {
+ * actions: ['view']
+ * }
+ * }]
+ * finally result: [{
+ * actions: {
+ * 'users:create': {...},
+ * 'users:view': {} // all view
+ * },
+ * {
+ * strategy: {
+ * actions: ['view']
+ * }]
+ **/
+function adjustActionByStrategy(
+ roles,
+ result: {
+ actions?: Record;
+ strategy?: { actions?: string[] };
+ resources?: string[];
+ },
+) {
+ const { actions, strategy } = result;
+ const actionSet = getAdjustActions(roles);
+ if (!_.isEmpty(actions) && !_.isEmpty(strategy?.actions) && !_.isEmpty(result.resources)) {
+ for (const resource of result.resources) {
+ for (const action of strategy.actions) {
+ if (actionSet.has(action)) {
+ actions[`${resource}:${action}`] = {};
+ }
+ }
+ }
+ }
+}
+
+function getAdjustActions(roles: ACLRole[]) {
+ const actionSet = new Set();
+ for (const role of roles) {
+ const jsonRole = role.toJSON();
+ // Within the same role, actions have higher priority than strategy.actions.
+ if (!_.isEmpty(jsonRole.strategy?.['actions']) && _.isEmpty(jsonRole.actions)) {
+ jsonRole.strategy['actions'].forEach((x) => !x.includes('own') && actionSet.add(x));
+ }
+ }
+ return actionSet;
+}
+
+function mergeRoleNames(sourceRoleNames, newRoleName) {
+ return newRoleName ? sourceRoleNames.concat(newRoleName) : sourceRoleNames;
+}
+
+function mergeRoleStrategy(sourceStrategy, newStrategy) {
+ if (!newStrategy) {
+ return sourceStrategy;
+ }
+ if (_.isArray(newStrategy.actions)) {
+ if (!sourceStrategy.actions) {
+ sourceStrategy.actions = newStrategy.actions;
+ } else {
+ const actions = sourceStrategy.actions.concat(newStrategy.actions);
+ return {
+ ...sourceStrategy,
+ actions: [...new Set(actions)],
+ };
+ }
+ }
+ return sourceStrategy;
+}
+
+function mergeRoleActions(sourceActions, newActions) {
+ if (_.isEmpty(sourceActions)) return newActions;
+ if (_.isEmpty(newActions)) return sourceActions;
+
+ const result = {};
+ [...new Set(Reflect.ownKeys(sourceActions).concat(Reflect.ownKeys(newActions)))].forEach((key) => {
+ if (_.has(sourceActions, key) && _.has(newActions, key)) {
+ result[key] = mergeAclActionParams(sourceActions[key], newActions[key]);
+ return;
+ }
+ result[key] = _.has(sourceActions, key) ? sourceActions[key] : newActions[key];
+ });
+
+ return result;
+}
+
+function mergeRoleSnippets(allRoleSnippets: string[][]): string[] {
+ if (!allRoleSnippets.length) {
+ return [];
+ }
+
+ const allSnippets = allRoleSnippets.flat();
+ const isExclusion = (value) => value.startsWith('!');
+ const includes = new Set(allSnippets.filter((x) => !isExclusion(x)));
+ const excludes = new Set(allSnippets.filter(isExclusion));
+
+ // 统计 xxx.* 在多少个角色中存在
+ const domainRoleMap = new Map>();
+ allRoleSnippets.forEach((roleSnippets, i) => {
+ roleSnippets
+ .filter((x) => x.endsWith('.*') && !isExclusion(x))
+ .forEach((include) => {
+ const domain = include.slice(0, -1);
+ if (!domainRoleMap.has(domain)) {
+ domainRoleMap.set(domain, new Set());
+ }
+ domainRoleMap.get(domain).add(i);
+ });
+ });
+
+ // 处理黑名单交集(只有所有角色都有 `!xxx` 才保留)
+ const excludesSet = new Set();
+ for (const snippet of excludes) {
+ if (allRoleSnippets.every((x) => x.includes(snippet))) {
+ excludesSet.add(snippet);
+ }
+ }
+
+ for (const [domain, indexes] of domainRoleMap.entries()) {
+ const fullDomain = `${domain}.*`;
+
+ // xxx.* 存在时,覆盖 !xxx.*
+ if (includes.has(fullDomain)) {
+ excludesSet.delete(`!${fullDomain}`);
+ }
+
+ // 计算 !xxx.yyy,当所有 xxx.* 角色都包含 !xxx.yyy 时才保留
+ for (const roleIndex of indexes) {
+ for (const exclude of allRoleSnippets[roleIndex]) {
+ if (exclude.startsWith(`!${domain}`) && exclude !== `!${fullDomain}`) {
+ if ([...indexes].every((i) => allRoleSnippets[i].includes(exclude))) {
+ excludesSet.add(exclude);
+ }
+ }
+ }
+ }
+ }
+
+ // 确保 !xxx.yyy 只有在 xxx.* 存在时才有效,同时解决 [xxx] 和 [!xxx] 冲突
+ if (includes.size > 0) {
+ for (const x of [...excludesSet]) {
+ const exactMatch = x.slice(1);
+ const segments = exactMatch.split('.');
+ if (segments.length > 1 && segments[1] !== '*') {
+ const parentDomain = segments[0] + '.*';
+ if (!includes.has(parentDomain)) {
+ excludesSet.delete(x);
+ }
+ }
+ }
+ }
+
+ return [...includes, ...excludesSet];
+}
+
+function mergeRoleResources(sourceResources, newResources) {
+ if (sourceResources === null) {
+ return newResources;
+ }
+
+ return [...new Set(sourceResources.concat(newResources))];
+}
+
+export function mergeAclActionParams(sourceParams, targetParams) {
+ if (_.isEmpty(sourceParams) || _.isEmpty(targetParams)) {
+ return {};
+ }
+
+ // source 和 target 其中之一没有 fields 字段时, 最终希望没有此字段
+ removeUnmatchedParams(sourceParams, targetParams, ['fields', 'whitelist', 'appends']);
+
+ const andMerge = (x, y) => {
+ if (_.isEmpty(x) || _.isEmpty(y)) {
+ return [];
+ }
+ return _.uniq(x.concat(y)).filter(Boolean);
+ };
+
+ const mergedParams = assign(targetParams, sourceParams, {
+ own: (x, y) => x || y,
+ filter: (x, y) => {
+ if (_.isEmpty(x) || _.isEmpty(y)) {
+ return {};
+ }
+ const xHasOr = _.has(x, '$or'),
+ yHasOr = _.has(y, '$or');
+ let $or = [x, y];
+ if (xHasOr && !yHasOr) {
+ $or = [...x.$or, y];
+ } else if (!xHasOr && yHasOr) {
+ $or = [x, ...y.$or];
+ } else if (xHasOr && yHasOr) {
+ $or = [...x.$or, ...y.$or];
+ }
+
+ return { $or: _.uniqWith($or, _.isEqual) };
+ },
+ fields: andMerge,
+ whitelist: andMerge,
+ appends: 'union',
+ });
+ removeEmptyParams(mergedParams);
+ return mergedParams;
+}
+
+export function removeEmptyParams(params) {
+ if (!_.isObject(params)) {
+ return;
+ }
+ Object.keys(params).forEach((key) => {
+ if (_.isEmpty(params[key])) {
+ delete params[key];
+ }
+ });
+}
+
+function removeUnmatchedParams(source, target, keys: string[]) {
+ for (const key of keys) {
+ if (_.has(source, key) && !_.has(target, key)) {
+ delete source[key];
+ }
+ if (!_.has(source, key) && _.has(target, key)) {
+ delete target[key];
+ }
+ }
+}
diff --git a/packages/core/acl/src/utils/index.ts b/packages/core/acl/src/utils/index.ts
new file mode 100644
index 0000000000..48071b231d
--- /dev/null
+++ b/packages/core/acl/src/utils/index.ts
@@ -0,0 +1,10 @@
+/**
+ * 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 * from './acl-role';
diff --git a/packages/core/actions/package.json b/packages/core/actions/package.json
index a083426ea2..93e5f9b492 100644
--- a/packages/core/actions/package.json
+++ b/packages/core/actions/package.json
@@ -1,14 +1,14 @@
{
"name": "@nocobase/actions",
- "version": "1.6.0-beta.19",
+ "version": "1.7.0-beta.26",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
- "@nocobase/cache": "1.6.0-beta.19",
- "@nocobase/database": "1.6.0-beta.19",
- "@nocobase/resourcer": "1.6.0-beta.19"
+ "@nocobase/cache": "1.7.0-beta.26",
+ "@nocobase/database": "1.7.0-beta.26",
+ "@nocobase/resourcer": "1.7.0-beta.26"
},
"repository": {
"type": "git",
diff --git a/packages/core/app/package.json b/packages/core/app/package.json
index a472ea6272..876d992f89 100644
--- a/packages/core/app/package.json
+++ b/packages/core/app/package.json
@@ -1,17 +1,17 @@
{
"name": "@nocobase/app",
- "version": "1.6.0-beta.19",
+ "version": "1.7.0-beta.26",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
- "@nocobase/database": "1.6.0-beta.19",
- "@nocobase/preset-nocobase": "1.6.0-beta.19",
- "@nocobase/server": "1.6.0-beta.19"
+ "@nocobase/database": "1.7.0-beta.26",
+ "@nocobase/preset-nocobase": "1.7.0-beta.26",
+ "@nocobase/server": "1.7.0-beta.26"
},
"devDependencies": {
- "@nocobase/client": "1.6.0-beta.19"
+ "@nocobase/client": "1.7.0-beta.26"
},
"repository": {
"type": "git",
diff --git a/packages/core/app/src/__tests__/commands.test.ts b/packages/core/app/src/__tests__/commands.test.ts
index f300d4cbda..946df74d9c 100644
--- a/packages/core/app/src/__tests__/commands.test.ts
+++ b/packages/core/app/src/__tests__/commands.test.ts
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import { mockDatabase } from '@nocobase/database';
+import { createMockDatabase, mockDatabase } from '@nocobase/database';
import { uid } from '@nocobase/utils';
import axios from 'axios';
import execa from 'execa';
@@ -64,7 +64,7 @@ const createDatabase = async () => {
if (process.env.DB_DIALECT === 'sqlite') {
return 'nocobase';
}
- const db = mockDatabase();
+ const db = await createMockDatabase();
const name = `d_${uid()}`;
await db.sequelize.query(`CREATE DATABASE ${name}`);
await db.close();
diff --git a/packages/core/auth/package.json b/packages/core/auth/package.json
index 055f8eafe4..942e6a9b9a 100644
--- a/packages/core/auth/package.json
+++ b/packages/core/auth/package.json
@@ -1,18 +1,18 @@
{
"name": "@nocobase/auth",
- "version": "1.6.0-beta.19",
+ "version": "1.7.0-beta.26",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
- "@nocobase/actions": "1.6.0-beta.19",
- "@nocobase/cache": "1.6.0-beta.19",
- "@nocobase/database": "1.6.0-beta.19",
- "@nocobase/resourcer": "1.6.0-beta.19",
- "@nocobase/utils": "1.6.0-beta.19",
- "@types/jsonwebtoken": "^8.5.8",
- "jsonwebtoken": "^8.5.1"
+ "@nocobase/actions": "1.7.0-beta.26",
+ "@nocobase/cache": "1.7.0-beta.26",
+ "@nocobase/database": "1.7.0-beta.26",
+ "@nocobase/resourcer": "1.7.0-beta.26",
+ "@nocobase/utils": "1.7.0-beta.26",
+ "@types/jsonwebtoken": "^9.0.9",
+ "jsonwebtoken": "^9.0.2"
},
"repository": {
"type": "git",
diff --git a/packages/core/auth/src/__tests__/middleware.test.ts b/packages/core/auth/src/__tests__/middleware.test.ts
index e4640e96d2..440b14c6c0 100644
--- a/packages/core/auth/src/__tests__/middleware.test.ts
+++ b/packages/core/auth/src/__tests__/middleware.test.ts
@@ -22,7 +22,7 @@ describe('middleware', () => {
app = await createMockServer({
registerActions: true,
acl: true,
- plugins: ['users', 'auth', 'acl', 'field-sort', 'data-source-manager', 'error-handler'],
+ plugins: ['users', 'auth', 'acl', 'field-sort', 'data-source-manager', 'error-handler', 'system-settings'],
});
// app.plugin(ApiKeysPlugin);
diff --git a/packages/core/auth/src/base/auth.ts b/packages/core/auth/src/base/auth.ts
index 913fcd5615..0568f4bbd0 100644
--- a/packages/core/auth/src/base/auth.ts
+++ b/packages/core/auth/src/base/auth.ts
@@ -268,6 +268,24 @@ export class BaseAuth extends Auth {
return null;
}
+ async signNewToken(userId: number) {
+ const tokenInfo = await this.tokenController.add({ userId });
+ const expiresIn = Math.floor((await this.tokenController.getConfig()).tokenExpirationTime / 1000);
+ const token = this.jwt.sign(
+ {
+ userId,
+ temp: true,
+ iat: Math.floor(tokenInfo.issuedTime / 1000),
+ signInTime: tokenInfo.signInTime,
+ },
+ {
+ jwtid: tokenInfo.jti,
+ expiresIn,
+ },
+ );
+ return token;
+ }
+
async signIn() {
let user: Model;
try {
@@ -283,20 +301,7 @@ export class BaseAuth extends Auth {
code: AuthErrorCode.NOT_EXIST_USER,
});
}
- const tokenInfo = await this.tokenController.add({ userId: user.id });
- const expiresIn = Math.floor((await this.tokenController.getConfig()).tokenExpirationTime / 1000);
- const token = this.jwt.sign(
- {
- userId: user.id,
- temp: true,
- iat: Math.floor(tokenInfo.issuedTime / 1000),
- signInTime: tokenInfo.signInTime,
- },
- {
- jwtid: tokenInfo.jti,
- expiresIn,
- },
- );
+ const token = await this.signNewToken(user.id);
return {
user,
token,
diff --git a/packages/core/build/package.json b/packages/core/build/package.json
index 5c3f299ae2..5215de0e1f 100644
--- a/packages/core/build/package.json
+++ b/packages/core/build/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/build",
- "version": "1.6.0-beta.19",
+ "version": "1.7.0-beta.26",
"description": "Library build tool based on rollup.",
"main": "lib/index.js",
"types": "./lib/index.d.ts",
@@ -17,7 +17,7 @@
"@lerna/project": "4.0.0",
"@rsbuild/plugin-babel": "^1.0.3",
"@rsdoctor/rspack-plugin": "^0.4.8",
- "@rspack/core": "1.1.1",
+ "@rspack/core": "1.3.2",
"@svgr/webpack": "^8.1.0",
"@types/gulp": "^4.0.13",
"@types/lerna__package": "5.1.0",
@@ -39,7 +39,7 @@
"postcss-preset-env": "^9.1.2",
"react-imported-component": "^6.5.4",
"style-loader": "^3.3.3",
- "tar": "^6.2.0",
+ "tar": "^7.4.3",
"tsup": "8.2.4",
"typescript": "5.1.3",
"update-notifier": "3.0.0",
diff --git a/packages/core/build/src/buildPlugin.ts b/packages/core/build/src/buildPlugin.ts
index f851f37ed8..9b7f228072 100644
--- a/packages/core/build/src/buildPlugin.ts
+++ b/packages/core/build/src/buildPlugin.ts
@@ -347,6 +347,7 @@ export async function buildPluginClient(cwd: string, userConfig: UserConfig, sou
umdNamedDefine: true,
},
},
+ amd: {},
resolve: {
tsConfig: path.join(process.cwd(), 'tsconfig.json'),
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.less', '.css'],
diff --git a/packages/core/build/src/tarPlugin.ts b/packages/core/build/src/tarPlugin.ts
index 1d58224924..35b107e5aa 100644
--- a/packages/core/build/src/tarPlugin.ts
+++ b/packages/core/build/src/tarPlugin.ts
@@ -8,7 +8,7 @@
*/
import path from 'path';
-import tar from 'tar';
+import { create } from 'tar';
import fg from 'fast-glob';
import fs from 'fs-extra';
@@ -38,5 +38,5 @@ export function tarPlugin(cwd: string, log: PkgLog) {
fs.mkdirpSync(path.dirname(tarball));
fs.rmSync(tarball, { force: true });
- return tar.c({ gzip: true, file: tarball, cwd }, tarFiles);
+ return create({ gzip: true, file: tarball, cwd }, tarFiles);
}
diff --git a/packages/core/cache/package.json b/packages/core/cache/package.json
index 8feda49715..809cbbd222 100644
--- a/packages/core/cache/package.json
+++ b/packages/core/cache/package.json
@@ -1,12 +1,12 @@
{
"name": "@nocobase/cache",
- "version": "1.6.0-beta.19",
+ "version": "1.7.0-beta.26",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
- "@nocobase/lock-manager": "1.6.0-alpha.6",
+ "@nocobase/lock-manager": "1.7.0-beta.26",
"bloom-filters": "^3.0.1",
"cache-manager": "^5.2.4",
"cache-manager-redis-yet": "^4.1.2"
diff --git a/packages/core/cli/nocobase.conf.tpl b/packages/core/cli/nocobase.conf.tpl
index a40343cc02..141ca74282 100644
--- a/packages/core/cli/nocobase.conf.tpl
+++ b/packages/core/cli/nocobase.conf.tpl
@@ -17,7 +17,7 @@ server {
server_name _;
root {{cwd}}/node_modules/@nocobase/app/dist/client;
index index.html;
- client_max_body_size 1000M;
+ client_max_body_size 0;
access_log /var/log/nginx/nocobase.log apm;
gzip on;
diff --git a/packages/core/cli/package.json b/packages/core/cli/package.json
index d4fe1fec1e..85d0d638f3 100644
--- a/packages/core/cli/package.json
+++ b/packages/core/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/cli",
- "version": "1.6.0-beta.19",
+ "version": "1.7.0-beta.26",
"description": "",
"license": "AGPL-3.0",
"main": "./src/index.js",
@@ -8,24 +8,25 @@
"nocobase": "./bin/index.js"
},
"dependencies": {
- "@nocobase/app": "1.6.0-beta.19",
+ "@nocobase/app": "1.7.0-beta.26",
"@types/fs-extra": "^11.0.1",
"@umijs/utils": "3.5.20",
"chalk": "^4.1.1",
"commander": "^9.2.0",
+ "deepmerge": "^4.3.1",
"dotenv": "^16.0.0",
"execa": "^5.1.1",
"fast-glob": "^3.3.1",
"fs-extra": "^11.1.1",
"p-all": "3.0.0",
- "pm2": "^5.2.0",
+ "pm2": "^6.0.5",
"portfinder": "^1.0.28",
- "serve": "^13.0.2",
+ "tar": "^7.4.3",
"tree-kill": "^1.2.2",
"tsx": "^4.19.0"
},
"devDependencies": {
- "@nocobase/devtools": "1.6.0-beta.19"
+ "@nocobase/devtools": "1.7.0-beta.26"
},
"repository": {
"type": "git",
diff --git a/packages/core/cli/src/commands/dev.js b/packages/core/cli/src/commands/dev.js
index c12c5df2d5..9caa52b0ff 100644
--- a/packages/core/cli/src/commands/dev.js
+++ b/packages/core/cli/src/commands/dev.js
@@ -8,7 +8,7 @@
*/
const _ = require('lodash');
const { Command } = require('commander');
-const { generatePlugins, run, postCheck, nodeCheck, promptForTs, isPortReachable } = require('../util');
+const { generatePlugins, run, postCheck, nodeCheck, promptForTs, isPortReachable, checkDBDialect } = require('../util');
const { getPortPromise } = require('portfinder');
const chokidar = require('chokidar');
const { uid } = require('@formily/shared');
@@ -36,6 +36,7 @@ module.exports = (cli) => {
.option('-i, --inspect [port]')
.allowUnknownOption()
.action(async (opts) => {
+ checkDBDialect();
let subprocess;
const runDevClient = () => {
console.log('starting client', 1 * clientPort);
diff --git a/packages/core/cli/src/commands/e2e.js b/packages/core/cli/src/commands/e2e.js
index 8daa5e6389..cf3bf6172a 100644
--- a/packages/core/cli/src/commands/e2e.js
+++ b/packages/core/cli/src/commands/e2e.js
@@ -8,7 +8,7 @@
*/
const { Command } = require('commander');
-const { run, isPortReachable } = require('../util');
+const { run, isPortReachable, checkDBDialect } = require('../util');
const { execSync } = require('node:child_process');
const axios = require('axios');
const { pTest } = require('./p-test');
@@ -165,6 +165,7 @@ const filterArgv = () => {
*/
module.exports = (cli) => {
const e2e = cli.command('e2e').hook('preAction', () => {
+ checkDBDialect();
if (process.env.APP_BASE_URL) {
process.env.APP_BASE_URL = process.env.APP_BASE_URL.replace('localhost', '127.0.0.1');
console.log('APP_BASE_URL:', process.env.APP_BASE_URL);
diff --git a/packages/core/cli/src/commands/global.js b/packages/core/cli/src/commands/global.js
index 524ad3cd09..48bab4fb23 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, downloadPro } = require('../util');
+const { run, isDev, isProd, promptForTs, downloadPro, checkDBDialect } = require('../util');
/**
*
@@ -21,6 +21,7 @@ module.exports = (cli) => {
.option('-h, --help')
.option('--ts-node-dev')
.action(async (options) => {
+ checkDBDialect();
const cmd = process.argv.slice(2)?.[0];
if (cmd === 'install') {
await downloadPro();
diff --git a/packages/core/cli/src/commands/index.js b/packages/core/cli/src/commands/index.js
index 45daf3c483..97a685f668 100644
--- a/packages/core/cli/src/commands/index.js
+++ b/packages/core/cli/src/commands/index.js
@@ -18,6 +18,7 @@ module.exports = (cli) => {
generateAppDir();
require('./global')(cli);
require('./create-nginx-conf')(cli);
+ require('./locale')(cli);
require('./build')(cli);
require('./tar')(cli);
require('./dev')(cli);
@@ -29,6 +30,7 @@ module.exports = (cli) => {
require('./test')(cli);
require('./test-coverage')(cli);
require('./umi')(cli);
+ require('./update-deps')(cli);
require('./upgrade')(cli);
require('./postinstall')(cli);
require('./pkg')(cli);
diff --git a/packages/core/cli/src/commands/locale.js b/packages/core/cli/src/commands/locale.js
new file mode 100644
index 0000000000..6a947d7167
--- /dev/null
+++ b/packages/core/cli/src/commands/locale.js
@@ -0,0 +1,81 @@
+/**
+ * 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 fg = require('fast-glob');
+const fs = require('fs-extra');
+const path = require('path');
+const _ = require('lodash');
+const deepmerge = require('deepmerge');
+const { getCronstrueLocale } = require('./locale/cronstrue');
+const { getReactJsCron } = require('./locale/react-js-cron');
+
+function sortJSON(json) {
+ if (Array.isArray(json)) {
+ return json.map(sortJSON);
+ } else if (typeof json === 'object' && json !== null) {
+ const sortedKeys = Object.keys(json).sort();
+ const sortedObject = {};
+ sortedKeys.forEach((key) => {
+ sortedObject[key] = sortJSON(json[key]);
+ });
+ return sortedObject;
+ }
+ return json;
+}
+
+/**
+ *
+ * @param {Command} cli
+ */
+module.exports = (cli) => {
+ const locale = cli.command('locale');
+ locale.command('generate').action(async (options) => {
+ const cwd = path.resolve(process.cwd(), 'node_modules', '@nocobase');
+ const files = await fg('./*/src/locale/*.json', {
+ cwd,
+ });
+ let locales = {};
+ await fs.mkdirp(path.resolve(process.cwd(), 'storage/locales'));
+ for (const file of files) {
+ const locale = path.basename(file, '.json');
+ const pkg = path.basename(path.dirname(path.dirname(path.dirname(file))));
+ _.set(locales, [locale.replace(/_/g, '-'), `@nocobase/${pkg}`], await fs.readJSON(path.resolve(cwd, file)));
+ if (locale.includes('_')) {
+ await fs.rename(
+ path.resolve(cwd, file),
+ path.resolve(cwd, path.dirname(file), `${locale.replace(/_/g, '-')}.json`),
+ );
+ }
+ }
+ const zhCN = locales['zh-CN'];
+ const enUS = locales['en-US'];
+ for (const key1 in zhCN) {
+ for (const key2 in zhCN[key1]) {
+ if (!_.get(enUS, [key1, key2])) {
+ _.set(enUS, [key1, key2], key2);
+ }
+ }
+ }
+ for (const locale of Object.keys(locales)) {
+ locales[locale] = deepmerge(enUS, locales[locale]);
+ locales[locale]['cronstrue'] = getCronstrueLocale(locale);
+ locales[locale]['react-js-cron'] = getReactJsCron(locale);
+ }
+ locales = sortJSON(locales);
+ for (const locale of Object.keys(locales)) {
+ await fs.writeFile(
+ path.resolve(process.cwd(), 'storage/locales', `${locale}.json`),
+ JSON.stringify(sortJSON(locales[locale]), null, 2),
+ );
+ }
+ });
+
+ locale.command('sync').action(async (options) => {});
+};
diff --git a/packages/core/cli/src/commands/locale/cronstrue.js b/packages/core/cli/src/commands/locale/cronstrue.js
new file mode 100644
index 0000000000..b59d41c8ea
--- /dev/null
+++ b/packages/core/cli/src/commands/locale/cronstrue.js
@@ -0,0 +1,122 @@
+/**
+ * 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 methods = [
+ 'atX0SecondsPastTheMinuteGt20',
+ 'atX0MinutesPastTheHourGt20',
+ 'commaMonthX0ThroughMonthX1',
+ 'commaYearX0ThroughYearX1',
+ 'use24HourTimeFormatByDefault',
+ 'anErrorOccuredWhenGeneratingTheExpressionD',
+ 'everyMinute',
+ 'everyHour',
+ 'atSpace',
+ 'everyMinuteBetweenX0AndX1',
+ 'at',
+ 'spaceAnd',
+ 'everySecond',
+ 'everyX0Seconds',
+ 'secondsX0ThroughX1PastTheMinute',
+ 'atX0SecondsPastTheMinute',
+ 'everyX0Minutes',
+ 'minutesX0ThroughX1PastTheHour',
+ 'atX0MinutesPastTheHour',
+ 'everyX0Hours',
+ 'betweenX0AndX1',
+ 'atX0',
+ 'commaEveryDay',
+ 'commaEveryX0DaysOfTheWeek',
+ 'commaX0ThroughX1',
+ 'commaAndX0ThroughX1',
+ 'first',
+ 'second',
+ 'third',
+ 'fourth',
+ 'fifth',
+ 'commaOnThe',
+ 'spaceX0OfTheMonth',
+ 'lastDay',
+ 'commaOnTheLastX0OfTheMonth',
+ 'commaOnlyOnX0',
+ 'commaAndOnX0',
+ 'commaEveryX0Months',
+ 'commaOnlyInX0',
+ 'commaOnTheLastDayOfTheMonth',
+ 'commaOnTheLastWeekdayOfTheMonth',
+ 'commaDaysBeforeTheLastDayOfTheMonth',
+ 'firstWeekday',
+ 'weekdayNearestDayX0',
+ 'commaOnTheX0OfTheMonth',
+ 'commaEveryX0Days',
+ 'commaBetweenDayX0AndX1OfTheMonth',
+ 'commaOnDayX0OfTheMonth',
+ 'commaEveryHour',
+ 'commaEveryX0Years',
+ 'commaStartingX0',
+ 'daysOfTheWeek',
+ 'monthsOfTheYear',
+];
+
+const langs = {
+ af: 'af',
+ ar: 'ar',
+ be: 'be',
+ ca: 'ca',
+ cs: 'cs',
+ da: 'da',
+ de: 'de',
+ 'en-US': 'en',
+ es: 'es',
+ fa: 'fa',
+ fi: 'fi',
+ fr: 'fr',
+ he: 'he',
+ hu: 'hu',
+ id: 'id',
+ it: 'it',
+ 'ja-JP': 'ja',
+ ko: 'ko',
+ nb: 'nb',
+ nl: 'nl',
+ pl: 'pl',
+ pt_BR: 'pt_BR',
+ pt_PT: 'pt_PT',
+ ro: 'ro',
+ 'ru-RU': 'ru',
+ sk: 'sk',
+ sl: 'sl',
+ sv: 'sv',
+ sw: 'sw',
+ 'th-TH': 'th',
+ 'tr-TR': 'tr',
+ uk: 'uk',
+ 'zh-CN': 'zh_CN',
+ 'zh-TW': 'zh_TW',
+};
+
+exports.getCronstrueLocale = (lang) => {
+ const lng = langs[lang] || 'en';
+ const Locale = require(`cronstrue/locales/${lng}`);
+ let locale;
+ if (Locale?.default) {
+ locale = Locale.default.locales[lng];
+ } else {
+ const L = Locale[lng];
+ locale = new L();
+ }
+ const items = {};
+ for (const method of methods) {
+ try {
+ items[method] = locale[method]();
+ } catch (error) {
+ // empty
+ }
+ }
+ return items;
+};
diff --git a/packages/core/cli/src/commands/locale/react-js-cron/en-US.json b/packages/core/cli/src/commands/locale/react-js-cron/en-US.json
new file mode 100644
index 0000000000..c518d108a4
--- /dev/null
+++ b/packages/core/cli/src/commands/locale/react-js-cron/en-US.json
@@ -0,0 +1,75 @@
+{
+ "everyText": "every",
+ "emptyMonths": "every month",
+ "emptyMonthDays": "every day of the month",
+ "emptyMonthDaysShort": "day of the month",
+ "emptyWeekDays": "every day of the week",
+ "emptyWeekDaysShort": "day of the week",
+ "emptyHours": "every hour",
+ "emptyMinutes": "every minute",
+ "emptyMinutesForHourPeriod": "every",
+ "yearOption": "year",
+ "monthOption": "month",
+ "weekOption": "week",
+ "dayOption": "day",
+ "hourOption": "hour",
+ "minuteOption": "minute",
+ "rebootOption": "reboot",
+ "prefixPeriod": "Every",
+ "prefixMonths": "in",
+ "prefixMonthDays": "on",
+ "prefixWeekDays": "on",
+ "prefixWeekDaysForMonthAndYearPeriod": "and",
+ "prefixHours": "at",
+ "prefixMinutes": ":",
+ "prefixMinutesForHourPeriod": "at",
+ "suffixMinutesForHourPeriod": "minute(s)",
+ "errorInvalidCron": "Invalid cron expression",
+ "clearButtonText": "Clear",
+ "weekDays": [
+ "Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday"
+ ],
+ "months": [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December"
+ ],
+ "altWeekDays": [
+ "SUN",
+ "MON",
+ "TUE",
+ "WED",
+ "THU",
+ "FRI",
+ "SAT"
+ ],
+ "altMonths": [
+ "JAN",
+ "FEB",
+ "MAR",
+ "APR",
+ "MAY",
+ "JUN",
+ "JUL",
+ "AUG",
+ "SEP",
+ "OCT",
+ "NOV",
+ "DEC"
+ ]
+}
\ No newline at end of file
diff --git a/packages/core/cli/src/commands/locale/react-js-cron/index.js b/packages/core/cli/src/commands/locale/react-js-cron/index.js
new file mode 100644
index 0000000000..c006afe6a2
--- /dev/null
+++ b/packages/core/cli/src/commands/locale/react-js-cron/index.js
@@ -0,0 +1,17 @@
+/**
+ * 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.
+ */
+
+exports.getReactJsCron = (lang) => {
+ const langs = {
+ 'en-US': require('./en-US.json'),
+ 'zh-CN': require('./zh-CN.json'),
+ 'z-TW': require('./zh-TW.json'),
+ }
+ return langs[lang] || langs['en-US'];
+}
diff --git a/packages/core/cli/src/commands/locale/react-js-cron/zh-CN.json b/packages/core/cli/src/commands/locale/react-js-cron/zh-CN.json
new file mode 100644
index 0000000000..3deea0f35c
--- /dev/null
+++ b/packages/core/cli/src/commands/locale/react-js-cron/zh-CN.json
@@ -0,0 +1,33 @@
+{
+ "everyText": "每",
+ "emptyMonths": "每月",
+ "emptyMonthDays": "每日(月)",
+ "emptyMonthDaysShort": "每日",
+ "emptyWeekDays": "每天(周)",
+ "emptyWeekDaysShort": "每天(周)",
+ "emptyHours": "每小时",
+ "emptyMinutes": "每分钟",
+ "emptyMinutesForHourPeriod": "每",
+ "yearOption": "年",
+ "monthOption": "月",
+ "weekOption": "周",
+ "dayOption": "天",
+ "hourOption": "小时",
+ "minuteOption": "分钟",
+ "rebootOption": "重启",
+ "prefixPeriod": "每",
+ "prefixMonths": "的",
+ "prefixMonthDays": "的",
+ "prefixWeekDays": "的",
+ "prefixWeekDaysForMonthAndYearPeriod": "或者",
+ "prefixHours": "的",
+ "prefixMinutes": ":",
+ "prefixMinutesForHourPeriod": "的",
+ "suffixMinutesForHourPeriod": "分钟",
+ "errorInvalidCron": "不符合 cron 规则的表达式",
+ "clearButtonText": "清空",
+ "weekDays": ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
+ "months": ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
+ "altWeekDays": ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
+ "altMonths": ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"]
+}
diff --git a/packages/core/cli/src/commands/locale/react-js-cron/zh-TW.json b/packages/core/cli/src/commands/locale/react-js-cron/zh-TW.json
new file mode 100644
index 0000000000..ddc825b472
--- /dev/null
+++ b/packages/core/cli/src/commands/locale/react-js-cron/zh-TW.json
@@ -0,0 +1,33 @@
+{
+ "everyText": "每",
+ "emptyMonths": "每月",
+ "emptyMonthDays": "每日(月)",
+ "emptyMonthDaysShort": "每日",
+ "emptyWeekDays": "每天(週)",
+ "emptyWeekDaysShort": "每天(週)",
+ "emptyHours": "每小時",
+ "emptyMinutes": "每分鐘",
+ "emptyMinutesForHourPeriod": "每",
+ "yearOption": "年",
+ "monthOption": "月",
+ "weekOption": "週",
+ "dayOption": "天",
+ "hourOption": "小時",
+ "minuteOption": "分鐘",
+ "rebootOption": "重啟",
+ "prefixPeriod": "每",
+ "prefixMonths": "的",
+ "prefixMonthDays": "的",
+ "prefixWeekDays": "的",
+ "prefixWeekDaysForMonthAndYearPeriod": "或者",
+ "prefixHours": "的",
+ "prefixMinutes": ":",
+ "prefixMinutesForHourPeriod": "的",
+ "suffixMinutesForHourPeriod": "分鐘",
+ "errorInvalidCron": "不符合 cron 規則的表示式",
+ "clearButtonText": "清空",
+ "weekDays": ["週日", "週一", "週二", "週三", "週四", "週五", "週六"],
+ "months": ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
+ "altWeekDays": ["週日", "週一", "週二", "週三", "週四", "週五", "週六"],
+ "altMonths": ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"]
+}
diff --git a/packages/core/cli/src/commands/start.js b/packages/core/cli/src/commands/start.js
index 968e8654b2..e0f0bc905c 100644
--- a/packages/core/cli/src/commands/start.js
+++ b/packages/core/cli/src/commands/start.js
@@ -8,7 +8,7 @@
*/
const _ = require('lodash');
const { Command } = require('commander');
-const { run, postCheck, downloadPro, promptForTs } = require('../util');
+const { run, postCheck, downloadPro, promptForTs, checkDBDialect } = require('../util');
const { existsSync, rmSync } = require('fs');
const { resolve, isAbsolute } = require('path');
const chalk = require('chalk');
@@ -48,8 +48,10 @@ module.exports = (cli) => {
.option('-i, --instances [instances]')
.option('--db-sync')
.option('--quickstart')
+ .option('--launch-mode [launchMode]')
.allowUnknownOption()
.action(async (opts) => {
+ checkDBDialect();
if (opts.quickstart) {
await downloadPro();
}
@@ -118,17 +120,27 @@ module.exports = (cli) => {
]);
process.exit();
} else {
- run(
- 'pm2-runtime',
- [
- 'start',
- ...instancesArgs,
- `${APP_PACKAGE_ROOT}/lib/index.js`,
- NODE_ARGS ? `--node-args="${NODE_ARGS}"` : undefined,
- '--',
- ...process.argv.slice(2),
- ].filter(Boolean),
- );
+ const launchMode = opts.launchMode || process.env.APP_LAUNCH_MODE || 'pm2';
+ if (launchMode === 'pm2') {
+ run(
+ 'pm2-runtime',
+ [
+ 'start',
+ ...instancesArgs,
+ `${APP_PACKAGE_ROOT}/lib/index.js`,
+ NODE_ARGS ? `--node-args="${NODE_ARGS}"` : undefined,
+ '--',
+ ...process.argv.slice(2),
+ ].filter(Boolean),
+ );
+ } else {
+ run(
+ 'node',
+ [`${APP_PACKAGE_ROOT}/lib/index.js`, ...(NODE_ARGS || '').split(' '), ...process.argv.slice(2)].filter(
+ Boolean,
+ ),
+ );
+ }
}
});
};
diff --git a/packages/core/cli/src/commands/test-coverage.js b/packages/core/cli/src/commands/test-coverage.js
index ee6d50695d..8a26113203 100644
--- a/packages/core/cli/src/commands/test-coverage.js
+++ b/packages/core/cli/src/commands/test-coverage.js
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-const { run } = require('../util');
+const { run, checkDBDialect } = require('../util');
const fg = require('fast-glob');
const coreClientPackages = ['packages/core/client', 'packages/core/sdk'];
@@ -30,6 +30,7 @@ const getPackagesDir = (isClient) => {
module.exports = (cli) => {
cli.command('test-coverage:server').action(async () => {
+ checkDBDialect();
const packageRoots = getPackagesDir(false);
for (const dir of packageRoots) {
try {
@@ -41,6 +42,7 @@ module.exports = (cli) => {
});
cli.command('test-coverage:client').action(async () => {
+ checkDBDialect();
const packageRoots = getPackagesDir(true);
for (const dir of packageRoots) {
try {
diff --git a/packages/core/cli/src/commands/test.js b/packages/core/cli/src/commands/test.js
index d97a5f1026..0ec62ac4e8 100644
--- a/packages/core/cli/src/commands/test.js
+++ b/packages/core/cli/src/commands/test.js
@@ -8,7 +8,7 @@
*/
const { Command } = require('commander');
-const { run } = require('../util');
+const { run, checkDBDialect } = require('../util');
const path = require('path');
/**
@@ -29,6 +29,7 @@ function addTestCommand(name, cli) {
.arguments('[paths...]')
.allowUnknownOption()
.action(async (paths, opts) => {
+ checkDBDialect();
if (name === 'test:server') {
process.env.TEST_ENV = 'server-side';
} else if (name === 'test:client') {
diff --git a/packages/core/cli/src/commands/update-deps.js b/packages/core/cli/src/commands/update-deps.js
new file mode 100644
index 0000000000..ed54d06721
--- /dev/null
+++ b/packages/core/cli/src/commands/update-deps.js
@@ -0,0 +1,71 @@
+/**
+ * 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 chalk = require('chalk');
+const { Command } = require('commander');
+const { resolve } = require('path');
+const { run, promptForTs, runAppCommand, hasCorePackages, downloadPro, hasTsNode, checkDBDialect } = require('../util');
+const { existsSync, rmSync } = require('fs');
+const { readJSON, writeJSON } = require('fs-extra');
+const deepmerge = require('deepmerge');
+
+const rmAppDir = () => {
+ // If ts-node is not installed, do not do the following
+ const appDevDir = resolve(process.cwd(), './storage/.app-dev');
+ if (existsSync(appDevDir)) {
+ rmSync(appDevDir, { recursive: true, force: true });
+ }
+};
+
+/**
+ *
+ * @param {Command} cli
+ */
+module.exports = (cli) => {
+ cli
+ .command('update-deps')
+ .option('--force')
+ .allowUnknownOption()
+ .action(async (options) => {
+ if (hasCorePackages() || !hasTsNode()) {
+ await downloadPro();
+ return;
+ }
+ const pkg = require('../../package.json');
+ let distTag = 'latest';
+ if (pkg.version.includes('alpha')) {
+ distTag = 'alpha';
+ } else if (pkg.version.includes('beta')) {
+ distTag = 'beta';
+ }
+ const { stdout } = await run('npm', ['info', `@nocobase/cli@${distTag}`, 'version'], {
+ stdio: 'pipe',
+ });
+ if (!options.force && pkg.version === stdout) {
+ await downloadPro();
+ rmAppDir();
+ return;
+ }
+ const descPath = resolve(process.cwd(), 'package.json');
+ const descJson = await readJSON(descPath, 'utf8');
+ const sourcePath = resolve(__dirname, '../../templates/create-app-package.json');
+ const sourceJson = await readJSON(sourcePath, 'utf8');
+ if (descJson['dependencies']?.['@nocobase/cli']) {
+ descJson['dependencies']['@nocobase/cli'] = stdout;
+ }
+ if (descJson['devDependencies']?.['@nocobase/devtools']) {
+ descJson['devDependencies']['@nocobase/devtools'] = stdout;
+ }
+ const json = deepmerge(descJson, sourceJson);
+ await writeJSON(descPath, json, { spaces: 2, encoding: 'utf8' });
+ await run('yarn', ['install']);
+ await downloadPro();
+ rmAppDir();
+ });
+};
diff --git a/packages/core/cli/src/commands/upgrade.js b/packages/core/cli/src/commands/upgrade.js
index 4774564e41..c2bb0736d3 100644
--- a/packages/core/cli/src/commands/upgrade.js
+++ b/packages/core/cli/src/commands/upgrade.js
@@ -10,15 +10,25 @@
const chalk = require('chalk');
const { Command } = require('commander');
const { resolve } = require('path');
-const { run, promptForTs, runAppCommand, hasCorePackages, downloadPro, hasTsNode } = require('../util');
+const { run, promptForTs, runAppCommand, hasCorePackages, downloadPro, hasTsNode, checkDBDialect } = require('../util');
const { existsSync, rmSync } = require('fs');
+const { readJSON, writeJSON } = require('fs-extra');
+const deepmerge = require('deepmerge');
+
+async function updatePackage() {
+ const sourcePath = resolve(__dirname, '../../templates/create-app-package.json');
+ const descPath = resolve(process.cwd(), 'package.json');
+ const sourceJson = await readJSON(sourcePath, 'utf8');
+ const descJson = await readJSON(descPath, 'utf8');
+ const json = deepmerge(descJson, sourceJson);
+ await writeJSON(descPath, json, { spaces: 2, encoding: 'utf8' });
+}
/**
*
* @param {Command} cli
*/
module.exports = (cli) => {
- const { APP_PACKAGE_ROOT } = process.env;
cli
.command('upgrade')
.allowUnknownOption()
@@ -26,52 +36,12 @@ module.exports = (cli) => {
.option('--next')
.option('-S|--skip-code-update')
.action(async (options) => {
- if (hasTsNode()) promptForTs();
- if (hasCorePackages()) {
- // await run('yarn', ['install']);
- await downloadPro();
- await runAppCommand('upgrade');
- return;
- }
+ checkDBDialect();
if (options.skipCodeUpdate) {
- await downloadPro();
await runAppCommand('upgrade');
- return;
+ } else {
+ await run('nocobase', ['update-deps']);
+ await run('nocobase', ['upgrade', '--skip-code-update']);
}
- // await runAppCommand('upgrade');
- if (!hasTsNode()) {
- await downloadPro();
- await runAppCommand('upgrade');
- return;
- }
- const rmAppDir = () => {
- // If ts-node is not installed, do not do the following
- const appDevDir = resolve(process.cwd(), './storage/.app-dev');
- if (existsSync(appDevDir)) {
- rmSync(appDevDir, { recursive: true, force: true });
- }
- };
- const pkg = require('../../package.json');
- let distTag = 'latest';
- if (pkg.version.includes('alpha')) {
- distTag = 'alpha';
- } else if (pkg.version.includes('beta')) {
- distTag = 'beta';
- }
- // get latest version
- const { stdout } = await run('npm', ['info', `@nocobase/cli@${distTag}`, 'version'], {
- stdio: 'pipe',
- });
- if (pkg.version === stdout) {
- await downloadPro();
- await runAppCommand('upgrade');
- await rmAppDir();
- return;
- }
- await run('yarn', ['add', `@nocobase/cli@${distTag}`, `@nocobase/devtools@${distTag}`, '-W']);
- await run('yarn', ['install']);
- await downloadPro();
- await runAppCommand('upgrade');
- await rmAppDir();
});
};
diff --git a/packages/core/cli/src/util.js b/packages/core/cli/src/util.js
index f48dbd4c1d..855acd317b 100644
--- a/packages/core/cli/src/util.js
+++ b/packages/core/cli/src/util.js
@@ -360,7 +360,7 @@ exports.initEnv = function initEnv() {
API_BASE_PATH: '/api/',
API_CLIENT_STORAGE_PREFIX: 'NOCOBASE_',
API_CLIENT_STORAGE_TYPE: 'localStorage',
- DB_DIALECT: 'sqlite',
+ // DB_DIALECT: 'sqlite',
DB_STORAGE: 'storage/db/nocobase.sqlite',
// DB_TIMEZONE: '+00:00',
DB_UNDERSCORED: parseEnv('DB_UNDERSCORED'),
@@ -460,6 +460,22 @@ exports.initEnv = function initEnv() {
process.env.SOCKET_PATH = generateGatewayPath();
fs.mkdirpSync(dirname(process.env.SOCKET_PATH), { force: true, recursive: true });
fs.mkdirpSync(process.env.PM2_HOME, { force: true, recursive: true });
+ const pkgs = [
+ '@nocobase/plugin-multi-app-manager',
+ '@nocobase/plugin-departments',
+ '@nocobase/plugin-field-attachment-url',
+ '@nocobase/plugin-workflow-response-message',
+ ];
+ for (const pkg of pkgs) {
+ const pkgDir = resolve(process.cwd(), 'storage/plugins', pkg);
+ fs.existsSync(pkgDir) && fs.rmdirSync(pkgDir, { recursive: true, force: true });
+ }
+};
+
+exports.checkDBDialect = function () {
+ if (!process.env.DB_DIALECT) {
+ throw new Error('DB_DIALECT is required.');
+ }
};
exports.generatePlugins = function () {
diff --git a/packages/core/cli/templates/create-app-package.json b/packages/core/cli/templates/create-app-package.json
new file mode 100644
index 0000000000..3caaa3882e
--- /dev/null
+++ b/packages/core/cli/templates/create-app-package.json
@@ -0,0 +1,39 @@
+{
+ "private": true,
+ "workspaces": ["packages/*/*", "packages/*/*/*"],
+ "engines": {
+ "node": ">=18"
+ },
+ "scripts": {
+ "nocobase": "nocobase",
+ "pm": "nocobase pm",
+ "pm2": "nocobase pm2",
+ "dev": "nocobase dev",
+ "start": "nocobase start",
+ "clean": "nocobase clean",
+ "build": "nocobase build",
+ "test": "nocobase test",
+ "e2e": "nocobase e2e",
+ "tar": "nocobase tar",
+ "postinstall": "nocobase postinstall",
+ "lint": "eslint ."
+ },
+ "resolutions": {
+ "cytoscape": "3.28.0",
+ "@types/react": "18.3.18",
+ "@types/react-dom": "^18.0.0",
+ "react-router-dom": "6.28.1",
+ "react-router": "6.28.1",
+ "async": "^3.2.6",
+ "antd": "5.12.8",
+ "rollup": "4.24.0",
+ "semver": "^7.7.1"
+ },
+ "dependencies": {
+ "pm2": "^6.0.5",
+ "mysql2": "^3.14.0",
+ "mariadb": "^2.5.6",
+ "pg": "^8.14.1",
+ "pg-hstore": "^2.3.4"
+ }
+}
diff --git a/packages/core/cli/templates/plugin/src/locale/nl-NL.json b/packages/core/cli/templates/plugin/src/locale/nl-NL.json
new file mode 100644
index 0000000000..9e26dfeeb6
--- /dev/null
+++ b/packages/core/cli/templates/plugin/src/locale/nl-NL.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/packages/core/client/.dumirc.ts b/packages/core/client/.dumirc.ts
index 1debccd479..8389585d18 100644
--- a/packages/core/client/.dumirc.ts
+++ b/packages/core/client/.dumirc.ts
@@ -234,6 +234,10 @@ export default defineConfig({
"title": "Filter",
"link": "/components/filter"
},
+ {
+ "title": "LinkageFilter",
+ "link": "/components/linkage-filter"
+ },
]
},
{
diff --git a/packages/core/client/package.json b/packages/core/client/package.json
index 1af0cfab5e..61726eaf97 100644
--- a/packages/core/client/package.json
+++ b/packages/core/client/package.json
@@ -1,6 +1,6 @@
{
"name": "@nocobase/client",
- "version": "1.6.0-beta.19",
+ "version": "1.7.0-beta.26",
"license": "AGPL-3.0",
"main": "lib/index.js",
"module": "es/index.mjs",
@@ -11,13 +11,13 @@
"@ant-design/icons": "^5.6.1",
"@ant-design/pro-layout": "^7.22.1",
"@antv/g2plot": "^2.4.18",
- "@budibase/handlebars-helpers": "^0.14.0",
+ "@budibase/handlebars-helpers": "0.14.0",
"@ctrl/tinycolor": "^3.6.0",
"@dnd-kit/core": "^5.0.1",
"@dnd-kit/modifiers": "^6.0.0",
"@dnd-kit/sortable": "^6.0.0",
"@emotion/css": "^11.7.1",
- "@formily/antd-v5": "1.1.9",
+ "@formily/antd-v5": "1.2.3",
"@formily/core": "^2.2.27",
"@formily/grid": "^2.2.27",
"@formily/json-schema": "^2.2.27",
@@ -27,11 +27,11 @@
"@formily/reactive-react": "^2.2.27",
"@formily/shared": "^2.2.27",
"@formily/validator": "^2.2.27",
- "@nocobase/evaluators": "1.6.0-beta.19",
- "@nocobase/sdk": "1.6.0-beta.19",
- "@nocobase/utils": "1.6.0-beta.19",
+ "@nocobase/evaluators": "1.7.0-beta.26",
+ "@nocobase/sdk": "1.7.0-beta.26",
+ "@nocobase/utils": "1.7.0-beta.26",
"ahooks": "^3.7.2",
- "antd": "5.12.8",
+ "antd": "5.24.2",
"antd-style": "3.7.1",
"axios": "^1.7.0",
"bignumber.js": "^9.1.2",
diff --git a/packages/core/client/src/acl/ACLProvider.tsx b/packages/core/client/src/acl/ACLProvider.tsx
index 6dfa112361..482bea60d7 100644
--- a/packages/core/client/src/acl/ACLProvider.tsx
+++ b/packages/core/client/src/acl/ACLProvider.tsx
@@ -74,6 +74,7 @@ export const ACLRolesCheckProvider = (props) => {
url: 'roles:check',
},
{
+ manual: !api.auth.token,
onSuccess(data) {
if (!data?.data?.snippets.includes('ui.*')) {
setDesignable(false);
@@ -102,6 +103,11 @@ export const useRoleRecheck = () => {
};
};
+export const useCurrentRoleMode = () => {
+ const ctx = useContext(ACLContext);
+ return ctx?.data?.data?.roleMode;
+};
+
export const useACLContext = () => {
return useContext(ACLContext);
};
@@ -308,15 +314,17 @@ export const ACLActionProvider = (props) => {
const schema = useFieldSchema();
const currentUid = schema['x-uid'];
let actionPath = schema['x-acl-action'];
- const editablePath = ['create', 'update', 'destroy', 'importXlsx'];
+ // 只兼容这些数据表资源按钮
+ const resourceActionPath = ['create', 'update', 'destroy', 'importXlsx', 'export'];
+ // 视图表无编辑权限时不支持的操作
+ const writableViewCollectionAction = ['create', 'update', 'destroy', 'importXlsx', 'bulkDestroy', 'bulkUpdate'];
- if (!actionPath && resource && schema['x-action'] && editablePath.includes(schema['x-action'])) {
+ if (!actionPath && resource && schema['x-action'] && resourceActionPath.includes(schema['x-action'])) {
actionPath = `${resource}:${schema['x-action']}`;
}
if (actionPath && !actionPath?.includes(':')) {
actionPath = `${resource}:${actionPath}`;
}
-
const params = useMemo(
() => actionPath && parseAction(actionPath, { schema, recordPkValue }),
[parseAction, actionPath, schema, recordPkValue],
@@ -333,16 +341,18 @@ export const ACLActionProvider = (props) => {
if (!params) {
return {props.children} ;
}
- //视图表无编辑权限时不显示
- if (editablePath.includes(actionPath) || editablePath.includes(actionPath?.split(':')[1])) {
+ //视图表无编辑权限时不支持 writableViewCollectionAction 的按钮
+ if (
+ writableViewCollectionAction.includes(actionPath) ||
+ writableViewCollectionAction.includes(actionPath?.split(':')[1])
+ ) {
if ((collection && collection.template !== 'view') || collection?.writableView) {
return {props.children} ;
}
- return null;
+ return {props.children} ;
}
return {props.children} ;
};
-
export const useACLFieldWhitelist = () => {
const params = useContext(ACLActionParamsContext);
const whitelist = useMemo(() => {
diff --git a/packages/core/client/src/acl/Configuration/ConfigureCenter.tsx b/packages/core/client/src/acl/Configuration/ConfigureCenter.tsx
index ddb4bb16c5..f6c9704fb9 100644
--- a/packages/core/client/src/acl/Configuration/ConfigureCenter.tsx
+++ b/packages/core/client/src/acl/Configuration/ConfigureCenter.tsx
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import { Checkbox, message, Table } from 'antd';
+import { Checkbox, message, Table, TableProps } from 'antd';
import { omit } from 'lodash';
import React, { createContext, useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -102,44 +102,46 @@ export const SettingsCenterConfigure = () => {
expandable={{
defaultExpandAllRows: true,
}}
- columns={[
- {
- dataIndex: 'title',
- title: t('Plugin name'),
- render: (value) => {
- return compile(value);
+ columns={
+ [
+ {
+ dataIndex: 'title',
+ title: t('Plugin name'),
+ render: (value) => {
+ return compile(value);
+ },
},
- },
- {
- dataIndex: 'accessible',
- title: (
- <>
- {
- const values = allAclSnippets.map((v) => '!' + v);
- if (!allChecked) {
- await resource.remove({
- values,
- });
- } else {
- await resource.add({
- values,
- });
- }
- refresh();
- message.success(t('Saved successfully'));
- }}
- />{' '}
- {t('Accessible')}
- >
- ),
- render: (_, record) => {
- const checked = !snippets.includes('!' + record.aclSnippet);
- return handleChange(checked, record)} />;
+ {
+ dataIndex: 'accessible',
+ title: (
+ <>
+ {
+ const values = allAclSnippets.map((v) => '!' + v);
+ if (!allChecked) {
+ await resource.remove({
+ values,
+ });
+ } else {
+ await resource.add({
+ values,
+ });
+ }
+ refresh();
+ message.success(t('Saved successfully'));
+ }}
+ />{' '}
+ {t('Accessible')}
+ >
+ ),
+ render: (_, record) => {
+ const checked = !snippets.includes('!' + record.aclSnippet);
+ return handleChange(checked, record)} />;
+ },
},
- },
- ]}
+ ] as TableProps['columns']
+ }
dataSource={settings
.filter((v) => {
return v.isTopLevel !== false;
diff --git a/packages/core/client/src/acl/Configuration/MenuConfigure.tsx b/packages/core/client/src/acl/Configuration/MenuConfigure.tsx
index 9f31491b89..2ab40c0f4d 100644
--- a/packages/core/client/src/acl/Configuration/MenuConfigure.tsx
+++ b/packages/core/client/src/acl/Configuration/MenuConfigure.tsx
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import { Checkbox, message, Table } from 'antd';
+import { Checkbox, message, Table, TableProps } from 'antd';
import { uniq } from 'lodash';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -121,40 +121,42 @@ export const MenuConfigure = () => {
expandable={{
defaultExpandAllRows: true,
}}
- columns={[
- {
- dataIndex: 'title',
- title: t('Menu item title'),
- },
- {
- dataIndex: 'accessible',
- title: (
- <>
- {
- if (allChecked) {
- await resource.set({
- values: [],
- });
- } else {
- await resource.set({
- values: allUids,
- });
- }
- refresh();
- message.success(t('Saved successfully'));
- }}
- />{' '}
- {t('Accessible')}
- >
- ),
- render: (_, schema) => {
- const checked = uids.includes(schema.uid);
- return handleChange(checked, schema)} />;
+ columns={
+ [
+ {
+ dataIndex: 'title',
+ title: t('Menu item title'),
},
- },
- ]}
+ {
+ dataIndex: 'accessible',
+ title: (
+ <>
+ {
+ if (allChecked) {
+ await resource.set({
+ values: [],
+ });
+ } else {
+ await resource.set({
+ values: allUids,
+ });
+ }
+ refresh();
+ message.success(t('Saved successfully'));
+ }}
+ />{' '}
+ {t('Accessible')}
+ >
+ ),
+ render: (_, schema: { uid: string }) => {
+ const checked = uids.includes(schema.uid);
+ return handleChange(checked, schema)} />;
+ },
+ },
+ ] as TableProps['columns']
+ }
dataSource={translateTitle(items)}
/>
);
diff --git a/packages/core/client/src/acl/Configuration/RolesResourcesActions.tsx b/packages/core/client/src/acl/Configuration/RolesResourcesActions.tsx
index da896e68d5..3dab660bf7 100644
--- a/packages/core/client/src/acl/Configuration/RolesResourcesActions.tsx
+++ b/packages/core/client/src/acl/Configuration/RolesResourcesActions.tsx
@@ -10,7 +10,7 @@
import { FormItem, FormLayout } from '@formily/antd-v5';
import { ArrayField } from '@formily/core';
import { connect, useField, useForm } from '@formily/react';
-import { Checkbox, Table, Tag } from 'antd';
+import { Checkbox, Table, Tag, TableProps } from 'antd';
import { isEmpty } from 'lodash';
import React, { createContext } from 'react';
import { useTranslation } from 'react-i18next';
@@ -105,48 +105,50 @@ export const RolesResourcesActions = connect((props) => {
className={antTableCell}
size={'small'}
pagination={false}
- columns={[
- {
- dataIndex: 'displayName',
- title: t('Action display name'),
- render: (value) => compile(value),
- },
- {
- dataIndex: 'onNewRecord',
- title: t('Action type'),
- render: (onNewRecord) =>
- onNewRecord ? (
- {t('Action on new records')}
- ) : (
- {t('Action on existing records')}
- ),
- },
- {
- dataIndex: 'enabled',
- title: t('Allow'),
- render: (enabled, action) => (
- {
- toggleAction(action.name);
- }}
- />
- ),
- },
- {
- dataIndex: 'scope',
- title: t('Data scope'),
- render: (value, action) =>
- !action.onNewRecord && (
- {
- setScope(action.name, scope);
+ columns={
+ [
+ {
+ dataIndex: 'displayName',
+ title: t('Action display name'),
+ render: (value) => compile(value),
+ },
+ {
+ dataIndex: 'onNewRecord',
+ title: t('Action type'),
+ render: (onNewRecord) =>
+ onNewRecord ? (
+ {t('Action on new records')}
+ ) : (
+ {t('Action on existing records')}
+ ),
+ },
+ {
+ dataIndex: 'enabled',
+ title: t('Allow'),
+ render: (enabled, action) => (
+ {
+ toggleAction(action.name);
}}
/>
),
- },
- ]}
+ },
+ {
+ dataIndex: 'scope',
+ title: t('Data scope'),
+ render: (value, action) =>
+ !action.onNewRecord && (
+ {
+ setScope(action.name, scope);
+ }}
+ />
+ ),
+ },
+ ] as TableProps['columns']
+ }
dataSource={availableActions?.map((item) => {
let enabled = false;
let scope = null;
@@ -169,60 +171,62 @@ export const RolesResourcesActions = connect((props) => {
className={antTableCell}
pagination={false}
dataSource={fieldPermissions}
- columns={[
- {
- dataIndex: ['uiSchema', 'title'],
- title: t('Field display name'),
- render: (value) => compile(value),
- },
- ...availableActionsWithFields.map((action) => {
- const checked = allChecked?.[action.name];
- return {
- dataIndex: action.name,
- title: (
- <>
+ columns={
+ [
+ {
+ dataIndex: ['uiSchema', 'title'],
+ title: t('Field display name'),
+ render: (value) => compile(value),
+ },
+ ...availableActionsWithFields.map((action) => {
+ const checked = allChecked?.[action.name];
+ return {
+ dataIndex: action.name,
+ title: (
+ <>
+ {
+ const item = actionMap[action.name] || {
+ name: action.name,
+ };
+ if (checked) {
+ item.fields = [];
+ } else {
+ item.fields = collectionFields?.map?.((item) => item.name);
+ }
+ actionMap[action.name] = item;
+ onChange(Object.values(actionMap));
+ }}
+ />{' '}
+ {compile(action.displayName)}
+ >
+ ),
+ render: (checked, field) => (
{
const item = actionMap[action.name] || {
name: action.name,
};
+ const fields: string[] = item.fields || [];
if (checked) {
- item.fields = [];
+ const index = fields.indexOf(field.name);
+ fields.splice(index, 1);
} else {
- item.fields = collectionFields?.map?.((item) => item.name);
+ fields.push(field.name);
}
+ item.fields = fields;
actionMap[action.name] = item;
onChange(Object.values(actionMap));
}}
- />{' '}
- {compile(action.displayName)}
- >
- ),
- render: (checked, field) => (
- {
- const item = actionMap[action.name] || {
- name: action.name,
- };
- const fields: string[] = item.fields || [];
- if (checked) {
- const index = fields.indexOf(field.name);
- fields.splice(index, 1);
- } else {
- fields.push(field.name);
- }
- item.fields = fields;
- actionMap[action.name] = item;
- onChange(Object.values(actionMap));
- }}
- />
- ),
- };
- }),
- ]}
+ />
+ ),
+ };
+ }),
+ ] as TableProps['columns']
+ }
/>
diff --git a/packages/core/client/src/acl/Configuration/StrategyActions.tsx b/packages/core/client/src/acl/Configuration/StrategyActions.tsx
index df501db691..4efa5a8716 100644
--- a/packages/core/client/src/acl/Configuration/StrategyActions.tsx
+++ b/packages/core/client/src/acl/Configuration/StrategyActions.tsx
@@ -9,7 +9,7 @@
import { ArrayField } from '@formily/core';
import { connect, useField } from '@formily/react';
-import { Checkbox, Select, Table, Tag } from 'antd';
+import { Checkbox, Select, Table, Tag, TableProps } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useCompile } from '../..';
@@ -55,62 +55,64 @@ export const StrategyActions = connect((props) => {
size={'small'}
pagination={false}
rowKey={'name'}
- columns={[
- {
- dataIndex: 'displayName',
- title: t('Action display name'),
- render: (value) => compile(value),
- },
- {
- dataIndex: 'onNewRecord',
- title: t('Action type'),
- render: (onNewRecord) =>
- onNewRecord ? (
- {t('Action on new records')}
- ) : (
- {t('Action on existing records')}
- ),
- },
- {
- dataIndex: 'enabled',
- title: t('Allow'),
- render: (enabled, action) => (
- {
- if (enabled) {
- delete scopes[action.name];
- } else {
- scopes[action.name] = 'all';
- }
- onChange(toFieldValue(scopes));
- }}
- />
- ),
- },
- {
- dataIndex: 'scope',
- title: t('Data scope'),
- render: (scope, action) =>
- !action.onNewRecord && (
- {
- scopes[action.name] = value;
+ columns={
+ [
+ {
+ dataIndex: 'displayName',
+ title: t('Action display name'),
+ render: (value) => compile(value),
+ },
+ {
+ dataIndex: 'onNewRecord',
+ title: t('Action type'),
+ render: (onNewRecord) =>
+ onNewRecord ? (
+ {t('Action on new records')}
+ ) : (
+ {t('Action on existing records')}
+ ),
+ },
+ {
+ dataIndex: 'enabled',
+ title: t('Allow'),
+ render: (enabled, action) => (
+ {
+ if (enabled) {
+ delete scopes[action.name];
+ } else {
+ scopes[action.name] = 'all';
+ }
onChange(toFieldValue(scopes));
}}
/>
),
- },
- ]}
+ },
+ {
+ dataIndex: 'scope',
+ title: t('Data scope'),
+ render: (scope, action) =>
+ !action.onNewRecord && (
+ {
+ scopes[action.name] = value;
+ onChange(toFieldValue(scopes));
+ }}
+ />
+ ),
+ },
+ ] as TableProps['columns']
+ }
dataSource={availableActions?.map((item) => {
let scope = 'all';
let enabled = false;
diff --git a/packages/core/client/src/acl/Configuration/schemas/scopes.ts b/packages/core/client/src/acl/Configuration/schemas/scopes.ts
index a202d7871d..5984e48225 100644
--- a/packages/core/client/src/acl/Configuration/schemas/scopes.ts
+++ b/packages/core/client/src/acl/Configuration/schemas/scopes.ts
@@ -236,7 +236,6 @@ export const scopesSchema: ISchema = {
'x-component': 'Action.Link',
'x-component-props': {
openMode: 'drawer',
- icon: 'EditOutlined',
},
properties: {
drawer: {
diff --git a/packages/core/client/src/api-client/APIClient.ts b/packages/core/client/src/api-client/APIClient.ts
index 1cbbcdacdd..f85c766e10 100644
--- a/packages/core/client/src/api-client/APIClient.ts
+++ b/packages/core/client/src/api-client/APIClient.ts
@@ -139,7 +139,19 @@ export class APIClient extends APIClientSDK {
if (typeof error?.response?.data === 'string') {
const tempElement = document.createElement('div');
tempElement.innerHTML = error?.response?.data;
- return [{ message: tempElement.textContent || tempElement.innerText }];
+ let message = tempElement.textContent || tempElement.innerText;
+ if (message.includes('Error occurred while trying')) {
+ message = 'The application may be starting up. Please try again later.';
+ return [{ code: 'APP_WARNING', message }];
+ }
+ if (message.includes('502 Bad Gateway')) {
+ message = 'The application may be starting up. Please try again later.';
+ return [{ code: 'APP_WARNING', message }];
+ }
+ return [{ message }];
+ }
+ if (error?.response?.data?.error) {
+ return [error?.response?.data?.error];
}
return (
error?.response?.data?.errors ||
diff --git a/packages/core/client/src/application/Application.tsx b/packages/core/client/src/application/Application.tsx
index c441f1ae92..ae517e9b52 100644
--- a/packages/core/client/src/application/Application.tsx
+++ b/packages/core/client/src/application/Application.tsx
@@ -108,6 +108,7 @@ export class Application {
public name: string;
public favicon: string;
public globalVars: Record = {};
+ public globalVarCtxs: Record = {};
public jsonLogic: JsonLogic;
loading = true;
maintained = false;
@@ -350,23 +351,9 @@ export class Application {
setTimeout(() => resolve(null), 1000);
});
}
- const toError = (error) => {
- if (typeof error?.response?.data === 'string') {
- const tempElement = document.createElement('div');
- tempElement.innerHTML = error?.response?.data;
- return { message: tempElement.textContent || tempElement.innerText };
- }
- 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',
- ...toError(error),
+ ...this.apiClient.toErrMessages(error)?.[0],
};
console.error(error, this.error);
}
@@ -508,13 +495,20 @@ export class Application {
);
}
- addGlobalVar(key: string, value: any) {
+ addGlobalVar(key: string, value: any, varCtx?: any) {
set(this.globalVars, key, value);
+ if (varCtx) {
+ set(this.globalVarCtxs, key, varCtx);
+ }
}
getGlobalVar(key) {
return get(this.globalVars, key);
}
+
+ getGlobalVarCtx(key) {
+ return get(this.globalVarCtxs, key);
+ }
addUserCenterSettingsItem(item: SchemaSettingsItemType & { aclSnippet?: string }) {
const useVisibleProp = item.useVisible || (() => true);
const useVisible = () => {
diff --git a/packages/core/client/src/application/RouterManager.tsx b/packages/core/client/src/application/RouterManager.tsx
index a8b08dafa0..cf16f2c9e4 100644
--- a/packages/core/client/src/application/RouterManager.tsx
+++ b/packages/core/client/src/application/RouterManager.tsx
@@ -9,6 +9,7 @@
import { get, set } from 'lodash';
import React, { ComponentType, createContext, useContext } from 'react';
+import { matchRoutes } from 'react-router';
import {
BrowserRouterProps,
createBrowserRouter,
@@ -42,6 +43,7 @@ export type RouterOptions = (HashRouterOptions | BrowserRouterOptions | MemoryRo
export type ComponentTypeAndString = ComponentType | string;
export interface RouteType extends Omit {
Component?: ComponentTypeAndString;
+ skipAuthCheck?: boolean;
}
export type RenderComponentType = (Component: ComponentTypeAndString, props?: any) => React.ReactNode;
@@ -134,6 +136,18 @@ export class RouterManager {
this.options.basename = basename;
}
+ matchRoutes(pathname: string) {
+ const routes = Object.values(this.routes);
+ // @ts-ignore
+ return matchRoutes(routes, pathname, this.basename);
+ }
+
+ isSkippedAuthCheckRoute(pathname: string) {
+ const matchedRoutes = this.matchRoutes(pathname);
+ return matchedRoutes.some((match) => {
+ return match?.route?.skipAuthCheck === true;
+ });
+ }
/**
* @internal
*/
diff --git a/packages/core/client/src/application/__tests__/RouterManager.test.tsx b/packages/core/client/src/application/__tests__/RouterManager.test.tsx
index 2ec1850a8e..0cf2419754 100644
--- a/packages/core/client/src/application/__tests__/RouterManager.test.tsx
+++ b/packages/core/client/src/application/__tests__/RouterManager.test.tsx
@@ -30,7 +30,7 @@ describe('Router', () => {
let router: RouterManager;
beforeEach(() => {
- router = new RouterManager({ type: 'memory', initialEntries: ['/'] }, app);
+ router = new RouterManager({ type: 'memory', initialEntries: ['/'], basename: '/nocobase/apps/test1' }, app);
});
it('basic', () => {
@@ -132,6 +132,38 @@ describe('Router', () => {
router.add('test', route);
expect(router.getRoutesTree()).toEqual([{ path: '/', element: , children: undefined }]);
});
+
+ it('add skipAuthCheck route', () => {
+ router.add('skip-auth-check', { path: '/skip-auth-check', Component: 'Hello', skipAuthCheck: true });
+ router.add('not-skip-auth-check', { path: '/not-skip-auth-check', Component: 'Hello' });
+
+ const RouterComponent = router.getRouterComponent();
+ const BaseLayout: FC = (props) => {
+ return BaseLayout {props.children}
;
+ };
+ render( );
+ router.navigate('/skip-auth-check');
+ const state = router.state;
+ const { pathname, search } = state.location;
+ const isSkipedAuthCheck = router.isSkippedAuthCheckRoute(pathname);
+ expect(isSkipedAuthCheck).toBe(true);
+ });
+
+ it('add not skipAuthCheck route', () => {
+ router.add('skip-auth-check', { path: '/skip-auth-check', Component: 'Hello', skipAuthCheck: true });
+ router.add('not-skip-auth-check', { path: '/not-skip-auth-check', Component: 'Hello' });
+
+ const RouterComponent = router.getRouterComponent();
+ const BaseLayout: FC = (props) => {
+ return BaseLayout {props.children}
;
+ };
+ render( );
+ router.navigate('/not-skip-auth-check');
+ const state = router.state;
+ const { pathname, search } = state.location;
+ const isSkipedAuthCheck = router.isSkippedAuthCheckRoute(pathname);
+ expect(isSkipedAuthCheck).toBe(false);
+ });
});
describe('remove', () => {
diff --git a/packages/core/client/src/application/components/defaultComponents.tsx b/packages/core/client/src/application/components/defaultComponents.tsx
index aeb2c475b9..31801e97ed 100644
--- a/packages/core/client/src/application/components/defaultComponents.tsx
+++ b/packages/core/client/src/application/components/defaultComponents.tsx
@@ -11,10 +11,11 @@ import React, { FC } from 'react';
import { MainComponent } from './MainComponent';
const Loading: FC = () => Loading...
;
-const AppError: FC<{ error: Error }> = ({ error }) => {
+const AppError: FC<{ error: Error & { title?: string } }> = ({ error }) => {
+ const title = error?.title || 'App Error';
return (
-
App Error
+
{title}
{error?.message}
{process.env.__TEST__ && error?.stack}
diff --git a/packages/core/client/src/application/globalOperators.js b/packages/core/client/src/application/globalOperators.js
index c3b16067e8..ce8f265eb1 100644
--- a/packages/core/client/src/application/globalOperators.js
+++ b/packages/core/client/src/application/globalOperators.js
@@ -124,7 +124,7 @@ export function getOperators() {
return !a.includes(b);
},
$anyOf: function (a, b) {
- if (a.length === 0) {
+ if (a == null || a.length === 0) {
return false;
}
if (Array.isArray(a) && Array.isArray(b) && a.some((element) => Array.isArray(element))) {
@@ -167,7 +167,7 @@ export function getOperators() {
const dateA = parseDate(a);
const dateB = parseDate(b);
if (!dateA || !dateB) {
- throw new Error('Invalid date format');
+ return false;
}
return dateA < dateB;
},
@@ -347,7 +347,7 @@ export function getOperators() {
/*
This helper will defer to the JsonLogic spec as a tie-breaker when different language interpreters define different behavior for the truthiness of primitives. E.g., PHP considers empty arrays to be falsy, but Javascript considers them to be truthy. JsonLogic, as an ecosystem, needs one consistent answer.
-
+
Spec and rationale here: http://jsonlogic.com/truthy
*/
jsonLogic.truthy = function (value) {
@@ -397,7 +397,7 @@ export function getOperators() {
if( 0 ){ 1 }else{ 2 };
if( 0 ){ 1 }else if( 2 ){ 3 }else{ 4 };
if( 0 ){ 1 }else if( 2 ){ 3 }else if( 4 ){ 5 }else{ 6 };
-
+
The implementation is:
For pairs of values (0,1 then 2,3 then 4,5 etc)
If the first evaluates truthy, evaluate and return the second
@@ -651,10 +651,11 @@ function parseYear(dateStr) {
}
function parseDate(targetDateStr) {
- let dateStr = Array.isArray(targetDateStr) ? targetDateStr[1] : targetDateStr;
- if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(dateStr)) {
- // ISO 8601 格式:YYYY-MM-DDTHH:mm:ss.sssZ
- return new Date(dateStr); // 直接解析为 Date 对象
+ let dateStr = Array.isArray(targetDateStr) ? targetDateStr[1] ?? targetDateStr[0] : targetDateStr;
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(dateStr)) {
+ return new Date(dateStr);
+ } else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(dateStr)) {
+ return new Date(dateStr.replace(' ', 'T'));
} else if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
// YYYY-MM-DD 格式
return parseFullDate(dateStr);
@@ -668,5 +669,6 @@ function parseDate(targetDateStr) {
// YYYY 格式
return parseYear(dateStr);
}
- return null; // Invalid format
+
+ return null;
}
diff --git a/packages/core/client/src/application/hooks/index.ts b/packages/core/client/src/application/hooks/index.ts
index 57fb89faff..3116f55c87 100644
--- a/packages/core/client/src/application/hooks/index.ts
+++ b/packages/core/client/src/application/hooks/index.ts
@@ -12,3 +12,4 @@ export * from './useAppSpin';
export * from './usePlugin';
export * from './useRouter';
export * from './useGlobalVariable';
+export * from './useAclSnippets';
diff --git a/packages/core/client/src/application/hooks/useGlobalVariable.ts b/packages/core/client/src/application/hooks/useGlobalVariable.ts
index 1ef7ef76d1..037c5256ca 100644
--- a/packages/core/client/src/application/hooks/useGlobalVariable.ts
+++ b/packages/core/client/src/application/hooks/useGlobalVariable.ts
@@ -29,3 +29,22 @@ export const useGlobalVariable = (key: string) => {
return variable;
};
+
+export const useGlobalVariableCtx = (key: string) => {
+ const app = useApp();
+
+ const variable = useMemo(() => {
+ return app?.getGlobalVarCtx?.(key);
+ }, [app, key]);
+
+ if (isFunction(variable)) {
+ try {
+ return variable();
+ } catch (error) {
+ console.error(`Error calling global variable function for key: ${key}`, error);
+ return undefined;
+ }
+ }
+
+ return variable;
+};
diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx
index 436fc54f37..dfdedad636 100644
--- a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx
+++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx
@@ -63,7 +63,7 @@ export const SchemaInitializerItem = memo(
className: className,
label: children || compile(title),
onClick: (info) => {
- if (info.key !== name) return;
+ if (disabled || info.key !== name) return;
if (closeInitializerMenuWhenClick) {
setVisible?.(false);
}
@@ -73,10 +73,10 @@ export const SchemaInitializerItem = memo(
children: childrenItems,
},
];
- }, [name, style, className, children, title, onClick, icon, childrenItems]);
+ }, [name, disabled, style, className, children, title, onClick, icon, childrenItems]);
if (items && items.length > 0) {
- return ;
+ return ;
}
return (
= (props) => {
hideIfNoChildren,
componentProps,
} = props as any;
+ const typeComponentMap = {
+ item: SchemaSettingsItem,
+ itemGroup: SchemaSettingsItemGroup,
+ subMenu: SchemaSettingsSubMenu,
+ divider: SchemaSettingsDivider,
+ remove: SchemaSettingsRemove,
+ select: SchemaSettingsSelectItem,
+ cascader: SchemaSettingsCascaderItem,
+ switch: SchemaSettingsSwitchItem,
+ popup: SchemaSettingsPopupItem,
+ actionModal: SchemaSettingsActionModalItem,
+ modal: SchemaSettingsModalItem,
+ };
const useChildrenRes = useChildren();
const useComponentPropsRes = useComponentProps();
const findComponent = useFindComponent();
diff --git a/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx b/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx
index 704c7156f7..2047aeb971 100644
--- a/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx
+++ b/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx
@@ -22,8 +22,15 @@ import { useCreateFormBlockProps } from '../modules/blocks/data-blocks/form/hook
import { useEditFormBlockDecoratorProps } from '../modules/blocks/data-blocks/form/hooks/useEditFormBlockDecoratorProps';
import { useEditFormBlockProps } from '../modules/blocks/data-blocks/form/hooks/useEditFormBlockProps';
import { useDataFormItemProps } from '../modules/blocks/data-blocks/form/hooks/useDataFormItemProps';
-import { useGridCardBlockDecoratorProps } from '../modules/blocks/data-blocks/grid-card/hooks/useGridCardBlockDecoratorProps';
-import { useListBlockDecoratorProps } from '../modules/blocks/data-blocks/list/hooks/useListBlockDecoratorProps';
+import {
+ useGridCardBlockDecoratorProps,
+ useGridCardBlockItemProps,
+ useGridCardBlockProps,
+} from '../modules/blocks/data-blocks/grid-card/hooks/useGridCardBlockDecoratorProps';
+import {
+ useListBlockDecoratorProps,
+ useListBlockProps,
+} from '../modules/blocks/data-blocks/list/hooks/useListBlockDecoratorProps';
import { useTableSelectorDecoratorProps } from '../modules/blocks/data-blocks/table-selector/hooks/useTableSelectorDecoratorProps';
import { TableColumnSchemaToolbar } from '../modules/blocks/data-blocks/table/TableColumnSchemaToolbar';
import { useTableBlockDecoratorProps } from '../modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps';
@@ -80,11 +87,14 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
useTableSelectorProps,
useTableBlockDecoratorProps,
useListBlockDecoratorProps,
+ useListBlockProps,
useTableSelectorDecoratorProps,
useCollapseBlockDecoratorProps,
useFilterFormBlockProps,
useFilterFormBlockDecoratorProps,
useGridCardBlockDecoratorProps,
+ useGridCardBlockItemProps,
+ useGridCardBlockProps,
useFormItemProps,
useDataFormItemProps,
}}
@@ -141,11 +151,14 @@ export class BlockSchemaComponentPlugin extends Plugin {
useTableSelectorProps,
useTableBlockDecoratorProps,
useListBlockDecoratorProps,
+ useListBlockProps,
useTableSelectorDecoratorProps,
useCollapseBlockDecoratorProps,
useFilterFormBlockProps,
useFilterFormBlockDecoratorProps,
useGridCardBlockDecoratorProps,
+ useGridCardBlockProps,
+ useGridCardBlockItemProps,
useFormItemProps,
useDataFormItemProps,
});
diff --git a/packages/core/client/src/block-provider/DetailsBlockProvider.tsx b/packages/core/client/src/block-provider/DetailsBlockProvider.tsx
index 624879ef94..074a1c9a2e 100644
--- a/packages/core/client/src/block-provider/DetailsBlockProvider.tsx
+++ b/packages/core/client/src/block-provider/DetailsBlockProvider.tsx
@@ -12,7 +12,6 @@ import { useField, useFieldSchema } from '@formily/react';
import { useUpdate } from 'ahooks';
import { Spin } from 'antd';
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
-import { useCollectionManager_deprecated } from '../collection-manager';
import { useCollection, useCollectionRecordData } from '../data-source';
import { useCollectionParentRecord } from '../data-source/collection-record/CollectionRecordProvider';
import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps';
@@ -103,8 +102,7 @@ const useCompatDetailsBlockParams = (props) => {
export const DetailsBlockProvider = withDynamicSchemaProps((props) => {
const { params, parseVariableLoading } = useCompatDetailsBlockParams(props);
const record = useCollectionRecordData();
- const { association, dataSource, action } = props;
- const { getCollection } = useCollectionManager_deprecated(dataSource);
+ const { association, action } = props;
const { __collection } = record || {};
const { designable } = useDesignable();
const collectionName = props.collection;
diff --git a/packages/core/client/src/block-provider/FilterFormBlockProvider.tsx b/packages/core/client/src/block-provider/FilterFormBlockProvider.tsx
index f6eacc4ede..1a46420a4b 100644
--- a/packages/core/client/src/block-provider/FilterFormBlockProvider.tsx
+++ b/packages/core/client/src/block-provider/FilterFormBlockProvider.tsx
@@ -10,11 +10,11 @@
import { useFieldSchema } from '@formily/react';
import React from 'react';
import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps';
-import { DatePickerProvider, ActionBarProvider, SchemaComponentOptions } from '../schema-component';
+import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField';
+import { ActionBarProvider, DatePickerProvider, SchemaComponentOptions } from '../schema-component';
import { DefaultValueProvider } from '../schema-settings';
import { CollectOperators } from './CollectOperators';
import { FormBlockProvider } from './FormBlockProvider';
-import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField';
export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
const filedSchema = useFieldSchema();
@@ -35,7 +35,7 @@ export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
}}
>
false}>
-
+
diff --git a/packages/core/client/src/block-provider/TableBlockProvider.tsx b/packages/core/client/src/block-provider/TableBlockProvider.tsx
index d1ae360457..7df1fecbfc 100644
--- a/packages/core/client/src/block-provider/TableBlockProvider.tsx
+++ b/packages/core/client/src/block-provider/TableBlockProvider.tsx
@@ -62,6 +62,7 @@ interface Props {
children?: any;
expandFlag?: boolean;
dragSortBy?: string;
+ enableIndexÏColumn?: boolean;
}
const InternalTableBlockProvider = (props: Props) => {
@@ -74,6 +75,7 @@ const InternalTableBlockProvider = (props: Props) => {
expandFlag: propsExpandFlag = false,
fieldNames,
collection,
+ enableIndexÏColumn,
} = props;
const field: any = useField();
const { resource, service } = useBlockRequestContext();
@@ -131,6 +133,7 @@ const InternalTableBlockProvider = (props: Props) => {
allIncludesChildren,
setExpandFlag: setExpandFlagValue,
heightProps,
+ enableIndexÏColumn,
}),
[
allIncludesChildren,
@@ -146,6 +149,7 @@ const InternalTableBlockProvider = (props: Props) => {
service,
setExpandFlagValue,
showIndex,
+ enableIndexÏColumn,
],
);
diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts
index 168374889f..f602f86ee5 100644
--- a/packages/core/client/src/block-provider/hooks/index.ts
+++ b/packages/core/client/src/block-provider/hooks/index.ts
@@ -97,6 +97,35 @@ const filterValue = (value) => {
return obj;
};
+function getFilteredFormValues(form) {
+ const values = _.cloneDeep(form.values);
+ const allFields = [];
+ form.query('*').forEach((field) => {
+ if (field) {
+ allFields.push(field);
+ }
+ });
+ const readonlyPaths = _.uniq(
+ allFields
+ .filter((field) => field?.componentProps?.readOnlySubmit)
+ .map((field) => {
+ const segments = field.path?.segments || [];
+ if (segments.length <= 1) {
+ return segments.join('.');
+ }
+ return segments.slice(0, -1).join('.');
+ }),
+ );
+ readonlyPaths.forEach((path, index) => {
+ if (index !== 0 || path.includes('.')) {
+ // 清空值,但跳过第一层
+ _.unset(values, path);
+ }
+ });
+
+ return values;
+}
+
export function getFormValues({
filterByTk,
field,
@@ -124,7 +153,7 @@ export function getFormValues({
}
}
- return form.values;
+ return getFilteredFormValues(form);
}
export function useCollectValuesToSubmit() {
@@ -167,7 +196,7 @@ export function useCollectValuesToSubmit() {
if (parsedValue !== null && parsedValue !== undefined) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
- } else if (value != null && value !== '') {
+ } else if (value !== '') {
assignedValues[key] = value;
}
});
@@ -203,6 +232,12 @@ export function useCollectValuesToSubmit() {
]);
}
+export function interpolateVariables(str: string, scope: Record
): string {
+ return str.replace(/\{\{\s*([a-zA-Z0-9_$.-]+?)\s*\}\}/g, (_, key) => {
+ return scope[key] !== undefined ? String(scope[key]) : '';
+ });
+}
+
export const useCreateActionProps = () => {
const filterByTk = useFilterByTk();
const record = useCollectionRecord();
@@ -219,11 +254,20 @@ export const useCreateActionProps = () => {
const collectValues = useCollectValuesToSubmit();
const action = record.isNew ? actionField.componentProps.saveMode || 'create' : 'update';
const filterKeys = actionField.componentProps.filterKeys?.checked || [];
+ const localVariables = useLocalVariables();
+ const variables = useVariables();
return {
async onClick() {
const { onSuccess, skipValidator, triggerWorkflows } = actionSchema?.['x-action-settings'] ?? {};
- const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {};
+ const {
+ manualClose,
+ redirecting,
+ redirectTo: rawRedirectTo,
+ successMessage,
+ actionAfterSuccess,
+ } = onSuccess || {};
+
if (!skipValidator) {
await form.submit();
}
@@ -241,6 +285,15 @@ export const useCreateActionProps = () => {
: undefined,
updateAssociationValues,
});
+ let redirectTo = rawRedirectTo;
+ if (rawRedirectTo) {
+ const { exp, scope: expScope } = await replaceVariables(rawRedirectTo, {
+ variables,
+ localVariables: [...localVariables, { name: '$record', ctx: new Proxy(data?.data?.data, {}) }],
+ });
+ redirectTo = interpolateVariables(exp, expScope);
+ }
+
if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) {
setVisible?.(false);
}
@@ -338,7 +391,7 @@ export const useAssociationCreateActionProps = () => {
if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
- } else if (value != null && value !== '') {
+ } else if (value !== '') {
assignedValues[key] = value;
}
});
@@ -468,6 +521,10 @@ const useDoFilter = () => {
block.defaultFilter,
]);
+ if (_.isEmpty(storedFilter[uid])) {
+ block.clearSelection?.();
+ }
+
if (doNothingWhenFilterIsEmpty && _.isEmpty(storedFilter[uid])) {
return;
}
@@ -518,9 +575,11 @@ export const useFilterBlockActionProps = () => {
const { doFilter } = useDoFilter();
const actionField = useField();
actionField.data = actionField.data || {};
+ const form = useForm();
return {
async onClick() {
+ await form.submit();
actionField.data.loading = true;
await doFilter();
actionField.data.loading = false;
@@ -584,7 +643,13 @@ export const useCustomizeUpdateActionProps = () => {
skipValidator,
triggerWorkflows,
} = actionSchema?.['x-action-settings'] ?? {};
- const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {};
+ const {
+ manualClose,
+ redirecting,
+ redirectTo: rawRedirectTo,
+ successMessage,
+ actionAfterSuccess,
+ } = onSuccess || {};
const assignedValues = {};
const waitList = Object.keys(originalAssignedValues).map(async (key) => {
const value = originalAssignedValues[key];
@@ -601,7 +666,7 @@ export const useCustomizeUpdateActionProps = () => {
if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
- } else if (value != null && value !== '') {
+ } else if (value !== '') {
assignedValues[key] = value;
}
});
@@ -610,7 +675,7 @@ export const useCustomizeUpdateActionProps = () => {
if (skipValidator === false) {
await form.submit();
}
- await resource.update({
+ const result = await resource.update({
filterByTk,
values: { ...assignedValues },
// TODO(refactor): should change to inject by plugin
@@ -618,6 +683,16 @@ export const useCustomizeUpdateActionProps = () => {
? triggerWorkflows.map((row) => [row.workflowKey, row.context].filter(Boolean).join('!')).join(',')
: undefined,
});
+
+ let redirectTo = rawRedirectTo;
+ if (rawRedirectTo) {
+ const { exp, scope: expScope } = await replaceVariables(rawRedirectTo, {
+ variables,
+ localVariables: [...localVariables, { name: '$record', ctx: new Proxy(result?.data?.data?.[0], {}) }],
+ });
+ redirectTo = interpolateVariables(exp, expScope);
+ }
+
if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) {
setVisible?.(false);
}
@@ -704,7 +779,7 @@ export const useCustomizeBulkUpdateActionProps = () => {
if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
- } else if (value != null && value !== '') {
+ } else if (value !== '') {
assignedValues[key] = value;
}
});
@@ -909,7 +984,13 @@ export const useUpdateActionProps = () => {
skipValidator,
triggerWorkflows,
} = actionSchema?.['x-action-settings'] ?? {};
- const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {};
+ const {
+ manualClose,
+ redirecting,
+ redirectTo: rawRedirectTo,
+ successMessage,
+ actionAfterSuccess,
+ } = onSuccess || {};
const assignedValues = {};
const waitList = Object.keys(originalAssignedValues).map(async (key) => {
const value = originalAssignedValues[key];
@@ -926,7 +1007,7 @@ export const useUpdateActionProps = () => {
if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
- } else if (value != null && value !== '') {
+ } else if (value !== '') {
assignedValues[key] = value;
}
});
@@ -948,7 +1029,7 @@ export const useUpdateActionProps = () => {
actionField.data = field.data || {};
actionField.data.loading = true;
try {
- await resource.update({
+ const result = await resource.update({
filterByTk,
values: {
...values,
@@ -967,6 +1048,15 @@ export const useUpdateActionProps = () => {
if (callBack) {
callBack?.();
}
+ let redirectTo = rawRedirectTo;
+ if (rawRedirectTo) {
+ const { exp, scope: expScope } = await replaceVariables(rawRedirectTo, {
+ variables,
+ localVariables: [...localVariables, { name: '$record', ctx: new Proxy(result?.data?.data?.[0], {}) }],
+ });
+ redirectTo = interpolateVariables(exp, expScope);
+ }
+
if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) {
setVisible?.(false);
}
@@ -1153,6 +1243,7 @@ export const useDetailsPaginationProps = () => {
current: ctx.service?.data?.meta?.page || 1,
pageSize: 1,
showSizeChanger: false,
+ align: 'center',
async onChange(page) {
const params = ctx.service?.params?.[0];
ctx.service.run({ ...params, page });
@@ -1178,6 +1269,7 @@ export const useDetailsPaginationProps = () => {
total: count,
pageSize: 1,
showSizeChanger: false,
+ align: 'center',
async onChange(page) {
const params = ctx.service?.params?.[0];
ctx.service.run({ ...params, page });
@@ -1349,6 +1441,7 @@ export const useAssociationFilterBlockProps = () => {
[filterKey]: value,
};
} else {
+ block.clearSelection?.();
if (block.dataLoadingMode === 'manual') {
return block.clearData();
}
@@ -1420,6 +1513,7 @@ export const useAssociationFilterBlockProps = () => {
run,
valueKey,
labelKey,
+ dataScopeFilter: filter,
};
};
async function doReset({
@@ -1442,6 +1536,8 @@ async function doReset({
const target = targets.find((target) => target.uid === block.uid);
if (!target) return;
+ block.clearSelection?.();
+
if (block.dataLoadingMode === 'manual') {
return block.clearData();
}
@@ -1516,7 +1612,7 @@ export const getAppends = ({
const fieldNames = getTargetField(item);
// 只应该收集关系字段,只有大于 1 的时候才是关系字段
- if (fieldNames.length > 1) {
+ if (fieldNames.length > 1 && !item.op) {
appends.add(fieldNames.join('.'));
}
});
diff --git a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx
index 0fb0ad2965..2e44491840 100644
--- a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx
+++ b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx
@@ -25,6 +25,7 @@ import { useCollectionManager_deprecated } from '../hooks';
import useDialect from '../hooks/useDialect';
import * as components from './components';
import { useFieldInterfaceOptions } from './interfaces';
+import { ItemType, MenuItemType } from 'antd/es/menu/interface';
const getSchema = (schema: CollectionFieldInterface, record: any, compile) => {
if (!schema) {
@@ -231,7 +232,7 @@ export const AddFieldAction = (props) => {
}, [getTemplate, record]);
const items = useMemo(() => {
return getFieldOptions()
- .map((option) => {
+ .map((option): ItemType & { title: string; children?: ItemType[] } => {
if (option?.children?.length === 0) {
return null;
}
diff --git a/packages/core/client/src/collection-manager/action-hooks.ts b/packages/core/client/src/collection-manager/action-hooks.ts
index cac488e796..422c207fc5 100644
--- a/packages/core/client/src/collection-manager/action-hooks.ts
+++ b/packages/core/client/src/collection-manager/action-hooks.ts
@@ -157,6 +157,8 @@ export const useCollectionFilterOptions = (collection: any, dataSource?: string)
const option = {
name: field.name,
title: field?.uiSchema?.title || field.name,
+ label: field?.uiSchema?.title || field.name,
+ value: field.name,
schema: field?.uiSchema,
operators:
operators?.filter?.((operator) => {
diff --git a/packages/core/client/src/collection-manager/interfaces/input.ts b/packages/core/client/src/collection-manager/interfaces/input.ts
index ac5897adb7..40d131e5be 100644
--- a/packages/core/client/src/collection-manager/interfaces/input.ts
+++ b/packages/core/client/src/collection-manager/interfaces/input.ts
@@ -62,6 +62,12 @@ export class InputFieldInterface extends CollectionFieldInterface {
hasDefaultValue = true;
properties = {
...defaultProps,
+ trim: {
+ type: 'boolean',
+ 'x-content': '{{t("Automatically remove heading and tailing spaces")}}',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Checkbox',
+ },
layout: {
type: 'void',
title: '{{t("Index")}}',
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 ce0b6d411f..80b4e31527 100644
--- a/packages/core/client/src/collection-manager/interfaces/properties/operators.ts
+++ b/packages/core/client/src/collection-manager/interfaces/properties/operators.ts
@@ -129,12 +129,12 @@ export const enumType = [
label: '{{t("is")}}',
value: '$eq',
selected: true,
- schema: { 'x-component': 'Select' },
+ schema: { 'x-component': 'Select', 'x-component-props': { mode: null } },
},
{
label: '{{t("is not")}}',
value: '$ne',
- schema: { 'x-component': 'Select' },
+ schema: { 'x-component': 'Select', 'x-component-props': { mode: null } },
},
{
label: '{{t("is any of")}}',
diff --git a/packages/core/client/src/collection-manager/interfaces/textarea.ts b/packages/core/client/src/collection-manager/interfaces/textarea.ts
index 26785ed0fb..19f48bc4c1 100644
--- a/packages/core/client/src/collection-manager/interfaces/textarea.ts
+++ b/packages/core/client/src/collection-manager/interfaces/textarea.ts
@@ -28,8 +28,15 @@ export class TextareaFieldInterface extends CollectionFieldInterface {
};
availableTypes = ['text', 'json', 'string'];
hasDefaultValue = true;
+ titleUsable = true;
properties = {
...defaultProps,
+ trim: {
+ type: 'boolean',
+ 'x-content': '{{t("Automatically remove heading and tailing spaces")}}',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Checkbox',
+ },
};
schemaInitialize(schema: ISchema, { block }) {
if (['Table', 'Kanban'].includes(block)) {
diff --git a/packages/core/client/src/collection-manager/mixins/InheritanceCollectionMixin.ts b/packages/core/client/src/collection-manager/mixins/InheritanceCollectionMixin.ts
index 5399efe4c1..5648500ddc 100644
--- a/packages/core/client/src/collection-manager/mixins/InheritanceCollectionMixin.ts
+++ b/packages/core/client/src/collection-manager/mixins/InheritanceCollectionMixin.ts
@@ -7,9 +7,9 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
+import { filter, unionBy, uniq } from 'lodash';
import type { CollectionFieldOptions, GetCollectionFieldPredicate } from '../../data-source';
import { Collection } from '../../data-source/collection/Collection';
-import _, { filter, unionBy, uniq } from 'lodash';
export class InheritanceCollectionMixin extends Collection {
protected parentCollectionsName: string[];
@@ -22,6 +22,7 @@ export class InheritanceCollectionMixin extends Collection {
protected parentCollectionFields: Record = {};
protected allCollectionsInheritChain: string[];
protected inheritCollectionsChain: string[];
+ protected inheritChain: string[];
protected foreignKeyFields: CollectionFieldOptions[];
getParentCollectionsName() {
@@ -233,6 +234,43 @@ export class InheritanceCollectionMixin extends Collection {
return this.inheritCollectionsChain;
}
+ /**
+ * 获取所有祖先数据表和后代数据表,不包括兄弟表。用于下面这些地方:
+ * - 筛选区块链接数据区块时使用
+ */
+ getInheritChain() {
+ if (this.inheritChain) {
+ return this.inheritChain.slice();
+ }
+
+ const ancestorChain = this.getInheritCollectionsChain();
+ const descendantNames = this.getChildrenCollectionsName();
+
+ // 构建最终的链,首先包含祖先链(包括自身)
+ const inheritChain = [...ancestorChain];
+
+ // 再添加直接后代及其后代,但不包括兄弟表
+ const addDescendants = (names: string[]) => {
+ for (const name of names) {
+ if (!inheritChain.includes(name)) {
+ inheritChain.push(name);
+ const childCollection = this.collectionManager.getCollection(name);
+ if (childCollection) {
+ // 递归添加每个后代的后代
+ const childrenNames = childCollection.getChildrenCollectionsName();
+ addDescendants(childrenNames);
+ }
+ }
+ }
+ };
+
+ // 从当前集合的直接后代开始添加
+ addDescendants(descendantNames);
+
+ this.inheritChain = inheritChain;
+ return this.inheritChain;
+ }
+
getAllFields(predicate?: GetCollectionFieldPredicate) {
if (this.allFields) {
return this.allFields.slice();
diff --git a/packages/core/client/src/collection-manager/mixins/__tests__/InheritanceCollectionMixin.test.ts b/packages/core/client/src/collection-manager/mixins/__tests__/InheritanceCollectionMixin.test.ts
new file mode 100644
index 0000000000..b3bd5c4613
--- /dev/null
+++ b/packages/core/client/src/collection-manager/mixins/__tests__/InheritanceCollectionMixin.test.ts
@@ -0,0 +1,189 @@
+import { Application } from '@nocobase/client';
+import { CollectionManager } from '../../../data-source/collection/CollectionManager';
+import { InheritanceCollectionMixin } from '../InheritanceCollectionMixin';
+
+describe('InheritanceCollectionMixin', () => {
+ let app: Application;
+ let collectionManager: CollectionManager;
+
+ beforeEach(() => {
+ app = new Application({
+ dataSourceManager: {
+ collectionMixins: [InheritanceCollectionMixin],
+ },
+ });
+ collectionManager = app.getCollectionManager();
+ });
+
+ describe('getInheritChain', () => {
+ it('should return itself when there are no ancestors or descendants', () => {
+ const options = {
+ name: 'test',
+ fields: [{ name: 'field1', interface: 'input' }],
+ };
+
+ collectionManager.addCollections([options]);
+ const collection = collectionManager.getCollection('test');
+
+ const inheritChain = collection.getInheritChain();
+ expect(inheritChain).toEqual(['test']);
+ });
+
+ it('should return a chain including all ancestor tables', () => {
+ // 创建三代数据表结构:grandparent -> parent -> child
+ const grandparentOptions = {
+ name: 'grandparent',
+ fields: [{ name: 'field1', interface: 'input' }],
+ };
+ const parentOptions = {
+ name: 'parent',
+ inherits: ['grandparent'],
+ fields: [{ name: 'field2', interface: 'input' }],
+ };
+ const childOptions = {
+ name: 'child',
+ inherits: ['parent'],
+ fields: [{ name: 'field3', interface: 'input' }],
+ };
+
+ // 先将所有集合添加到 collectionManager
+ collectionManager.addCollections([grandparentOptions, parentOptions, childOptions]);
+
+ // 获取最终的集合实例以调用方法
+ const child = collectionManager.getCollection('child');
+
+ // 测试 child 的继承链包含所有祖先表
+ const inheritChain = child.getInheritChain();
+ expect(inheritChain).toContain('child');
+ expect(inheritChain).toContain('parent');
+ expect(inheritChain).toContain('grandparent');
+ expect(inheritChain.length).toBe(3);
+ });
+
+ it('should include all descendant tables, but not sibling tables', () => {
+ // 创建具有兄弟和后代关系的数据表结构
+ // parent (祖先表)
+ // |-- child1 (子表)
+ // | |-- grandChild1 (孙表1)
+ // | |-- grandChild2 (孙表2)
+ // |-- child2 (兄弟表)
+ // |-- grandChild3 (兄弟的子表,不应该包括在测试集合的继承链中)
+
+ const collections = [
+ {
+ name: 'parent',
+ fields: [{ name: 'parentField', interface: 'input' }],
+ },
+ {
+ name: 'child1',
+ inherits: ['parent'],
+ fields: [{ name: 'child1Field', interface: 'input' }],
+ },
+ {
+ name: 'child2',
+ inherits: ['parent'],
+ fields: [{ name: 'child2Field', interface: 'input' }],
+ },
+ {
+ name: 'grandChild1',
+ inherits: ['child1'],
+ fields: [{ name: 'grandChild1Field', interface: 'input' }],
+ },
+ {
+ name: 'grandChild2',
+ inherits: ['child1'],
+ fields: [{ name: 'grandChild2Field', interface: 'input' }],
+ },
+ {
+ name: 'grandChild3',
+ inherits: ['child2'],
+ fields: [{ name: 'grandChild3Field', interface: 'input' }],
+ },
+ ];
+
+ // 一次性添加所有集合
+ collectionManager.addCollections(collections);
+
+ // 获取要测试的集合实例
+ const child1 = collectionManager.getCollection('child1');
+
+ // 测试 child1 的继承链
+ const child1InheritChain = child1.getInheritChain();
+
+ // 应该包含自身、父表和子表
+ expect(child1InheritChain).toContain('child1');
+ expect(child1InheritChain).toContain('parent');
+ expect(child1InheritChain).toContain('grandChild1');
+ expect(child1InheritChain).toContain('grandChild2');
+
+ // 不应该包含兄弟表及其子表
+ expect(child1InheritChain).not.toContain('child2');
+ expect(child1InheritChain).not.toContain('grandChild3');
+
+ // 检查总数量是否正确 (parent, child1, grandChild1, grandChild2)
+ expect(child1InheritChain.length).toBe(4);
+ });
+
+ it('should properly handle multiple inheritance', () => {
+ // 创建多重继承的数据表结构
+ // parent1 parent2
+ // \ /
+ // \ /
+ // child
+ // |
+ // grandChild
+
+ const collections = [
+ {
+ name: 'parent1',
+ fields: [{ name: 'parent1Field', interface: 'input' }],
+ },
+ {
+ name: 'parent2',
+ fields: [{ name: 'parent2Field', interface: 'input' }],
+ },
+ {
+ name: 'child',
+ inherits: ['parent1', 'parent2'],
+ fields: [{ name: 'childField', interface: 'input' }],
+ },
+ {
+ name: 'grandChild',
+ inherits: ['child'],
+ fields: [{ name: 'grandChildField', interface: 'input' }],
+ },
+ ];
+
+ // 一次性添加所有集合
+ collectionManager.addCollections(collections);
+
+ // 获取要测试的集合实例
+ const child = collectionManager.getCollection('child');
+ const grandChild = collectionManager.getCollection('grandChild');
+
+ // 测试 child 的继承链
+ const childInheritChain = child.getInheritChain();
+
+ // 应该包含自身、两个父表和子表
+ expect(childInheritChain).toContain('child');
+ expect(childInheritChain).toContain('parent1');
+ expect(childInheritChain).toContain('parent2');
+ expect(childInheritChain).toContain('grandChild');
+
+ // 检查总数量是否正确 (child, parent1, parent2, grandChild)
+ expect(childInheritChain.length).toBe(4);
+
+ // 测试 grandChild 的继承链
+ const grandChildInheritChain = grandChild.getInheritChain();
+
+ // 应该包含自身及所有祖先表
+ expect(grandChildInheritChain).toContain('grandChild');
+ expect(grandChildInheritChain).toContain('child');
+ expect(grandChildInheritChain).toContain('parent1');
+ expect(grandChildInheritChain).toContain('parent2');
+
+ // 检查总数量是否正确 (grandChild, child, parent1, parent2)
+ expect(grandChildInheritChain.length).toBe(4);
+ });
+ });
+});
diff --git a/packages/core/client/src/collection-manager/templates/components/PresetFields.tsx b/packages/core/client/src/collection-manager/templates/components/PresetFields.tsx
index 675617818e..5f52ca897b 100644
--- a/packages/core/client/src/collection-manager/templates/components/PresetFields.tsx
+++ b/packages/core/client/src/collection-manager/templates/components/PresetFields.tsx
@@ -96,7 +96,7 @@ export const PresetFields = observer(
rowSelection={{
type: 'checkbox',
selectedRowKeys,
- getCheckboxProps: (record) => ({
+ getCheckboxProps: (record: { name: string }) => ({
name: record.name,
disabled: props?.disabled || props?.presetFieldsDisabledIncludes?.includes?.(record.name),
}),
diff --git a/packages/core/client/src/common/AppNotFound.tsx b/packages/core/client/src/common/AppNotFound.tsx
new file mode 100644
index 0000000000..1b5f84c7d5
--- /dev/null
+++ b/packages/core/client/src/common/AppNotFound.tsx
@@ -0,0 +1,30 @@
+/**
+ * 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 { Button, Result } from 'antd';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router-dom';
+
+export const AppNotFound = () => {
+ const navigate = useNavigate();
+ const { t } = useTranslation();
+ return (
+ navigate('/', { replace: true })} type="primary">
+ Back Home
+
+ }
+ />
+ );
+};
diff --git a/packages/core/client/src/common/SelectWithTitle.tsx b/packages/core/client/src/common/SelectWithTitle.tsx
index 4bd43648a3..b3e0be59a8 100644
--- a/packages/core/client/src/common/SelectWithTitle.tsx
+++ b/packages/core/client/src/common/SelectWithTitle.tsx
@@ -18,7 +18,14 @@ export interface SelectWithTitleProps {
onChange?: (...args: any[]) => void;
}
-export function SelectWithTitle({ title, defaultValue, onChange, options, fieldNames }: SelectWithTitleProps) {
+export function SelectWithTitle({
+ title,
+ defaultValue,
+ onChange,
+ options,
+ fieldNames,
+ ...others
+}: SelectWithTitleProps) {
const [open, setOpen] = useState(false);
const timerRef = useRef(null);
return (
@@ -36,6 +43,7 @@ export function SelectWithTitle({ title, defaultValue, onChange, options, fieldN
>
{title}
{
document.body.style.setProperty('--colorWarningBg', token.colorWarningBg);
document.body.style.setProperty('--colorWarningBorder', token.colorWarningBorder);
document.body.style.setProperty('--colorText', token.colorText);
+ document.body.style.setProperty('--colorTextHeaderMenu', token.colorTextHeaderMenu);
document.body.style.setProperty('--colorPrimaryText', token.colorPrimaryText);
document.body.style.setProperty('--colorPrimaryTextActive', token.colorPrimaryTextActive);
document.body.style.setProperty('--colorPrimaryTextHover', token.colorPrimaryTextHover);
@@ -46,6 +47,7 @@ export const CSSVariableProvider = ({ children }) => {
document.body.style.setProperty('--colorBgScrollBarActive', colorBgScrollBarActive);
document.body.style.setProperty('--colorSettings', token.colorSettings || defaultTheme.token.colorSettings);
document.body.style.setProperty('--colorBgSettingsHover', token.colorBgSettingsHover);
+ document.body.style.setProperty('--colorTemplateBgSettingsHover', token.colorTemplateBgSettingsHover);
document.body.style.setProperty('--colorBorderSettingsHover', token.colorBorderSettingsHover);
document.body.style.setProperty('--colorBgMenuItemSelected', token.colorBgHeaderMenuActive);
@@ -59,6 +61,7 @@ export const CSSVariableProvider = ({ children }) => {
token.colorBgContainer,
token.colorBgLayout,
token.colorBgSettingsHover,
+ token.colorTemplateBgSettingsHover,
token.colorBorderSettingsHover,
token.colorInfoBg,
token.colorInfoBorder,
@@ -75,6 +78,7 @@ export const CSSVariableProvider = ({ children }) => {
token.marginXS,
token.paddingContentVerticalSM,
token.sizeXXL,
+ token.colorTextHeaderMenu,
]);
return children;
diff --git a/packages/core/client/src/data-source/collection-field/CollectionField.tsx b/packages/core/client/src/data-source/collection-field/CollectionField.tsx
index 4bb5f6df4f..532a27666f 100644
--- a/packages/core/client/src/data-source/collection-field/CollectionField.tsx
+++ b/packages/core/client/src/data-source/collection-field/CollectionField.tsx
@@ -18,6 +18,7 @@ import { useCollectionFieldUISchema, useIsInNocoBaseRecursionFieldContext } from
import { useDynamicComponentProps } from '../../hoc/withDynamicSchemaProps';
import { useCompile, useComponent } from '../../schema-component';
import { useIsAllowToSetDefaultValue } from '../../schema-settings/hooks/useIsAllowToSetDefaultValue';
+import { isVariable } from '../../variables/utils/isVariable';
import { CollectionFieldProvider, useCollectionField } from './CollectionFieldProvider';
type Props = {
@@ -83,6 +84,8 @@ const CollectionFieldInternalField_deprecated: React.FC = (props: Props) => {
setRequired(field, fieldSchema, uiSchema);
// @ts-ignore
field.dataSource = uiSchema.enum;
+ field.data = field.data || {};
+ field.data.dataSource = uiSchema?.enum;
const originalProps = compile(uiSchema['x-component-props']) || {};
field.componentProps = merge(originalProps, field.componentProps || {}, dynamicProps || {});
}, [uiSchemaOrigin]);
@@ -102,17 +105,45 @@ const CollectionFieldInternalField = (props) => {
const dynamicProps = useDynamicComponentProps(uiSchema?.['x-use-component-props'], props);
useEffect(() => {
- // There seems to be a bug in formily where after setting a field to readPretty, switching to editable,
- // then back to readPretty, and refreshing the page, the field remains in editable state. The expected state is readPretty.
- // This code is meant to fix this issue.
+ /**
+ * There seems to be a bug in formily where after setting a field to readPretty, switching to editable,
+ * then back to readPretty, and refreshing the page, the field remains in editable state. The expected state is readPretty.
+ * This code is meant to fix this issue.
+ */
if (fieldSchema['x-read-pretty'] === true && !field.readPretty) {
field.readPretty = true;
}
+
+ /**
+ * This solves the issue: After creating a form and setting a field to "read-only", the field remains editable when refreshing the page and reopening the dialog.
+ *
+ * Note: This might be a bug in Formily
+ * When both x-disabled and x-read-pretty exist in the Schema:
+ * - If x-disabled appears before x-read-pretty in the Schema JSON, the disabled state becomes ineffective
+ * - The reason is that during field instance initialization, field.disabled is set before field.readPretty, which causes the pattern value to be changed to 'editable'
+ * - This issue is related to the order of JSON fields, which might return different orders in different environments (databases), thus making the issue inconsistent to reproduce
+ *
+ * Reference to Formily source code:
+ * 1. Setting readPretty may cause pattern to be changed to 'editable': https://github.com/alibaba/formily/blob/d4bb96c40e7918210b1bd7d57b8fadee0cfe4b26/packages/core/src/models/BaseField.ts#L208-L224
+ * 2. The execution order of the each method depends on the order of JSON fields: https://github.com/alibaba/formily/blob/123d536b6076196e00b4e02ee160d72480359f54/packages/json-schema/src/schema.ts#L486-L519
+ */
+ if (fieldSchema['x-disabled'] === true) {
+ field.disabled = true;
+ }
+ field.data = field.data || {};
+ field.data.dataSource = uiSchema?.enum;
}, [field, fieldSchema]);
if (!uiSchema) return null;
- return ;
+ const mergedProps = { ...props, ...dynamicProps };
+
+ // Prevent displaying the variable string first, then the variable value
+ if (isVariable(mergedProps.value) && mergedProps.value === fieldSchema.default) {
+ mergedProps.value = undefined;
+ }
+
+ return ;
};
export const CollectionField = connect((props) => {
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 89a9786308..1102d5825c 100644
--- a/packages/core/client/src/data-source/data-block/DataBlockProvider.tsx
+++ b/packages/core/client/src/data-source/data-block/DataBlockProvider.tsx
@@ -23,6 +23,7 @@ import {
import { CollectionRecord } from '../collection-record';
import { BlockRequestProvider } from './DataBlockRequestProvider';
import { DataBlockResourceProvider } from './DataBlockResourceProvider';
+import { BlockLinkageRuleProvider } from '../../modules/blocks/BlockLinkageRuleProvider';
export interface AllDataBlockProps {
collection: string | CollectionOptions;
@@ -189,13 +190,15 @@ export const DataBlockProvider: FC> = withDynamicSche
-
-
-
- {children}
-
-
-
+
+
+
+
+ {children}
+
+
+
+
diff --git a/packages/core/client/src/data-source/data-source/DataSourceManagerProvider.tsx b/packages/core/client/src/data-source/data-source/DataSourceManagerProvider.tsx
index 547c038e0e..578b447868 100644
--- a/packages/core/client/src/data-source/data-source/DataSourceManagerProvider.tsx
+++ b/packages/core/client/src/data-source/data-source/DataSourceManagerProvider.tsx
@@ -29,7 +29,7 @@ export function useDataSourceManager() {
}
/**
- * 获取当前 collection 继承链路上的所有 collection
+ * 获取当前 collection 继承链路上的所有 collection(不包括兄弟表)
* @returns
*/
export function useAllCollectionsInheritChainGetter() {
@@ -39,7 +39,7 @@ export function useAllCollectionsInheritChainGetter() {
return dm
?.getDataSource(customDataSource)
?.collectionManager?.getCollection(collectionName)
- ?.getAllCollectionsInheritChain();
+ ?.getInheritChain();
},
[dm],
);
diff --git a/packages/core/client/src/demo-utils/dataSourceMainCollections.json b/packages/core/client/src/demo-utils/dataSourceMainCollections.json
index 848a38cdbc..c9c823d92a 100644
--- a/packages/core/client/src/demo-utils/dataSourceMainCollections.json
+++ b/packages/core/client/src/demo-utils/dataSourceMainCollections.json
@@ -241,36 +241,6 @@
}
}
},
- {
- "key": "qe7b1rsct5h",
- "name": "jobs",
- "type": "belongsToMany",
- "interface": null,
- "description": null,
- "collectionName": "users",
- "parentKey": null,
- "reverseKey": null,
- "through": "users_jobs",
- "foreignKey": "userId",
- "sourceKey": "id",
- "otherKey": "jobId",
- "targetKey": "id",
- "target": "jobs"
- },
- {
- "key": "vt0n1l1ruyz",
- "name": "usersJobs",
- "type": "hasMany",
- "interface": null,
- "description": null,
- "collectionName": "users",
- "parentKey": null,
- "reverseKey": null,
- "target": "users_jobs",
- "foreignKey": "userId",
- "sourceKey": "id",
- "targetKey": "id"
- },
{
"key": "ekol7p60nry",
"name": "sortName",
diff --git a/packages/core/client/src/filter-provider/FilterProvider.tsx b/packages/core/client/src/filter-provider/FilterProvider.tsx
index 6dab9105e0..9b5e821195 100644
--- a/packages/core/client/src/filter-provider/FilterProvider.tsx
+++ b/packages/core/client/src/filter-provider/FilterProvider.tsx
@@ -19,6 +19,7 @@ import { mergeFilter, useAssociatedFields } from './utils';
// @ts-ignore
import React, { createContext, useCallback, useEffect, useMemo, useRef } from 'react';
+import { useAllDataBlocks } from '../schema-component/antd/page/AllDataBlocksProvider';
enum FILTER_OPERATOR {
AND = '$and',
@@ -54,6 +55,8 @@ export interface DataBlock {
clearFilter: (uid: string) => void;
/** 将数据区块的数据置为空 */
clearData: () => void;
+ /** 清除表格的选中项 */
+ clearSelection?: () => void;
/** 数据区块表中所有的关系字段 */
associatedFields?: CollectionFieldOptions_deprecated[];
/** 数据区块表中所有的外键字段 */
@@ -69,6 +72,10 @@ export interface DataBlock {
* manual: 只有当点击了筛选按钮,才会请求数据
*/
dataLoadingMode?: 'auto' | 'manual';
+ /** 让整个区块悬浮起来 */
+ highlightBlock: () => void;
+ /** 取消悬浮 */
+ unhighlightBlock: () => void;
}
interface FilterContextValue {
@@ -122,7 +129,7 @@ export const DataBlockCollector = ({
const field = useField();
const fieldSchema = useFieldSchema();
const associatedFields = useAssociatedFields();
- const container = useRef(null);
+ const container = useRef(null);
const dataLoadingMode = useDataLoadingMode();
const shouldApplyFilter =
@@ -165,16 +172,49 @@ export const DataBlockCollector = ({
clearData() {
this.service.mutate(undefined);
},
+ clearSelection() {
+ if (field) {
+ field.data?.clearSelectedRowKeys?.();
+ }
+ },
+ highlightBlock() {
+ const dom = container.current;
+
+ if (!dom) return;
+
+ const designer = dom.querySelector('.ant-nb-schema-toolbar');
+ if (designer) {
+ designer.classList.remove(process.env.__E2E__ ? 'hidden-e2e' : 'hidden');
+ }
+ dom.style.boxShadow = '0 3px 12px rgba(0, 0, 0, 0.15)';
+ dom.style.transition = 'box-shadow 0.3s ease, transform 0.2s ease';
+ dom.scrollIntoView?.({
+ behavior: 'smooth',
+ block: 'start',
+ });
+ },
+ unhighlightBlock() {
+ const dom = container.current;
+
+ if (!dom) return;
+
+ const designer = dom.querySelector('.ant-nb-schema-toolbar');
+ if (designer) {
+ designer.classList.add(process.env.__E2E__ ? 'hidden-e2e' : 'hidden');
+ }
+ dom.style.boxShadow = 'none';
+ dom.style.transition = 'box-shadow 0.3s ease, transform 0.2s ease';
+ }
});
}, [
associatedFields,
collection,
dataLoadingMode,
- field?.componentProps?.title,
fieldSchema,
params?.filter,
recordDataBlocks,
getDataBlockRequest,
+ field,
]);
useEffect(() => {
@@ -190,12 +230,14 @@ export const DataBlockCollector = ({
*/
export const useFilterBlock = () => {
const ctx = React.useContext(FilterContext);
+ const allDataBlocksCtx = useAllDataBlocks();
// 有可能存在页面没有提供 FilterBlockProvider 的情况,比如内部使用的数据表管理页面
const getDataBlocks = useCallback<() => DataBlock[]>(() => ctx?.getDataBlocks() || [], [ctx]);
const recordDataBlocks = useCallback(
(block: DataBlock) => {
+ allDataBlocksCtx.recordDataBlocks(block);
const existingBlock = ctx?.getDataBlocks().find((item) => item.uid === block.uid);
if (existingBlock) {
@@ -211,6 +253,7 @@ export const useFilterBlock = () => {
const removeDataBlock = useCallback(
(uid: string) => {
+ allDataBlocksCtx.removeDataBlock(uid);
if (ctx?.getDataBlocks().every((item) => item.uid !== uid)) return;
ctx?.setDataBlocks((prev) => prev.filter((item) => item.uid !== uid));
},
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 be25dce192..f8340bd2a5 100644
--- a/packages/core/client/src/filter-provider/__tests__/utiles.test.ts
+++ b/packages/core/client/src/filter-provider/__tests__/utiles.test.ts
@@ -67,6 +67,108 @@ describe('getSupportFieldsByAssociation', () => {
});
describe('getSupportFieldsByForeignKey', () => {
+ it('should return foreign key fields matching both name and target collection', () => {
+ const filterBlockCollection = {
+ fields: [
+ { id: 1, name: 'field1', type: 'hasMany', foreignKey: 'fk1', target: 'collection1' },
+ { id: 2, name: 'field2', type: 'hasMany', foreignKey: 'fk2', target: 'collection2' },
+ { id: 3, name: 'field3', type: 'hasMany', foreignKey: 'fk3', target: 'collection3' },
+ ],
+ };
+
+ const block = {
+ foreignKeyFields: [
+ { id: 1, name: 'fk1', collectionName: 'collection1' },
+ { id: 2, name: 'fk2', collectionName: 'collection2' },
+ { id: 3, name: 'fk3', collectionName: 'collection3' },
+ ],
+ };
+
+ const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any);
+
+ expect(result).toEqual([
+ { id: 1, name: 'fk1', collectionName: 'collection1' },
+ { id: 2, name: 'fk2', collectionName: 'collection2' },
+ { id: 3, name: 'fk3', collectionName: 'collection3' },
+ ]);
+ });
+
+ it("should not return foreign key fields when target collection doesn't match", () => {
+ const filterBlockCollection = {
+ fields: [
+ { id: 1, name: 'field1', type: 'hasMany', foreignKey: 'fk1', target: 'collection1' },
+ { id: 2, name: 'field2', type: 'hasMany', foreignKey: 'fk2', target: 'collectionX' }, // target不匹配
+ { id: 3, name: 'field3', type: 'hasMany', foreignKey: 'fk3', target: 'collection3' },
+ ],
+ };
+
+ const block = {
+ foreignKeyFields: [
+ { id: 1, name: 'fk1', collectionName: 'collection1' },
+ { id: 2, name: 'fk2', collectionName: 'collection2' }, // 与field2的target不匹配
+ { id: 3, name: 'fk3', collectionName: 'collection3' },
+ ],
+ };
+
+ const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any);
+
+ expect(result).toEqual([
+ { id: 1, name: 'fk1', collectionName: 'collection1' },
+ { id: 3, name: 'fk3', collectionName: 'collection3' },
+ ]);
+ });
+
+ it('should filter out belongsTo type fields', () => {
+ const filterBlockCollection = {
+ fields: [
+ { id: 1, name: 'field1', type: 'hasMany', foreignKey: 'fk1', target: 'collection1' },
+ { id: 2, name: 'field2', type: 'belongsTo', foreignKey: 'fk2', target: 'collection2' }, // belongsTo类型
+ { id: 3, name: 'field3', type: 'hasMany', foreignKey: 'fk3', target: 'collection3' },
+ ],
+ };
+
+ const block = {
+ foreignKeyFields: [
+ { id: 1, name: 'fk1', collectionName: 'collection1' },
+ { id: 2, name: 'fk2', collectionName: 'collection2' },
+ { id: 3, name: 'fk3', collectionName: 'collection3' },
+ ],
+ };
+
+ const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any);
+
+ expect(result).toEqual([
+ { id: 1, name: 'fk1', collectionName: 'collection1' },
+ { id: 3, name: 'fk3', collectionName: 'collection3' },
+ ]);
+ });
+
+ it('should handle when both name and target collection match', () => {
+ const filterBlockCollection = {
+ fields: [
+ { id: 1, name: 'field1', type: 'hasMany', foreignKey: 'fk1', target: 'collection1' },
+ { id: 2, name: 'field2', type: 'hasOne', foreignKey: 'fk2', target: 'collection2' },
+ { id: 3, name: 'field3', type: 'hasMany', foreignKey: 'fk3', target: 'wrongCollection' }, // 目标表不匹配
+ ],
+ };
+
+ const block = {
+ foreignKeyFields: [
+ { id: 1, name: 'fk1', collectionName: 'collection1' },
+ { id: 2, name: 'fk2', collectionName: 'collection2' },
+ { id: 3, name: 'fk3', collectionName: 'collection3' }, // 与field3的target不匹配
+ ],
+ };
+
+ const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any);
+
+ expect(result).toEqual([
+ { id: 1, name: 'fk1', collectionName: 'collection1' },
+ { id: 2, name: 'fk2', collectionName: 'collection2' },
+ ]);
+ });
+
+ // 保留原有的通用测试用例
it("should return all foreign key fields matching the filter block collection's foreign key properties", () => {
const filterBlockCollection = {
fields: [
diff --git a/packages/core/client/src/filter-provider/highlightBlock.ts b/packages/core/client/src/filter-provider/highlightBlock.ts
new file mode 100644
index 0000000000..1e02301461
--- /dev/null
+++ b/packages/core/client/src/filter-provider/highlightBlock.ts
@@ -0,0 +1,47 @@
+let container: HTMLElement | null = null;
+
+export const highlightBlock = (clonedBlockDom: HTMLElement, boxRect: DOMRect) => {
+ if (!container) {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ container.style.position = 'absolute';
+ container.style.transition = 'opacity 0.3s ease';
+ container.style.pointerEvents = 'none';
+ }
+
+ container.appendChild(clonedBlockDom);
+ container.style.opacity = '1';
+ container.style.width = `${boxRect.width}px`;
+ container.style.height = `${boxRect.height}px`;
+ container.style.top = `${boxRect.top}px`;
+ container.style.left = `${boxRect.left}px`;
+ container.style.zIndex = '2000';
+}
+
+export const unhighlightBlock = () => {
+ if (container) {
+ container.style.opacity = '0';
+ container.innerHTML = '';
+ }
+}
+
+export const startScrollEndTracking = (dom: HTMLElement & { _prevRect?: DOMRect; _timer?: any }, callback: () => void) => {
+ dom._timer = setInterval(() => {
+ const prevRect = dom._prevRect;
+ const currentRect = dom.getBoundingClientRect();
+
+ if (!prevRect || currentRect.top !== prevRect.top) {
+ dom._prevRect = currentRect;
+ } else {
+ clearInterval(dom._timer);
+ callback();
+ }
+ }, 100)
+}
+
+export const stopScrollEndTracking = (dom: HTMLElement & { _timer?: any }) => {
+ if (dom._timer) {
+ clearInterval(dom._timer);
+ dom._timer = null;
+ }
+}
diff --git a/packages/core/client/src/filter-provider/utils.ts b/packages/core/client/src/filter-provider/utils.ts
index 17dfe9df44..c2759df6ed 100644
--- a/packages/core/client/src/filter-provider/utils.ts
+++ b/packages/core/client/src/filter-provider/utils.ts
@@ -49,9 +49,13 @@ export const getSupportFieldsByAssociation = (inheritCollectionsChain: string[],
export const getSupportFieldsByForeignKey = (filterBlockCollection: Collection, block: DataBlock) => {
return block.foreignKeyFields?.filter((foreignKeyField) => {
- return filterBlockCollection.fields.some(
- (field) => field.type !== 'belongsTo' && field.foreignKey === foreignKeyField.name,
- );
+ return filterBlockCollection.fields.some((field) => {
+ return (
+ field.type !== 'belongsTo' &&
+ field.foreignKey === foreignKeyField.name && // 1. 外键字段的 name 要一致
+ field.target === foreignKeyField.collectionName // 2. 关系字段的目标表要和外键的数据表一致
+ );
+ });
});
};
@@ -193,19 +197,21 @@ export const useFilterAPI = () => {
const doFilter = useCallback(
(
- value: any | ((target: FilterTarget['targets'][0], block: DataBlock) => any),
+ value: any | ((target: FilterTarget['targets'][0], block: DataBlock, sourceKey?: string) => any),
field: string | ((target: FilterTarget['targets'][0], block: DataBlock) => string) = 'id',
operator: string | ((target: FilterTarget['targets'][0]) => string) = '$eq',
) => {
+ const currentBlock = dataBlocks.find((block) => block.uid === fieldSchema.parent['x-uid']);
dataBlocks.forEach((block) => {
+ let key = field as string;
const target = targets.find((target) => target.uid === block.uid);
if (!target) return;
if (_.isFunction(value)) {
- value = value(target, block);
+ value = value(target, block, getSourceKey(currentBlock, target.field));
}
if (_.isFunction(field)) {
- field = field(target, block);
+ key = field(target, block);
}
if (_.isFunction(operator)) {
operator = operator(target);
@@ -215,18 +221,22 @@ export const useFilterAPI = () => {
// 保留原有的 filter
const storedFilter = block.service.params?.[1]?.filters || {};
- if (value !== undefined) {
+ if (value != null) {
storedFilter[uid] = {
$and: [
{
- [field]: {
+ [key]: {
[operator]: value,
},
},
],
};
} else {
+ block.clearSelection?.();
delete storedFilter[uid];
+ if (block.dataLoadingMode === 'manual') {
+ return block.clearData();
+ }
}
const mergedFilter = mergeFilter([
@@ -244,7 +254,7 @@ export const useFilterAPI = () => {
);
});
},
- [dataBlocks, targets, uid],
+ [dataBlocks, targets, uid, fieldSchema],
);
return {
@@ -264,3 +274,8 @@ export const isInFilterFormBlock = (fieldSchema: Schema) => {
}
return false;
};
+
+function getSourceKey(currentBlock: DataBlock, field: string) {
+ const associationField = currentBlock?.associatedFields?.find((item) => item.foreignKey === field);
+ return associationField?.sourceKey || field?.split?.('.')?.[1];
+}
diff --git a/packages/core/client/src/formily/NocoBaseRecursionField.tsx b/packages/core/client/src/formily/NocoBaseRecursionField.tsx
index c2c75af9bb..cf0d00c880 100644
--- a/packages/core/client/src/formily/NocoBaseRecursionField.tsx
+++ b/packages/core/client/src/formily/NocoBaseRecursionField.tsx
@@ -48,6 +48,7 @@ interface INocoBaseRecursionFieldProps extends IRecursionFieldProps {
* Whether to use Formily Field class - performance will be reduced but provides better compatibility with Formily
*/
isUseFormilyField?: boolean;
+ parentSchema?: Schema;
}
const CollectionFieldUISchemaContext = React.createContext({});
@@ -266,6 +267,7 @@ export const NocoBaseRecursionField: ReactFC = Rea
values,
isUseFormilyField = true,
uiSchema,
+ parentSchema,
} = props;
const basePath = useBasePath(props);
const newFieldSchemaRef = useRef(null);
@@ -279,6 +281,14 @@ export const NocoBaseRecursionField: ReactFC = Rea
const fieldSchema: Schema = newFieldSchemaRef.current || oldFieldSchema;
+ // Establish connection with the Schema tree
+ if (!fieldSchema.parent && parentSchema) {
+ fieldSchema.parent = parentSchema;
+ if (!fieldSchema.parent?.properties?.[fieldSchema.name] && fieldSchema.name) {
+ _.set(fieldSchema.parent, `properties.${fieldSchema.name}`, fieldSchema);
+ }
+ }
+
const refresh = useCallback(() => {
const parent = fieldSchema.parent;
newFieldSchemaRef.current = new Schema(fieldSchema.toJSON(), parent);
diff --git a/packages/core/client/src/global-theme/defaultTheme.ts b/packages/core/client/src/global-theme/defaultTheme.ts
index 990958a4e0..78c11135cf 100644
--- a/packages/core/client/src/global-theme/defaultTheme.ts
+++ b/packages/core/client/src/global-theme/defaultTheme.ts
@@ -24,11 +24,13 @@ const defaultTheme: ThemeConfig = {
// UI 配置组件
colorSettings: '#F18B62',
colorBgSettingsHover: 'rgba(241, 139, 98, 0.06)',
+ colorTemplateBgSettingsHover: 'rgba(98, 200, 241, 0.06)', // 默认为colorBgSettingsHover的互补色
colorBorderSettingsHover: 'rgba(241, 139, 98, 0.3)',
// 动画相关
motionUnit: 0.03,
- motion: !process.env.__E2E__,
+ // ant design 升级到5.24.2后,Modal.confirm在E2E中如果关闭动画,会出现ant-modal-mask不销毁的问题
+ // motion: !process.env.__E2E__,
},
};
diff --git a/packages/core/client/src/global-theme/type.ts b/packages/core/client/src/global-theme/type.ts
index d41b7ffee1..1a2377962d 100644
--- a/packages/core/client/src/global-theme/type.ts
+++ b/packages/core/client/src/global-theme/type.ts
@@ -30,6 +30,8 @@ export interface CustomToken extends AliasToken {
colorSettings: string;
/** 鼠标悬浮时显示的背景色 */
colorBgSettingsHover: string;
+ /** 鼠标悬浮模板区块时显示的背景色 */
+ colorTemplateBgSettingsHover: string;
/** 鼠标悬浮时显示的边框色 */
colorBorderSettingsHover: string;
@@ -47,6 +49,8 @@ export interface CustomToken extends AliasToken {
marginBlock: number;
/** 区块的圆角 */
borderRadiusBlock: number;
+
+ siderWidth: number;
}
export interface ThemeConfig extends _ThemeConfig {
diff --git a/packages/core/client/src/i18n/constant.ts b/packages/core/client/src/i18n/constant.ts
new file mode 100644
index 0000000000..e8dc4848b9
--- /dev/null
+++ b/packages/core/client/src/i18n/constant.ts
@@ -0,0 +1,12 @@
+/**
+ * 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 NAMESPACE_UI_SCHEMA = 'ui-schema-storage';
+
+export { NAMESPACE_UI_SCHEMA };
diff --git a/packages/core/client/src/i18n/index.ts b/packages/core/client/src/i18n/index.ts
index 5d83484458..4ca37cc1c6 100644
--- a/packages/core/client/src/i18n/index.ts
+++ b/packages/core/client/src/i18n/index.ts
@@ -8,3 +8,4 @@
*/
export * from './i18n';
+export * from './constant';
diff --git a/packages/core/client/src/locale/de-DE.json b/packages/core/client/src/locale/de-DE.json
new file mode 100644
index 0000000000..e5f3529d05
--- /dev/null
+++ b/packages/core/client/src/locale/de-DE.json
@@ -0,0 +1,895 @@
+{
+ "Display <1><0>100><1>201><2>502><3>1003>1> items per page": "<1><0>100><1>201><2>502><3>1003>1> Einträge pro Seite anzeigen",
+ "Meet <1><0>All0><1>Any1>1> conditions in the group": "<1><0>Alle0><1>Beliebige1>1> Bedingungen in der Gruppe erfüllen",
+ "Open in<1><0>Modal0><1>Drawer1><2>Window2>1>": "Öffnen in<1><0>Modal0><1>Seitenleiste1><2>Fenster2>1>",
+ "{{count}} filter items": "{{count}} Filterelemente",
+ "{{count}} more items": "{{count}} weitere Einträge",
+ "Total {{count}} items": "Insgesamt {{count}} Einträge",
+ "Today": "Heute",
+ "Yesterday": "Gestern",
+ "Tomorrow": "Morgen",
+ "Month": "Monat",
+ "Week": "Woche",
+ "This week": "Diese Woche",
+ "This month": "Dieser Monat",
+ "This year": "Dieses Jahr",
+ "Next year": "Nächstes Jahr",
+ "Last week": "Letzte Woche",
+ "Next week": "Nächste Woche",
+ "Last month": "Letzter Monat",
+ "Next month": "Nächster Monat",
+ "Last quarter": "Letztes Quartal",
+ "This quarter": "Dieses Quartal",
+ "Next quarter": "Nächstes Quartal",
+ "Last year": "Letztes Jahr",
+ "Last 7 days": "Letzte 7 Tage",
+ "Last 30 days": "Letzte 30 Tage",
+ "Last 90 days": "Letzte 90 Tage",
+ "Next 7 days": "Nächste 7 Tage",
+ "Next 30 days": "Nächste 30 Tage",
+ "Next 90 days": "Nächste 90 Tage",
+ "Work week": "Arbeitswoche",
+ "Day": "Tag",
+ "Agenda": "Agenda",
+ "Date": "Datum",
+ "Time": "Zeit",
+ "Event": "Ereignis",
+ "None": "Keine",
+ "Unconnected": "Nicht verbunden",
+ "System settings": "Systemeinstellungen",
+ "System title": "Systemtitel",
+ "Settings": "Einstellungen",
+ "Logo": "Logo",
+ "Add menu item": "Menüpunkt hinzufügen",
+ "Page": "Seite",
+ "Name": "Name",
+ "Icon": "Symbol",
+ "Group": "Gruppe",
+ "Link": "Link",
+ "Tab": "Tab",
+ "Save conditions": "Bedingungen speichern",
+ "Edit menu item": "Menüpunkt bearbeiten",
+ "Move to": "Verschieben nach",
+ "Insert left": "Links einfügen",
+ "Insert right": "Rechts einfügen",
+ "Insert inner": "Innen einfügen",
+ "Delete": "Löschen",
+ "Disassociate": "Trennen",
+ "Disassociate record": "Datensatz trennen",
+ "Are you sure you want to disassociate it?": "Sind Sie sicher, dass Sie die Verbindung trennen möchten?",
+ "UI editor": "UI-Editor",
+ "Collection": "Sammlung",
+ "Collection selector": "Sammlungsauswahl",
+ "Providing certain collections as options for users, typically used in polymorphic or inheritance scenarios": "Bestimmte Sammlungen als Optionen für Benutzer bereitstellen, typischerweise verwendet in polymorphen oder Vererbungsszenarien",
+ "Collections & Fields": "Sammlungen & Felder",
+ "All collections": "Alle Sammlungen",
+ "Add category": "Kategorie hinzufügen",
+ "Enable child collections": "Untersammlungen aktivieren",
+ "Allow adding records to the current collection": "Hinzufügen von Datensätzen zur aktuellen Sammlung erlauben",
+ "Delete category": "Kategorie löschen",
+ "Edit category": "Kategorie bearbeiten",
+ "Collection category": "Sammlungskategorie",
+ "Collection template": "Sammlungsvorlage",
+ "Sort": "Sortieren",
+ "Categories": "Kategorien",
+ "Visible": "Sichtbar",
+ "Read only": "Nur lesen",
+ "Easy reading": "Leicht lesbar",
+ "Hidden": "Versteckt",
+ "Hidden(reserved value)": "Versteckt (reservierter Wert)",
+ "Not required": "Nicht erforderlich",
+ "Value": "Wert",
+ "Disabled": "Deaktiviert",
+ "Enabled": "Aktiviert",
+ "Problematic": "Problematisch",
+ "Setting": "Einstellung",
+ "On": "Ein",
+ "Off": "Aus",
+ "Empty": "Leer",
+ "Linkage rule": "Verknüpfungsregel",
+ "Linkage rules": "Verknüpfungsregeln",
+ "Condition": "Bedingung",
+ "Properties": "Eigenschaften",
+ "Add linkage rule": "Verknüpfungsregel hinzufügen",
+ "Add property": "Eigenschaft hinzufügen",
+ "Category name": "Kategoriename",
+ "Roles & Permissions": "Rollen & Berechtigungen",
+ "Edit profile": "Profil bearbeiten",
+ "Change password": "Passwort ändern",
+ "Old password": "Altes Passwort",
+ "New password": "Neues Passwort",
+ "Switch role": "Rolle wechseln",
+ "Super admin": "Superadministrator",
+ "Language": "Sprache",
+ "Allow sign up": "Registrierung erlauben",
+ "Enable SMS authentication": "SMS-Authentifizierung aktivieren",
+ "Sign out": "Abmelden",
+ "Cancel": "Abbrechen",
+ "Submit": "Absenden",
+ "Close": "Schließen",
+ "Set the data scope": "Datenbereich festlegen",
+ "Set data loading mode": "Datenladungsmodus festlegen",
+ "Load all data when filter is empty": "Alle Daten laden, wenn der Filter leer ist",
+ "Do not load data when filter is empty": "Keine Daten laden, wenn der Filter leer ist",
+ "Data loading mode": "Datenladungsmodus",
+ "Data blocks": "Datenblöcke",
+ "Filter blocks": "Filterblöcke",
+ "Table": "Tabelle",
+ "Table OID(Inheritance)": "Tabellen-OID (Vererbung)",
+ "Form": "Formular",
+ "List": "Liste",
+ "Grid Card": "Rasterkarte",
+ "pixels": "Pixel",
+ "Screen size": "Bildschirmgröße",
+ "Display title": "Titel anzeigen",
+ "Set the count of columns displayed in a row": "Anzahl der Spalten in einer Zeile festlegen",
+ "Column": "Spalte",
+ "Phone device": "Mobiltelefon",
+ "Tablet device": "Tablet",
+ "Desktop device": "Desktop",
+ "Large screen device": "Großer Bildschirm",
+ "Collapse": "Einklappen",
+ "Select data source": "Datenquelle auswählen",
+ "Calendar": "Kalender",
+ "Delete events": "Ereignisse löschen",
+ "This event": "Dieses Ereignis",
+ "This and following events": "Dieses und folgende Ereignisse",
+ "All events": "Alle Ereignisse",
+ "Delete this event?": "Dieses Ereignis löschen?",
+ "Delete Event": "Ereignis löschen",
+ "Kanban": "Kanban",
+ "Gantt": "Gantt",
+ "Create gantt block": "Gantt-Block erstellen",
+ "Progress field": "Fortschrittsfeld",
+ "Time scale": "Zeitskala",
+ "Hour": "Stunde",
+ "Quarter of day": "Viertel des Tages",
+ "Half of day": "Halber Tag",
+ "Year": "Jahr",
+ "QuarterYear": "Jahresquartal",
+ "Select grouping field": "Gruppierungsfeld auswählen",
+ "Media": "Medien",
+ "Markdown": "Markdown",
+ "Wysiwyg": "Wysiwyg",
+ "Chart blocks": "Diagrammblöcke",
+ "Column chart": "Säulendiagramm",
+ "Bar chart": "Balkendiagramm",
+ "Line chart": "Liniendiagramm",
+ "Pie chart": "Kreisdiagramm",
+ "Area chart": "Flächendiagramm",
+ "Other chart": "Anderes Diagramm",
+ "Other blocks": "Andere Blöcke",
+ "In configuration": "In Konfiguration",
+ "Chart title": "Diagrammtitel",
+ "Chart type": "Diagrammtyp",
+ "Chart config": "Diagrammkonfiguration",
+ "Templates": "Vorlagen",
+ "Select template": "Vorlage auswählen",
+ "Action logs": "Aktionslogs",
+ "Create template": "Vorlage erstellen",
+ "Edit markdown": "Markdown bearbeiten",
+ "Add block": "Block hinzufügen",
+ "Add new": "Neu hinzufügen",
+ "Add record": "Datensatz hinzufügen",
+ "Add child": "Kind hinzufügen",
+ "Collapse all": "Alle einklappen",
+ "Expand all": "Alle ausklappen",
+ "Expand/Collapse": "Erweitern/Einklappen",
+ "Default collapse": "Standardmäßig eingeklappt",
+ "Tree table": "Baumtabelle",
+ "Custom field display name": "Benutzerdefinierter Feldanzeigename",
+ "Display fields": "Anzeigefelder der Sammlung",
+ "Edit record": "Datensatz bearbeiten",
+ "Delete menu item": "Menüpunkt löschen",
+ "Add page": "Seite hinzufügen",
+ "Add group": "Gruppe hinzufügen",
+ "Add link": "Link hinzufügen",
+ "Insert above": "Oben einfügen",
+ "Insert below": "Unten einfügen",
+ "Save": "Speichern",
+ "Delete block": "Block löschen",
+ "Are you sure you want to delete it?": "Sind Sie sicher, dass Sie es löschen möchten?",
+ "This is a demo text, **supports Markdown syntax**.": "Dies ist ein Beispieltext, **unterstützt Markdown-Syntax**.",
+ "Filter": "Filter",
+ "Connect data blocks": "Datenblöcke verbinden",
+ "Action type": "Aktionstyp",
+ "Actions": "Aktionen",
+ "Insert": "Einfügen",
+ "Insert if not exists": "Einfügen, wenn nicht vorhanden",
+ "Insert if not exists, or update": "Einfügen, wenn nicht vorhanden, sonst aktualisieren",
+ "Determine whether a record exists by the following fields": "Bestimmen Sie, ob ein Datensatz anhand der folgenden Felder existiert",
+ "Update": "Aktualisieren",
+ "Update record": "Datensatz aktualisieren",
+ "View": "Ansicht",
+ "View record": "Datensatz ansehen",
+ "Refresh": "Aktualisieren",
+ "Data changes": "Datenänderungen",
+ "Field name": "Feldname",
+ "Before change": "Vor der Änderung",
+ "After change": "Nach der Änderung",
+ "Delete record": "Datensatz löschen",
+ "Delete collection": "Sammlung löschen",
+ "Create collection": "Sammlung erstellen",
+ "Collection display name": "Anzeigename der Sammlung",
+ "Collection name": "Sammlungsname",
+ "Inherits": "Erbt von",
+ "Primary key, unique identifier, self growth": "Primärschlüssel, eindeutiger Bezeichner, automatische Erhöhung",
+ "Store the creation user of each record": "Speichert den Erstellungsbenutzer jedes Datensatzes",
+ "Store the last update user of each record": "Speichert den letzten Aktualisierungsbenutzer jedes Datensatzes",
+ "Store the creation time of each record": "Speichert die Erstellungszeit jedes Datensatzes",
+ "Store the last update time of each record": "Speichert die letzte Aktualisierungszeit jedes Datensatzes",
+ "More options": "Weitere Optionen",
+ "Records can be sorted": "Datensätze können sortiert werden",
+ "Calendar collection": "Kalendersammlung",
+ "General collection": "Allgemeine Sammlung",
+ "Connect to database view": "Mit Datenbankansicht verbinden",
+ "Sync from database": "Von Datenbank synchronisieren",
+ "Source collections": "Quellsammlungen",
+ "Field source": "Feldquelle",
+ "Preview": "Vorschau",
+ "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Zufällig generiert und kann geändert werden. Unterstützt Buchstaben, Zahlen und Unterstriche, muss mit einem Buchstaben beginnen.",
+ "Edit": "Bearbeiten",
+ "Edit collection": "Sammlung bearbeiten",
+ "Configure fields": "Felder konfigurieren",
+ "Configure columns": "Spalten konfigurieren",
+ "Edit field": "Feld bearbeiten",
+ "Override": "Überschreiben",
+ "Override field": "Feld überschreiben",
+ "Configure fields of {{title}}": "Felder von {{title}} konfigurieren",
+ "Association fields filter": "Filter für Verknüpfungsfelder",
+ "PK & FK fields": "PK & FK Felder",
+ "Association fields": "Verknüpfungsfelder",
+ "Choices fields": "Auswahlfelder",
+ "System fields": "Systemfelder",
+ "General fields": "Allgemeine Felder",
+ "Inherited fields": "Geerbte Felder",
+ "Parent collection fields": "Felder der übergeordneten Sammlung",
+ "Basic": "Grundlegend",
+ "Single line text": "Einzeiliger Text",
+ "Long text": "Langer Text",
+ "Phone": "Telefon",
+ "Email": "E-Mail",
+ "Number": "Zahl",
+ "Integer": "Ganzzahl",
+ "Percent": "Prozent",
+ "Password": "Passwort",
+ "Advanced type": "Erweitert",
+ "Formula": "Formel",
+ "Formula description": "Berechnet einen Wert in jedem Datensatz basierend auf anderen Feldern im selben Datensatz.",
+ "Choices": "Auswahlmöglichkeiten",
+ "Checkbox": "Kontrollkästchen",
+ "Single select": "Einzelauswahl",
+ "Multiple select": "Mehrfachauswahl",
+ "Radio group": "Radiogruppe",
+ "Checkbox group": "Kontrollkästchengruppe",
+ "China region": "China-Region",
+ "Date & Time": "Datum & Zeit",
+ "Datetime": "Datum/Zeit",
+ "Relation": "Beziehung",
+ "Link to": "Verknüpfen mit",
+ "Link to description": "Wird verwendet, um Sammlungsbeziehungen schnell zu erstellen und ist mit den meisten gängigen Szenarien kompatibel. Geeignet für Nicht-Entwickler. Als Feld dargestellt, ist es eine Dropdown-Auswahl zur Auswahl von Datensätzen aus der Zielsammlung. Nach der Erstellung werden gleichzeitig die zugehörigen Felder der aktuellen Sammlung in der Zielsammlung generiert.",
+ "Sub-table": "Untertabelle",
+ "Sub-details": "Unterdetails",
+ "Sub-form(Popover)": "Unterformular (Popover)",
+ "System info": "Systeminformationen",
+ "Created at": "Erstellt am",
+ "Last updated at": "Zuletzt aktualisiert am",
+ "Created by": "Erstellt von",
+ "Last updated by": "Zuletzt aktualisiert von",
+ "Add field": "Feld hinzufügen",
+ "Field display name": "Feldanzeigename",
+ "Field type": "Feldtyp",
+ "Field interface": "Feldschnittstelle",
+ "Date format": "Datumsformat",
+ "Year/Month/Day": "Jahr/Monat/Tag",
+ "Year-Month-Day": "Jahr-Monat-Tag",
+ "Day/Month/Year": "Tag/Monat/Jahr",
+ "Show time": "Zeit anzeigen",
+ "Time format": "Zeitformat",
+ "12 hour": "12 Stunden",
+ "24 hour": "24 Stunden",
+ "Relationship type": "Beziehungstyp",
+ "Inverse relationship type": "Inverse Beziehungstyp",
+ "Source collection": "Quellsammlung",
+ "Source key": "Quellschlüssel",
+ "Target collection": "Zielsammlung",
+ "Through collection": "Zwischensammlung",
+ "Target key": "Zielschlüssel",
+ "Foreign key": "Fremdschlüssel",
+ "One to one": "Eins zu Eins",
+ "One to many": "Eins zu Viele",
+ "Many to one": "Viele zu Eins",
+ "Many to many": "Viele zu Viele",
+ "Foreign key 1": "Fremdschlüssel 1",
+ "Foreign key 2": "Fremdschlüssel 2",
+ "One to one description": "Wird verwendet, um eine Eins-zu-Eins-Beziehung zu erstellen. Zum Beispiel hat ein Benutzer ein Profil.",
+ "One to many description": "Wird verwendet, um eine Eins-zu-Viele-Beziehung zu erstellen. Zum Beispiel hat ein Land viele Städte und eine Stadt kann nur in einem Land sein. Als Feld dargestellt, ist es eine Untertabelle, die die Datensätze der zugehörigen Sammlung anzeigt. Bei der Erstellung wird automatisch ein Viele-zu-Eins-Feld in der zugehörigen Sammlung generiert.",
+ "Many to one description": "Wird verwendet, um Viele-zu-Eins-Beziehungen zu erstellen. Zum Beispiel kann eine Stadt nur zu einem Land gehören und ein Land kann viele Städte haben. Als Feld dargestellt, ist es eine Dropdown-Auswahl zur Auswahl eines Datensatzes aus der zugehörigen Sammlung. Nach der Erstellung wird automatisch ein Eins-zu-Viele-Feld in der zugehörigen Sammlung generiert.",
+ "Many to many description": "Wird verwendet, um Viele-zu-Viele-Beziehungen zu erstellen. Zum Beispiel hat ein Schüler viele Lehrer und ein Lehrer hat viele Schüler. Als Feld dargestellt, ist es eine Dropdown-Auswahl zur Auswahl von Datensätzen aus der zugehörigen Sammlung.",
+ "Generated automatically if left blank": "Wird automatisch generiert, wenn leer gelassen",
+ "Display association fields": "Verknüpfungsfelder anzeigen",
+ "Display field title": "Feldtitel anzeigen",
+ "Field component": "Feldkomponente",
+ "Allow multiple": "Mehrere erlauben",
+ "Quick upload": "Schnelles Hochladen",
+ "Select file": "Datei auswählen",
+ "Subtable": "Untertabelle",
+ "Sub-form": "Unterformular",
+ "Field mode": "Feldmodus",
+ "Allow add new data": "Hinzufügen neuer Daten erlauben",
+ "Record picker": "Datensatzauswahl",
+ "Toggles the subfield mode": "Schaltet den Unterfeld-Modus um",
+ "Selector mode": "Auswahlmodus",
+ "Subtable mode": "Untertabellenmodus",
+ "Subform mode": "Unterformularmodus",
+ "Edit block title": "Blocktitel bearbeiten",
+ "Block title": "Blocktitel",
+ "Pattern": "Muster",
+ "Operator": "Operator",
+ "Editable": "Bearbeitbar",
+ "Readonly": "Schreibgeschützt",
+ "Easy-reading": "Leicht lesbar",
+ "Add filter": "Filter hinzufügen",
+ "Add filter group": "Filtergruppe hinzufügen",
+ "Comparision": "Vergleich",
+ "is": "ist",
+ "is not": "ist nicht",
+ "contains": "enthält",
+ "does not contain": "enthält nicht",
+ "starts with": "beginnt mit",
+ "not starts with": "beginnt nicht mit",
+ "ends with": "endet mit",
+ "not ends with": "endet nicht mit",
+ "is empty": "ist leer",
+ "is not empty": "ist nicht leer",
+ "Edit chart": "Diagramm bearbeiten",
+ "Add text": "Text hinzufügen",
+ "Filterable fields": "Filterbare Felder",
+ "Edit button": "Schaltfläche bearbeiten",
+ "Hide": "Ausblenden",
+ "Enable actions": "Aktionen aktivieren",
+ "Import": "Importieren",
+ "Export": "Exportieren",
+ "Customize": "Anpassen",
+ "Custom": "Benutzerdefiniert",
+ "Function": "Funktion",
+ "Popup form": "Popup-Formular",
+ "Flexible popup": "Flexibles Popup",
+ "Configure actions": "Aktionen konfigurieren",
+ "Display order number": "Bestellnummer anzeigen",
+ "Enable drag and drop sorting": "Drag & Drop-Sortierung aktivieren",
+ "Triggered when the row is clicked": "Wird ausgelöst, wenn auf die Zeile geklickt wird",
+ "Add tab": "Tab hinzufügen",
+ "Disable tabs": "Tabs deaktivieren",
+ "Details": "Details",
+ "Edit form": "Formular bearbeiten",
+ "Create form": "Formular erstellen",
+ "Form (Edit)": "Formular (Bearbeiten)",
+ "Form (Add new)": "Formular (Neu hinzufügen)",
+ "Edit tab": "Tab bearbeiten",
+ "Relationship blocks": "Beziehungsblöcke",
+ "Select record": "Datensatz auswählen",
+ "Display name": "Anzeigename",
+ "Select icon": "Symbol auswählen",
+ "Custom column name": "Benutzerdefinierter Spaltenname",
+ "Edit description": "Beschreibung bearbeiten",
+ "Required": "Erforderlich",
+ "Unique": "Eindeutig",
+ "Primary": "Primär",
+ "Auto increment": "Automatische Erhöhung",
+ "Label field": "Beschriftungsfeld",
+ "Default is the ID field": "Standard ist das ID-Feld",
+ "Set default sorting rules": "Standardsortierregeln festlegen",
+ "Set validation rules": "Validierungsregeln festlegen",
+ "Max length": "Maximale Länge",
+ "Min length": "Minimale Länge",
+ "Maximum": "Maximum",
+ "Minimum": "Minimum",
+ "Max length must greater than min length": "Maximale Länge muss größer als minimale Länge sein",
+ "Min length must less than max length": "Minimale Länge muss kleiner als maximale Länge sein",
+ "Maximum must greater than minimum": "Maximum muss größer als Minimum sein",
+ "Minimum must less than maximum": "Minimum muss kleiner als Maximum sein",
+ "Validation rule": "Validierungsregel",
+ "Add validation rule": "Validierungsregel hinzufügen",
+ "Format": "Format",
+ "Regular expression": "Regulärer Ausdruck",
+ "Error message": "Fehlermeldung",
+ "Length": "Länge",
+ "The field value cannot be greater than ": "Der Feldwert darf nicht größer sein als ",
+ "The field value cannot be less than ": "Der Feldwert darf nicht kleiner sein als ",
+ "The field value is not an integer number": "Der Feldwert ist keine ganze Zahl",
+ "Set default value": "Standardwert festlegen",
+ "Default value": "Standardwert",
+ "is before": "ist vor",
+ "is after": "ist nach",
+ "is on or after": "ist am oder nach",
+ "is on or before": "ist am oder vor",
+ "is between": "ist zwischen",
+ "Upload": "Hochladen",
+ "Select level": "Ebene auswählen",
+ "Province": "Provinz",
+ "City": "Stadt",
+ "Area": "Gebiet",
+ "Street": "Straße",
+ "Village": "Dorf",
+ "Must select to the last level": "Muss bis zur letzten Ebene ausgewählt werden",
+ "Move {{title}} to": "{{title}} verschieben nach",
+ "Target position": "Zielposition",
+ "After": "Nach",
+ "Before": "Vor",
+ "Add {{type}} before \"{{title}}\"": "{{type}} vor \"{{title}}\" hinzufügen",
+ "Add {{type}} after \"{{title}}\"": "{{type}} nach \"{{title}}\" hinzufügen",
+ "Add {{type}} in \"{{title}}\"": "{{type}} in \"{{title}}\" hinzufügen",
+ "Original name": "Ursprünglicher Name",
+ "Custom name": "Benutzerdefinierter Name",
+ "Custom Title": "Benutzerdefinierter Titel",
+ "Options": "Optionen",
+ "Option value": "Optionswert",
+ "Option label": "Optionsbezeichnung",
+ "Color": "Farbe",
+ "Background Color": "Hintergrundfarbe",
+ "Text Align": "Textausrichtung",
+ "Add option": "Option hinzufügen",
+ "Related collection": "Zugehörige Sammlung",
+ "Allow linking to multiple records": "Verknüpfung mit mehreren Datensätzen erlauben",
+ "Allow uploading multiple files": "Hochladen mehrerer Dateien erlauben",
+ "Configure calendar": "Kalender konfigurieren",
+ "Title field": "Titelfeld",
+ "Custom title": "Benutzerdefinierter Titel",
+ "Daily": "Täglich",
+ "Weekly": "Wöchentlich",
+ "Monthly": "Monatlich",
+ "Yearly": "Jährlich",
+ "Repeats": "Wiederholungen",
+ "Show lunar": "Mondkalender anzeigen",
+ "Start date field": "Startdatumsfeld",
+ "End date field": "Enddatumsfeld",
+ "Navigate": "Navigieren",
+ "Title": "Titel",
+ "Description": "Beschreibung",
+ "Select view": "Ansicht auswählen",
+ "Reset": "Zurücksetzen",
+ "Importable fields": "Importierbare Felder",
+ "Exportable fields": "Exportierbare Felder",
+ "Saved successfully": "Erfolgreich gespeichert",
+ "Nickname": "Spitzname",
+ "Sign in": "Anmelden",
+ "Sign in via account": "Über Konto anmelden",
+ "Sign in via phone": "Über Telefon anmelden",
+ "Create an account": "Konto erstellen",
+ "Sign up": "Registrieren",
+ "Confirm password": "Passwort bestätigen",
+ "Log in with an existing account": "Mit einem bestehenden Konto anmelden",
+ "Signed up successfully. It will jump to the login page.": "Registrierung erfolgreich. Sie werden zur Anmeldeseite weitergeleitet.",
+ "Password mismatch": "Passwörter stimmen nicht überein",
+ "Users": "Benutzer",
+ "Verification code": "Bestätigungscode",
+ "Send code": "Code senden",
+ "Retry after {{count}} seconds": "Wiederholen nach {{count}} Sekunden",
+ "Roles": "Rollen",
+ "Add role": "Rolle hinzufügen",
+ "Role name": "Rollenname",
+ "Configure": "Konfigurieren",
+ "Configure permissions": "Berechtigungen konfigurieren",
+ "Edit role": "Rolle bearbeiten",
+ "Action permissions": "Aktionsberechtigungen",
+ "Menu permissions": "Menüberechtigungen",
+ "Menu item name": "Menüpunktname",
+ "Allow access": "Zugriff erlauben",
+ "Action name": "Aktionsname",
+ "Allow action": "Aktion erlauben",
+ "Action scope": "Aktionsbereich",
+ "Operate on new data": "Mit neuen Daten arbeiten",
+ "Operate on existing data": "Mit vorhandenen Daten arbeiten",
+ "Yes": "Ja",
+ "No": "Nein",
+ "Red": "Rot",
+ "Magenta": "Magenta",
+ "Volcano": "Vulkan",
+ "Orange": "Orange",
+ "Gold": "Gold",
+ "Lime": "Limette",
+ "Green": "Grün",
+ "Cyan": "Cyan",
+ "Blue": "Blau",
+ "Geek blue": "Geek-Blau",
+ "Purple": "Lila",
+ "Default": "Standard",
+ "Add card": "Karte hinzufügen",
+ "edit title": "Titel bearbeiten",
+ "Turn pages": "Seiten umblättern",
+ "Others": "Andere",
+ "Other records": "Andere Datensätze",
+ "Save as reference template": "Als Referenzvorlage speichern",
+ "Save as inherited template": "Als vererbte Vorlage speichern",
+ "Save as block template": "Als Blockvorlage speichern",
+ "Block templates": "Blockvorlagen",
+ "Block template": "Blockvorlage",
+ "Convert reference to duplicate": "Referenz in Duplikat umwandeln",
+ "Template name": "Vorlagenname",
+ "Block type": "Blocktyp",
+ "No blocks to connect": "Keine Blöcke zum Verbinden",
+ "Action column": "Aktionsspalte",
+ "Records per page": "Datensätze pro Seite",
+ "(Fields only)": "(Nur Felder)",
+ "Button title": "Schaltflächentitel",
+ "Button icon": "Schaltflächensymbol",
+ "Submitted successfully": "Erfolgreich übermittelt",
+ "Operation succeeded": "Operation erfolgreich",
+ "Operation failed": "Operation fehlgeschlagen",
+ "Open mode": "Öffnungsmodus",
+ "Popup size": "Popup-Größe",
+ "Small": "Klein",
+ "Middle": "Mittel",
+ "Large": "Groß",
+ "Size": "Größe",
+ "Oversized": "Übergroß",
+ "Auto": "Automatisch",
+ "Object Fit": "Objektanpassung",
+ "Cover": "Abdecken",
+ "Fill": "Füllen",
+ "Contain": "Enthalten",
+ "Scale Down": "Verkleinern",
+ "Menu item title": "Menüpunkttitel",
+ "Menu item icon": "Menüpunktsymbol",
+ "Target": "Ziel",
+ "Position": "Position",
+ "Insert before": "Davor einfügen",
+ "Insert after": "Danach einfügen",
+ "UI Editor": "UI-Editor",
+ "ASC": "Aufsteigend",
+ "DESC": "Absteigend",
+ "Add sort field": "Sortierfeld hinzufügen",
+ "ID": "ID",
+ "Identifier for program usage. Support letters, numbers and underscores, must start with an letter.": "Bezeichner für Programmnutzung. Unterstützt Buchstaben, Zahlen und Unterstriche, muss mit einem Buchstaben beginnen.",
+ "Drawer": "Seitenleiste",
+ "Dialog": "Dialog",
+ "Delete action": "Aktion löschen",
+ "Custom column title": "Benutzerdefinierter Spaltentitel",
+ "Column title": "Spaltentitel",
+ "Original title: ": "Ursprünglicher Titel: ",
+ "Delete table column": "Tabellenspalte löschen",
+ "Skip required validation": "Erforderliche Validierung überspringen",
+ "Form values": "Formularwerte",
+ "Fields values": "Feldwerte",
+ "The field has been deleted": "Das Feld wurde gelöscht",
+ "When submitting the following fields, the saved values are": "Beim Absenden der folgenden Felder sind die gespeicherten Werte",
+ "After successful submission": "Nach erfolgreicher Übermittlung",
+ "Then": "Dann",
+ "Stay on current page": "Auf aktueller Seite bleiben",
+ "Redirect to": "Weiterleiten zu",
+ "Save action": "Aktion speichern",
+ "Exists": "Existiert",
+ "Add condition": "Bedingung hinzufügen",
+ "Add condition group": "Bedingungsgruppe hinzufügen",
+ "exists": "existiert",
+ "not exists": "existiert nicht",
+ "Style": "Stil",
+ "=": "=",
+ "≠": "≠",
+ ">": ">",
+ "≥": "≥",
+ "<": "<",
+ "≤": "≤",
+ "Role UID": "Rollen-UID",
+ "Precision": "Genauigkeit",
+ "Formula mode": "Formelmodus",
+ "Expression": "Ausdruck",
+ "Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "Geben Sie +, -, *, /, ( ) zum Berechnen ein, geben Sie @ ein, um Feldvariablen zu öffnen.",
+ "Formula error.": "Formelfehler.",
+ "Rich Text": "Rich Text",
+ "Junction collection": "Verbindungssammlung",
+ "Leave it blank, unless you need a custom intermediate table": "Lassen Sie es leer, es sei denn, Sie benötigen eine benutzerdefinierte Zwischentabelle",
+ "Fields": "Felder",
+ "Edit field title": "Feldtitel bearbeiten",
+ "Field title": "Feldtitel",
+ "Original field title: ": "Ursprünglicher Feldtitel: ",
+ "Edit tooltip": "Tooltip bearbeiten",
+ "Delete field": "Feld löschen",
+ "Select collection": "Sammlung auswählen",
+ "Blank block": "Leerer Block",
+ "Duplicate template": "Vorlage duplizieren",
+ "Reference template": "Referenzvorlage",
+ "Inherited template": "Vererbte Vorlage",
+ "Create calendar block": "Kalenderblock erstellen",
+ "Create kanban block": "Kanban-Block erstellen",
+ "Grouping field": "Gruppierungsfeld",
+ "Single select and radio fields can be used as the grouping field": "Einzelauswahl- und Radiofelder können als Gruppierungsfeld verwendet werden",
+ "Tab name": "Tab-Name",
+ "Current record blocks": "Blöcke des aktuellen Datensatzes",
+ "Popup message": "Popup-Nachricht",
+ "Delete role": "Rolle löschen",
+ "Role display name": "Rollenanzeigename",
+ "Default role": "Standardrolle",
+ "All collections use general action permissions by default; permission configured individually will override the default one.": "Alle Sammlungen verwenden standardmäßig allgemeine Aktionsberechtigungen; individuell konfigurierte Berechtigungen überschreiben die Standardeinstellung.",
+ "Allows configuration of the whole system, including UI, collections, permissions, etc.": "Ermöglicht die Konfiguration des gesamten Systems, einschließlich UI, Sammlungen, Berechtigungen usw.",
+ "New menu items are allowed to be accessed by default.": "Neue Menüpunkte dürfen standardmäßig zugegriffen werden.",
+ "Global permissions": "Globale Berechtigungen",
+ "General permissions": "Allgemeine Berechtigungen",
+ "Global action permissions": "Globale Aktionsberechtigungen",
+ "General action permissions": "Allgemeine Aktionsberechtigungen",
+ "Plugin settings permissions": "Plugin-Einstellungsberechtigungen",
+ "Allow to desgin pages": "Erlauben, Seiten zu gestalten",
+ "Allow to manage plugins": "Erlauben, Plugins zu verwalten",
+ "Allow to configure plugins": "Erlauben, Plugins zu konfigurieren",
+ "Allows to configure interface": "Erlaubt die Konfiguration der Schnittstelle",
+ "Allows to install, activate, disable plugins": "Erlaubt das Installieren, Aktivieren und Deaktivieren von Plugins",
+ "Allows to configure plugins": "Erlaubt die Konfiguration von Plugins",
+ "Action display name": "Anzeigeame der Aktion",
+ "Allow": "Erlauben",
+ "Data scope": "Datenbereich",
+ "Action on new records": "Aktion für neue Datensätze",
+ "Action on existing records": "Aktion für bestehende Datensätze",
+ "All records": "Alle Datensätze",
+ "Own records": "Eigene Datensätze",
+ "Permission policy": "Berechtigungsrichtlinie",
+ "Individual": "Individuell",
+ "General": "Allgemein",
+ "Accessible": "Zugänglich",
+ "Configure permission": "Berechtigung konfigurieren",
+ "Action permission": "Aktionsberechtigung",
+ "Field permission": "Feldberechtigung",
+ "Scope name": "Bereichsname",
+ "Unsaved changes": "Ungespeicherte Änderungen",
+ "Are you sure you don't want to save?": "Sind Sie sicher, dass Sie nicht speichern möchten?",
+ "Dragging": "Ziehen",
+ "Popup": "Popup",
+ "Trigger workflow": "Workflow auslösen",
+ "Request API": "API anfragen",
+ "Assign field values": "Feldwerte zuweisen",
+ "Constant value": "Konstanter Wert",
+ "Dynamic value": "Dynamischer Wert",
+ "Current user": "Aktueller Benutzer",
+ "Current role": "Aktuelle Rolle",
+ "Current record": "Aktueller Datensatz",
+ "Current collection": "Aktuelle Sammlung",
+ "Other collections": "Andere Sammlungen",
+ "Current popup record": "Aktueller Popup-Datensatz",
+ "Parent popup record": "Übergeordneter Popup-Datensatz",
+ "Associated records": "Zugehörige Datensätze",
+ "Parent record": "Übergeordneter Datensatz",
+ "Current time": "Aktuelle Zeit",
+ "System variables": "Systemvariablen",
+ "Date variables": "Datumsvariablen",
+ "Message popup close method": "Schließmethode für Popup-Nachrichten",
+ "Automatic close": "Automatisch schließen",
+ "Manually close": "Manuell schließen",
+ "After successful update": "Nach erfolgreicher Aktualisierung",
+ "Save record": "Datensatz speichern",
+ "Updated successfully": "Erfolgreich aktualisiert",
+ "After successful save": "Nach erfolgreichem Speichern",
+ "After clicking the custom button, the following field values will be assigned according to the following form.": "Nach dem Klicken auf die benutzerdefinierte Schaltfläche werden die folgenden Feldwerte gemäß dem folgenden Formular zugewiesen.",
+ "After clicking the custom button, the following fields of the current record will be saved according to the following form.": "Nach dem Klicken auf die benutzerdefinierte Schaltfläche werden die folgenden Felder des aktuellen Datensatzes gemäß dem folgenden Formular gespeichert.",
+ "Button background color": "Schaltflächen-Hintergrundfarbe",
+ "Highlight": "Hervorheben",
+ "Danger red": "Gefahr-Rot",
+ "Custom request": "Benutzerdefinierte Anfrage",
+ "Request settings": "Anfrageeinstellungen",
+ "Request URL": "Anfrage-URL",
+ "Request method": "Anfragemethode",
+ "Request query parameters": "Anfrageparameter",
+ "Request headers": "Anfrageheader",
+ "Request body": "Anfragekörper",
+ "Request success": "Anfrage erfolgreich",
+ "Invalid JSON format": "Ungültiges JSON-Format",
+ "After successful request": "Nach erfolgreicher Anfrage",
+ "Add exportable field": "Exportierbares Feld hinzufügen",
+ "Audit logs": "Prüfprotokolle",
+ "Record ID": "Datensatz-ID",
+ "User": "Benutzer",
+ "Field": "Feld",
+ "Select": "Auswählen",
+ "Select field": "Feld auswählen",
+ "Field value changes": "Feldwertänderungen",
+ "One to one (has one)": "Eins zu Eins (hat ein)",
+ "One to one (belongs to)": "Eins zu Eins (gehört zu)",
+ "Use the same time zone (GMT) for all users": "Verwenden Sie die gleiche Zeitzone (GMT) für alle Benutzer",
+ "Province/city/area name": "Provinz/Stadt/Gebietsname",
+ "Enabled languages": "Aktivierte Sprachen",
+ "View all plugins": "Alle Plugins anzeigen",
+ "Print": "Drucken",
+ "Done": "Fertig",
+ "Sign up successfully, and automatically jump to the sign in page": "Registrierung erfolgreich, Sie werden automatisch zur Anmeldeseite weitergeleitet",
+ "File manager": "Dateimanager",
+ "ACL": "ACL",
+ "Collection manager": "Sammlungsmanager",
+ "Plugin manager": "Plugin-Manager",
+ "Local": "Lokal",
+ "Built-in": "Eingebaut",
+ "Marketplace": "Marktplatz",
+ "Add plugin": "Plugin hinzufügen",
+ "Plugin source": "Plugin-Quelle",
+ "Upgrade": "Aktualisieren",
+ "Plugin dependencies check failed": "Überprüfung der Plugin-Abhängigkeiten fehlgeschlagen",
+ "More details": "Weitere Details",
+ "Upload new version": "Neue Version hochladen",
+ "Version": "Version",
+ "Npm package": "NPM-Paket",
+ "Npm package name": "NPM-Paketname",
+ "Upload plugin": "Plugin hochladen",
+ "Official plugin": "Offizielles Plugin",
+ "Add type": "Typ hinzufügen",
+ "Changelog": "Änderungsprotokoll",
+ "Dependencies check": "Abhängigkeitsprüfung",
+ "Update plugin": "Plugin aktualisieren",
+ "Installing": "Installiere",
+ "The deletion was successful.": "Das Löschen war erfolgreich.",
+ "Plugin Zip File": "Plugin-ZIP-Datei",
+ "Compressed file url": "URL der komprimierten Datei",
+ "Last updated": "Zuletzt aktualisiert",
+ "PackageName": "Paketname",
+ "DisplayName": "Anzeigename",
+ "Readme": "Readme",
+ "Dependencies compatibility check": "Kompatibilitätsprüfung der Abhängigkeiten",
+ "Plugin dependencies check failed, you should change the dependent version to meet the version requirements.": "Die Überprüfung der Plugin-Abhängigkeiten ist fehlgeschlagen. Sie sollten die abhängige Version ändern, um die Versionsanforderungen zu erfüllen.",
+ "Version range": "Versionsbereich",
+ "Plugin's version": "Plugin-Version",
+ "Result": "Ergebnis",
+ "No CHANGELOG.md file": "Keine CHANGELOG.md-Datei",
+ "No README.md file": "Keine README.md-Datei",
+ "Homepage": "Startseite",
+ "Drag and drop the file here or click to upload, file size should not exceed 30M": "Ziehen Sie die Datei hierher oder klicken Sie zum Hochladen, die Dateigröße sollte 30M nicht überschreiten",
+ "Dependencies check failed, can't enable.": "Abhängigkeitsprüfung fehlgeschlagen, kann nicht aktiviert werden.",
+ "Plugin starting...": "Plugin wird gestartet...",
+ "Plugin stopping...": "Plugin wird gestoppt...",
+ "Are you sure to delete this plugin?": "Sind Sie sicher, dass Sie dieses Plugin löschen möchten?",
+ "Are you sure to disable this plugin?": "Sind Sie sicher, dass Sie dieses Plugin deaktivieren möchten?",
+ "re-download file": "Datei erneut herunterladen",
+ "Not enabled": "Nicht aktiviert",
+ "Search plugin": "Plugin suchen",
+ "Author": "Autor",
+ "Plugin loading failed. Please check the server logs.": "Plugin-Ladung fehlgeschlagen. Bitte überprüfen Sie die Serverprotokolle.",
+ "Coming soon...": "Demnächst verfügbar...",
+ "All plugin settings": "Alle Plugin-Einstellungen",
+ "Bookmark": "Lesezeichen",
+ "Manage all settings": "Alle Einstellungen verwalten",
+ "Create inverse field in the target collection": "Inverses Feld in der Zielsammlung erstellen",
+ "Inverse field name": "Name des inversen Feldes",
+ "Inverse field display name": "Anzeigename des inversen Feldes",
+ "Bulk update": "Massenaktualisierung",
+ "After successful bulk update": "Nach erfolgreicher Massenaktualisierung",
+ "Bulk edit": "Massenbearbeitung",
+ "Data will be updated": "Daten werden aktualisiert",
+ "Selected": "Ausgewählt",
+ "All": "Alle",
+ "Update selected data?": "Ausgewählte Daten aktualisieren?",
+ "Update all data?": "Alle Daten aktualisieren?",
+ "Remains the same": "Bleibt gleich",
+ "Changed to": "Geändert zu",
+ "Clear": "Löschen",
+ "Add attach": "Anhang hinzufügen",
+ "Please select the records to be updated": "Bitte wählen Sie die zu aktualisierenden Datensätze aus",
+ "Selector": "Selektor",
+ "Inner": "Innen",
+ "Search and select collection": "Sammlung suchen und auswählen",
+ "Please fill in the iframe URL": "Bitte geben Sie die iframe-URL ein",
+ "Fix block": "Block fixieren",
+ "Plugin name": "Plugin-Name",
+ "Plugin tab name": "Plugin-Tab-Name",
+ "AutoGenId": "Automatisch generiertes ID-Feld",
+ "CreatedBy": "Erstellt von",
+ "UpdatedBy": "Aktualisiert von",
+ "CreatedAt": "Erstellt am",
+ "UpdatedAt": "Aktualisiert am",
+ "Column width": "Spaltenbreite",
+ "Sortable": "Sortierbar",
+ "Enable link": "Link aktivieren",
+ "This is likely a NocoBase internals bug. Please open an issue at <1>here1>": "Dies ist wahrscheinlich ein interner Fehler von NocoBase. Bitte öffnen Sie ein Problem <1>hier1>",
+ "Render Failed": "Rendering fehlgeschlagen",
+ "App error": "App-Fehler",
+ "Feedback": "Feedback",
+ "Try again": "Erneut versuchen",
+ "Download logs": "Protokolle herunterladen",
+ "Data template": "Datenvorlage",
+ "Duplicate": "Duplizieren",
+ "Duplicating": "Dupliziere",
+ "Duplicate mode": "Duplikationsmodus",
+ "Quick duplicate": "Schnelles Duplizieren",
+ "Duplicate and continue": "Duplizieren und fortfahren",
+ "Please configure the duplicate fields": "Bitte konfigurieren Sie die Duplikatfelder",
+ "Add": "Hinzufügen",
+ "Add new mode": "Neuer Hinzufügungsmodus",
+ "Quick add": "Schnell hinzufügen",
+ "Modal add": "Modal hinzufügen",
+ "Save mode": "Speichermodus",
+ "First or create": "Zuerst oder erstellen",
+ "Update or create": "Aktualisieren oder erstellen",
+ "Find by the following fields": "Nach den folgenden Feldern suchen",
+ "Create": "Erstellen",
+ "Current form": "Aktuelles Formular",
+ "Current object": "Aktuelles Objekt",
+ "Linkage with form fields": "Verknüpfung mit Formularfeldern",
+ "Allow add new, update and delete actions": "Hinzufügen, Aktualisieren und Löschen erlauben",
+ "Date display format": "Datumsanzeigeformat",
+ "Assign data scope for the template": "Datenbereich für die Vorlage zuweisen",
+ "Table selected records": "Ausgewählte Tabellendatensätze",
+ "Tag": "Tag",
+ "Tag color field": "Tag-Farbfeld",
+ "Sync successfully": "Synchronisierung erfolgreich",
+ "Sync from form fields": "Von Formularfeldern synchronisieren",
+ "Select all": "Alle auswählen",
+ "Restart": "Neustart",
+ "Restart application": "Anwendung neu starten",
+ "Cascade Select": "Kaskadierte Auswahl",
+ "Execute": "Ausführen",
+ "Please use a valid SELECT or WITH AS statement": "Bitte verwenden Sie eine gültige SELECT- oder WITH AS-Anweisung",
+ "Please confirm the SQL statement first": "Bitte bestätigen Sie zuerst die SQL-Anweisung",
+ "Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects": "Automatisches Löschen von Objekten, die von der Sammlung abhängen (wie Ansichten), und wiederum aller Objekte, die von diesen Objekten abhängen",
+ "Sign in with another account": "Mit einem anderen Konto anmelden",
+ "Return to the main application": "Zurück zur Hauptanwendung",
+ "Permission deined": "Berechtigung verweigert",
+ "loading": "Lädt",
+ "name is required": "Name ist erforderlich",
+ "data source": "Datenquelle",
+ "Data source": "Datenquelle",
+ "DataSource": "Datenquelle",
+ "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "Der {{type}} \"{{name}}\" wurde möglicherweise gelöscht. Bitte entfernen Sie diesen {{blockType}}.",
+ "Preset fields": "Voreingestellte Felder",
+ "Home page": "Startseite",
+ "Handbook": "Handbuch",
+ "License": "Lizenz",
+ "Generic properties": "Allgemeine Eigenschaften",
+ "Specific properties": "Spezifische Eigenschaften",
+ "Used for drag and drop sorting scenarios, supporting grouping sorting": "Wird für Drag & Drop-Sortierungsszenarien verwendet und unterstützt Gruppensortierung",
+ "Grouped sorting": "Gruppierte Sortierung",
+ "When a field is selected for grouping, it will be grouped first before sorting.": "Wenn ein Feld für die Gruppierung ausgewählt wird, wird es zuerst gruppiert, bevor es sortiert wird.",
+ "Departments": "Abteilungen",
+ "Main department": "Hauptabteilung",
+ "Department name": "Abteilungsname",
+ "Superior department": "Übergeordnete Abteilung",
+ "Owners": "Eigentümer",
+ "Plugin settings": "Plugin-Einstellungen",
+ "Menu": "Menü",
+ "Drag and drop sorting field": "Feld für Drag & Drop-Sortierung",
+ "This variable has been deprecated and can be replaced with \"Current form\"": "Diese Variable ist veraltet und kann durch \"Aktuelles Formular\" ersetzt werden",
+ "The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "Der Wert dieser Variable wird aus der Abfragezeichenfolge der Seiten-URL abgeleitet. Diese Variable kann nur normal verwendet werden, wenn die Seite eine Abfragezeichenfolge hat.",
+ "URL search params": "URL-Suchparameter",
+ "Expand All": "Alle erweitern",
+ "Search": "Suchen",
+ "Clear default value": "Standardwert löschen",
+ "Open in new window": "In neuem Fenster öffnen",
+ "Sorry, the page you visited does not exist.": "Entschuldigung, die von Ihnen besuchte Seite existiert nicht.",
+ "is none of": "ist keines von",
+ "is any of": "ist eines von",
+ "Plugin dependency version mismatch": "Versionsinkompatibilität der Plugin-Abhängigkeit",
+ "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?": "Die aktuelle Abhängigkeitsversion des Plugins stimmt nicht mit der Version der Anwendung überein und funktioniert möglicherweise nicht ordnungsgemäß. Sind Sie sicher, dass Sie das Plugin weiterhin aktivieren möchten?",
+ "Allow multiple selection": "Mehrfachauswahl erlauben",
+ "Parent object": "Übergeordnetes Objekt",
+ "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": "Überspringt das Abrufen der Gesamtanzahl der Tabellendatensätze während der Paginierung, um das Laden zu beschleunigen. Es wird empfohlen, diese Option für Datentabellen mit einer großen Datenmenge zu aktivieren",
+ "Enable secondary confirmation": "Sekundäre Bestätigung aktivieren",
+ "Notification": "Benachrichtigung",
+ "Ellipsis overflow content": "Auslassungszeichen für Überlaufinhalt",
+ "Hide column": "Spalte ausblenden",
+ "In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "Im Konfigurationsmodus wird die gesamte Spalte transparent. Im Nicht-Konfigurationsmodus wird die gesamte Spalte ausgeblendet. Auch wenn die gesamte Spalte ausgeblendet ist, werden ihre konfigurierten Standardwerte und andere Einstellungen weiterhin wirksam.",
+ "Unauthenticated. Please sign in to continue.": "Nicht authentifiziert. Bitte melden Sie sich an, um fortzufahren.",
+ "User not found. Please sign in again to continue.": "Benutzer nicht gefunden. Bitte melden Sie sich erneut an, um fortzufahren.",
+ "Your session has expired. Please sign in again.": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
+ "User password changed, please signin again.": "Benutzerpasswort geändert, bitte melden Sie sich erneut an.",
+ "Desktop routes": "Desktop-Routen",
+ "Route permissions": "Routenberechtigungen",
+ "New routes are allowed to be accessed by default": "Neue Routen dürfen standardmäßig zugegriffen werden",
+ "Route name": "Routenname",
+ "Mobile routes": "Mobile Routen",
+ "Show in menu": "Im Menü anzeigen",
+ "Hide in menu": "Im Menü ausblenden",
+ "Path": "Pfad",
+ "Type": "Typ",
+ "Access": "Zugriff",
+ "Routes": "Routen",
+ "Add child route": "Unterroute hinzufügen",
+ "Delete routes": "Routen löschen",
+ "Delete route": "Route löschen",
+ "Are you sure you want to hide these routes in menu?": "Sind Sie sicher, dass Sie diese Routen im Menü ausblenden möchten?",
+ "Are you sure you want to show these routes in menu?": "Sind Sie sicher, dass Sie diese Routen im Menü anzeigen möchten?",
+ "Are you sure you want to hide this menu?": "Sind Sie sicher, dass Sie dieses Menü ausblenden möchten?",
+ "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Nach dem Ausblenden wird dieses Menü nicht mehr in der Menüleiste angezeigt. Um es wieder anzuzeigen, müssen Sie zur Routenverwaltungsseite gehen, um es zu konfigurieren.",
+ "If selected, the page will display Tab pages.": "Wenn ausgewählt, zeigt die Seite Tab-Seiten an.",
+ "If selected, the route will be displayed in the menu.": "Wenn ausgewählt, wird die Route im Menü angezeigt.",
+ "Are you sure you want to hide this tab?": "Sind Sie sicher, dass Sie diesen Tab ausblenden möchten?",
+ "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Nach dem Ausblenden wird dieser Tab nicht mehr in der Tableiste angezeigt. Um ihn wieder anzuzeigen, müssen Sie zur Routenverwaltungsseite gehen, um ihn einzustellen.",
+ "No pages yet, please configure first": "Noch keine Seiten, bitte zuerst konfigurieren",
+ "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Klicken Sie auf das \"UI-Editor\"-Symbol in der oberen rechten Ecke, um den UI-Editor-Modus zu betreten",
+ "Refresh data blocks": "Aktualisieren Sie die Datenblöcke",
+ "Select data blocks to refresh": "Wählen Sie die Datenblöcke aus, die aktualisiert werden sollen.",
+ "After successful submission, the selected data blocks will be automatically refreshed.": "Nach erfolgreicher Übermittlung werden die ausgewählten Datenblöcke automatisch aktualisiert."
+}
diff --git a/packages/core/client/src/locale/en-US.json b/packages/core/client/src/locale/en-US.json
index f80a244aa7..dc001c1ca9 100644
--- a/packages/core/client/src/locale/en-US.json
+++ b/packages/core/client/src/locale/en-US.json
@@ -164,6 +164,7 @@
"Chart type": "Chart type",
"Chart config": "Chart config",
"Templates": "Templates",
+ "Template": "Template",
"Select template": "Select template",
"Action logs": "Action logs",
"Create template": "Create template",
@@ -504,7 +505,7 @@
"Save as block template": "Save as block template",
"Block templates": "Block templates",
"Block template": "Block template",
- "Convert reference to duplicate": "Convert reference to duplicate",
+ "Convert template to duplicate": "Convert template to duplicate",
"Template name": "Template name",
"Block type": "Block type",
"No blocks to connect": "No blocks to connect",
@@ -589,6 +590,7 @@
"Blank block": "Blank block",
"Duplicate template": "Duplicate template",
"Reference template": "Reference template",
+ "Inherited template": "Inherited template",
"Create calendar block": "Create calendar block",
"Create kanban block": "Create kanban block",
"Grouping field": "Grouping field",
@@ -884,5 +886,13 @@
"If selected, the page will display Tab pages.": "If selected, the page will display Tab pages.",
"If selected, the route will be displayed in the menu.": "If selected, the route will be displayed in the menu.",
"Are you sure you want to hide this tab?": "Are you sure you want to hide this tab?",
- "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it."
+ "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.",
+ "No pages yet, please configure first": "No pages yet, please configure first",
+ "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode",
+ "Deprecated": "Deprecated",
+ "Full permissions": "Full permissions",
+ "Refresh data blocks": "Refresh data blocks",
+ "Select data blocks to refresh": "Select data blocks to refresh",
+ "After successful submission, the selected data blocks will be automatically refreshed.": "After successful submission, the selected data blocks will be automatically refreshed."
+
}
diff --git a/packages/core/client/src/locale/es-ES.json b/packages/core/client/src/locale/es-ES.json
index 3a3a45d646..69765e3b84 100644
--- a/packages/core/client/src/locale/es-ES.json
+++ b/packages/core/client/src/locale/es-ES.json
@@ -148,6 +148,7 @@
"Chart type": "Tipo del gráfico",
"Chart config": "Configuración del gráfico",
"Templates": "Plantillas",
+ "Template": "Plantilla",
"Select template": "Seleccione plantilla",
"Action logs": "Acción logs",
"Create template": "Crear plantilla",
@@ -469,7 +470,8 @@
"Turn pages": "Pasar páginas",
"Others": "Otros",
"Other records": "Otros registros",
- "Save as template": "Guardar como plantilla",
+ "Save as reference template": "Guardar como plantilla de referencia",
+ "Save as inherited template": "Guardar como plantilla heredada",
"Save as block template": "Guardar como plantilla de bloque",
"Block templates": "Bloquear plantillas",
"Block template": "Plantilla de bloque",
@@ -559,6 +561,7 @@
"Blank block": "Bloque en blanco",
"Duplicate template": "Duplicar plantilla",
"Reference template": "Plantilla de referencia",
+ "Inherited template": "Plantilla heredada",
"Create calendar block": "Crear bloque de calendario",
"Create kanban block": "Crear bloque kanban",
"Grouping field": "Campo de agrupación",
@@ -801,5 +804,12 @@
"If selected, the page will display Tab pages.": "Si se selecciona, la página mostrará páginas de pestañas.",
"If selected, the route will be displayed in the menu.": "Si se selecciona, la ruta se mostrará en el menú.",
"Are you sure you want to hide this tab?": "¿Estás seguro de que quieres ocultar esta pestaña?",
- "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Después de ocultar, esta pestaña ya no aparecerá en la barra de pestañas. Para mostrarla de nuevo, deberás ir a la página de gestión de rutas para configurarla."
+ "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Después de ocultar, esta pestaña ya no aparecerá en la barra de pestañas. Para mostrarla de nuevo, deberás ir a la página de gestión de rutas para configurarla.",
+ "No pages yet, please configure first": "Aún no hay páginas, por favor configura primero",
+ "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Haga clic en el icono \"Editor de UI\" en la esquina superior derecha para entrar en el modo de Editor de UI.",
+ "Deprecated": "Obsoleto",
+ "Full permissions": "Todos los derechos",
+ "Refresh data blocks": "Actualizar bloques de datos",
+ "Select data blocks to refresh": "Actualizar bloques de datos",
+ "After successful submission, the selected data blocks will be automatically refreshed.": "Después de enviar correctamente, los bloques de datos seleccionados se actualizarán automáticamente."
}
diff --git a/packages/core/client/src/locale/fr-FR.json b/packages/core/client/src/locale/fr-FR.json
index c5d0ee72d0..a0fa53f2fa 100644
--- a/packages/core/client/src/locale/fr-FR.json
+++ b/packages/core/client/src/locale/fr-FR.json
@@ -159,6 +159,7 @@
"Chart type": "Type de graphique",
"Chart config": "Configuration du graphique",
"Templates": "Modèles",
+ "Template": "Modèle",
"Select template": "Sélectionner un modèle",
"Action logs": "Logs d'action",
"Create template": "Créer un modèle",
@@ -484,7 +485,8 @@
"Turn pages": "Tourner les pages",
"Others": "Autres",
"Other records": "Autres enregistrements",
- "Save as template": "Enregistrer en tant que modèle",
+ "Save as reference template": "Enregistrer en tant que modèle de référence",
+ "Save as inherited template": "Enregistrer en tant que modèle hérité",
"Save as block template": "Enregistrer en tant que modèle de bloc",
"Block templates": "Modèles de bloc",
"Block template": "Modèle de bloc",
@@ -572,6 +574,7 @@
"Blank block": "Bloc vierge",
"Duplicate template": "Dupliquer le modèle",
"Reference template": "Référencer le modèle",
+ "Inherited template": "Modèle hérité",
"Create calendar block": "Créer un bloc de calendrier",
"Create kanban block": "Créer un bloc kanban",
"Grouping field": "Champ de regroupement",
@@ -821,5 +824,12 @@
"If selected, the page will display Tab pages.": "Si sélectionné, la page affichera des onglets.",
"If selected, the route will be displayed in the menu.": "Si sélectionné, la route sera affichée dans le menu.",
"Are you sure you want to hide this tab?": "Êtes-vous sûr de vouloir masquer cet onglet ?",
- "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Après avoir masqué, cette tab ne sera plus affichée dans la barre de tab. Pour la montrer à nouveau, vous devez vous rendre sur la page de gestion des routes pour la configurer."
+ "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Après avoir masqué, cette tab ne sera plus affichée dans la barre de tab. Pour la montrer à nouveau, vous devez vous rendre sur la page de gestion des routes pour la configurer.",
+ "No pages yet, please configure first": "Pas encore de pages, veuillez configurer d'abord",
+ "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur",
+ "Deprecated": "Déprécié",
+ "Full permissions": "Tous les droits",
+ "Refresh data blocks": "Actualiser les blocs de données",
+ "Select data blocks to refresh": "Actualiser les blocs de données",
+ "After successful submission, the selected data blocks will be automatically refreshed.": "Après une soumission réussie, les blocs de données sélectionnés seront automatiquement actualisés."
}
diff --git a/packages/core/client/src/locale/it-IT.json b/packages/core/client/src/locale/it-IT.json
index 5963f2120f..a8a75e7028 100644
--- a/packages/core/client/src/locale/it-IT.json
+++ b/packages/core/client/src/locale/it-IT.json
@@ -1,861 +1,1091 @@
-{
- "Display <1><0>100><1>201><2>502><3>1003>1> items per page": "Visualizza <1><0>100><1>201><2>502><3>1003>1> articoli per pagina",
- "Meet <1><0>All0><1>Any1>1> conditions in the group": "Soddisfa<1><0>Tutte0><1>Qualsiasi1>1>condizioni nel gruppo",
- "Open in<1><0>Modal0><1>Drawer1><2>Window2>1>": "Apri in<1><0>Modale0><1>Cassetto1><2>Finestra2>1>",
- "{{count}} filter items": "{{Count}} filtri elementi",
- "{{count}} more items": "{{Count}} altri elementi",
- "Total {{count}} items": "{{count}} elementi totali",
- "Today": "Oggi",
- "Yesterday": "Ieri",
- "Tomorrow": "Domani",
- "Month": "Mese",
- "Week": "Settimana",
- "This week": "Questa settimana",
- "This month": "Questo mese",
- "This year": "Quest'anno",
- "Next year": "Anno prossimo",
- "Last week": "Settimana scorsa",
- "Next week": "Prossima settimana",
- "Last month": "Mese scorso",
- "Next month": "Mese prossimo",
- "Last quarter": "Ultimo trimestre",
- "This quarter": "Questo trimestre",
- "Next quarter": "Prossimo trimestre",
- "Last year": "Anno scorso",
- "Last 7 days": "Ultimi 7 giorni",
- "Last 30 days": "Ultimi 30 giorni",
- "Last 90 days": "Ultimi 90 giorni",
- "Next 7 days": "Prossimi 7 giorni",
- "Next 30 days": "Prossimi 30 giorni",
- "Next 90 days": "Prossimi 90 giorni",
- "Work week": "Settimana lavorativa",
- "Day": "Giorno",
- "Agenda": "Agenda",
- "Date": "Data",
- "Time": "Tempo",
- "Event": "Evento",
- "None": "Nessuno",
- "Unconnected": "Non collegato",
- "System settings": "Impostazioni di sistema",
- "System title": "Titolo del sistema",
- "Settings": "Impostazioni",
- "Logo": "Logo",
- "Add menu item": "Aggiungi voce di menu",
- "Page": "Pagina",
- "Name": "Nome",
- "Icon": "Icona",
- "Group": "Gruppo",
- "Link": "Collegamento",
- "Save conditions": "Salva condizioni",
- "Edit menu item": "Modifica voce di menu",
- "Move to": "Passa a",
- "Insert left": "Inserisci a sinistra",
- "Insert right": "Inserire a destra",
- "Insert inner": "Inserire dentro",
- "Delete": "Eliminare",
- "Disassociate": "Dissociare",
- "Disassociate record": "Dissociare il record",
- "Are you sure you want to disassociate it?": "Sei sicuro di voler dissociare?",
- "UI editor": "Editor UI",
- "Collection": "Raccolta",
- "Collection selector": "Selettore di raccolta",
- "Providing certain collections as options for users, typically used in polymorphic or inheritance scenarios": "Fornire alcune raccolte come opzioni per gli utenti, in genere utilizzati negli scenari polimorfici o ereditari",
- "Collections & Fields": "Raccolte e campi",
- "All collections": "Tutte le raccolte",
- "Add category": "Aggiungi categoria",
- "Enable child collections": "Abilita raccolte figlie",
- "Allow adding records to the current collection": "Consenti l'aggiunta di record alla raccolta corrente",
- "Delete category": "Elimina categoria",
- "Edit category": "Modifica categoria",
- "Collection category": "Categoria raccolta",
- "Collection template": "Modello raccolta",
- "Sort": "Ordina",
- "Categories": "Categorie",
- "Visible": "Visibile",
- "Read only": "Solo lettura",
- "Easy reading": "Lettura facile",
- "Hidden": "Nascosto",
- "Hidden(reserved value)": "Nascosto (valore riservato)",
- "Not required": "Non richiesto",
- "Value": "Valore",
- "Disabled": "Disabilitato",
- "Enabled": "Abilitato",
- "Problematic": "Problematico",
- "Setting": "Impostazioni",
- "On": "Acceso",
- "Off": "Spento",
- "Empty": "Vuoto",
- "Linkage rule": "Regola di collegamento",
- "Linkage rules": "Regole di collegamento",
- "Condition": "Condizione",
- "Properties": "Proprietà",
- "Add linkage rule": "Aggiungi regola di collegamento",
- "Add property": "Aggiungi proprietà",
- "Category name": "Nome della categoria",
- "Roles & Permissions": "Ruoli e autorizzazioni",
- "Edit profile": "Modifica profilo",
- "Change password": "Cambia password",
- "Old password": "Vecchia password",
- "New password": "Nuova password",
- "Switch role": "Cambia ruolo",
- "Super admin": "Super Admin",
- "Language": "Lingua",
- "Allow sign up": "Consenti iscrizione",
- "Enable SMS authentication": "Abilita autenticazione SMS",
- "Sign out": "Disconnessione",
- "Cancel": "Annulla",
- "Submit": "Invia",
- "Close": "Chiudi",
- "Set the data scope": "Imposta l'ambito dei dati",
- "Set data loading mode": "Imposta modalità di caricamento dei dati",
- "Load all data when filter is empty": "Carica tutti i dati quando il filtro è vuoto",
- "Do not load data when filter is empty": "Non caricare i dati quando il filtro è vuoto",
- "Data loading mode": "Modalità di caricamento dei dati",
- "Data blocks": "Blocchi dati",
- "Filter blocks": "Blocchi filtro",
- "Table": "Tabella",
- "Table OID(Inheritance)": "Tabella OID (eredità)",
- "Form": "Modulo",
- "List": "Elenco",
- "Grid Card": "Scheda griglia",
- "pixels": "pixel",
- "Screen size": "Dimensione dello schermo",
- "Display title": "Visualizza titolo",
- "Set the count of columns displayed in a row": "Imposta il conteggio delle colonne visualizzate in una riga",
- "Column": "Colonna",
- "Phone device": "Telefono",
- "Tablet device": "Tablet",
- "Desktop device": "Desktop",
- "Large screen device": "Schermo di grandi dimensioni",
- "Collapse": "Collassa",
- "Select data source": "Seleziona origine dati",
- "Calendar": "Calendario",
- "Delete events": "Elimina eventi",
- "This event": "Questo evento",
- "This and following events": "Questo e seguenti eventi",
- "All events": "Tutti gli eventi",
- "Delete this event?": "Eliminare questo evento?",
- "Delete Event": "Elimina evento",
- "Kanban": "Kanban",
- "Gantt": "Gantt",
- "Create gantt block": "Crea blocco Gantt",
- "Progress field": "Campo avanzamento",
- "Time scale": "Scala del tempo",
- "Hour": "Ora",
- "Quarter of day": "Quarto del giorno",
- "Half of day": "Metà del giorno",
- "Year": "Anno",
- "QuarterYear": "Quarto dell' anno",
- "Select grouping field": "Seleziona il campo di raggruppamento",
- "Media": "Media",
- "Markdown": "Markdown",
- "Wysiwyg": "Wysiwyg",
- "Chart blocks": "Blocchi grafici",
- "Column chart": "Grafico a colonne",
- "Bar chart": "Grafico a barre",
- "Line chart": "Grafico a linee",
- "Pie chart": "Grafico a torta",
- "Area chart": "Grafico ad area",
- "Other chart": "Altro grafico",
- "Other blocks": "Altri blocchi",
- "In configuration": "In configurazione",
- "Chart title": "Titolo grafico",
- "Chart type": "Tipo grafico",
- "Chart config": "Configurazione grafico",
- "Templates": "Modelli",
- "Select template": "Seleziona modello",
- "Action logs": "Registri eventi",
- "Create template": "Crea modello",
- "Edit markdown": "Modifica Markdown",
- "Add block": "Aggiungi blocco",
- "Add new": "Aggiungi nuovo",
- "Add record": "Aggiungi record",
- "Add child": "Aggiungi figlio",
- "Collapse all": "Collassare tutto",
- "Expand all": "Espandere tutto",
- "Expand/Collapse": "Espandere/Collassare",
- "Default collapse": "Collassa di default",
- "Tree table": "Tabella ad albero",
- "Custom field display name": "Nome visualizzato campo personalizzato ",
- "Display fields": "Visualizza campi",
- "Edit record": "Modifica record",
- "Delete menu item": "Elimina voce di menu",
- "Add page": "Aggiungi pagina",
- "Add group": "Aggiungi gruppo",
- "Add link": "Aggiungi link",
- "Insert above": "Inserisci sopra",
- "Insert below": "Inserisci sotto",
- "Save": "Salva",
- "Delete block": "Elimina blocco",
- "Are you sure you want to delete it?": "Sei sicuro di volerlo eliminare?",
- "This is a demo text, **supports Markdown syntax**.": "Questo è un testo demo, ** supporta la sintassi di Markdown **.",
- "Filter": "Filtro",
- "Connect data blocks": "Collega blocchi di dati",
- "Action type": "Tipo di operazione",
- "Actions": "Operazioni",
- "Insert": "Inserisci",
- "Insert if not exists": "Inserisci se non esiste",
- "Insert if not exists, or update": "Inserisci se non esiste o aggiorna",
- "Determine whether a record exists by the following fields": "Determina se un record esiste dai seguenti campi",
- "Update": "Aggiorna",
- "Update record": "Aggiorna record",
- "View": "Visualizza",
- "View record": "Visualizza record",
- "Refresh": "Refresh",
- "Data changes": "Modifiche ai dati",
- "Field name": "Nome campo",
- "Before change": "Prima delle modifiche",
- "After change": "Dopo le modifiche",
- "Delete record": "Elimina record",
- "Delete collection": "Elimina raccolta",
- "Create collection": "Crea raccolta",
- "Collection display name": "Nome visualizzato raccolta",
- "Collection name": "Nome raccolta",
- "Inherits": "Eredita",
- "Primary key, unique identifier, self growth": "Chiave primaria, identificatore univoco, auto-incremento",
- "Store the creation user of each record": "Memorizza l'utente della creazione di ogni record",
- "Store the last update user of each record": "Memorizza l'ultimo utente di aggiornamento di ogni record",
- "Store the creation time of each record": "Memorizza l'orario di creazione di ogni record",
- "Store the last update time of each record": "Memorizza l'ultimo orario di aggiornamento di ogni record",
- "More options": "Più opzioni",
- "Records can be sorted": "I record possono essere ordinati",
- "Calendar collection": "Raccolta calendario",
- "General collection": "Raccolta generale",
- "Connect to database view": "Connetti alla vista del database",
- "Sync from database": "Sincronizza dal database",
- "Source collections": "Sorgente raccolte",
- "Field source": "Sorgente campo",
- "Preview": "Anteprima",
- "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Generato casualmente e può essere modificato. Supporta lettere, numeri e underscore, deve iniziare con una lettera.",
- "Edit": "Modifica",
- "Edit collection": "Modifica raccolta",
- "Configure fields": "Configura campi",
- "Configure columns": "Configura colonne",
- "Edit field": "Modifica campo",
- "Override": "Forza",
- "Override field": "Forza campo",
- "Configure fields of {{title}}": "Configura campi di {{title}}",
- "Association fields filter": "Filtro associazione campi",
- "PK & FK fields": "Campi PK e FK",
- "Association fields": "Campi associazione",
- "Choices fields": "Campi scelte",
- "System fields": "Campi di sistema",
- "General fields": "Campi generali",
- "Inherited fields": "Campi ereditati",
- "Parent collection fields": "Campi raccolta padre",
- "Basic": "Di base",
- "Single line text": "Testo a riga singola",
- "Long text": "Testo lungo",
- "Phone": "Telefono",
- "Email": "E-mail",
- "Number": "Numero",
- "Integer": "Intero",
- "Percent": "Percentuale",
- "Password": "Password",
- "Advanced type": "Avanzato",
- "Formula": "Formula",
- "Formula description": "Calcola un valore in ciascun record in base ad altri campi nello stesso record.",
- "Choices": "Scelte",
- "Checkbox": "Casella di controllo",
- "Single select": "Selezione singola",
- "Multiple select": "Selezione multipla",
- "Radio group": "Gruppo radio",
- "Checkbox group": "Gruppo cassella di controllo",
- "China region": "Regione cinese",
- "Date & Time": "Data e ora",
- "Datetime": "DateTime",
- "Relation": "Relazione",
- "Link to": "Collegamento a",
- "Link to description": "Utilizzato per creare relazioni tra raccolte in modo rapido e compatibile con gli scenari più comuni. Adatto per un uso da non sviluppatore. Se presente come campo, è una selezione a discesa utilizzata per selezionare i record dalla raccolta di destinazione. Una volta creato, genererà contemporaneamente i campi associati dell'attuale raccolta nella raccolta di destinazione.",
- "Sub-table": "Sotto-tabella",
- "Sub-details": "Sotto-dettagli",
- "Sub-form(Popover)": "Sotto-modulo (Popover)",
- "System info": "Informazioni di sistema",
- "Created at": "Creato il",
- "Last updated at": "Ultimo aggiornamento il",
- "Created by": "Creato da",
- "Last updated by": "Ultimo aggiornamento da",
- "Add field": "Aggiungi campo",
- "Field display name": "Nome visualizzato campo",
- "Field type": "Tipo campo",
- "Field interface": "Interfaccia campo",
- "Date format": "Formato data",
- "Year/Month/Day": "Anno/Mese/Giorno",
- "Year-Month-Day": "Anno-Mese-Giorno",
- "Day/Month/Year": "Giorno/Mese/Anno",
- "Show time": "Mostra orario",
- "Time format": "Formato tempo",
- "12 hour": "12 ore",
- "24 hour": "24 ore",
- "Relationship type": "Tipo di relazione",
- "Inverse relationship type": "Tipo di relazione inversa",
- "Source collection": "Raccolta sorgente",
- "Source key": "Chiave sorgente",
- "Target collection": "Raccolta di destinazione",
- "Through collection": "Attraverso la raccolta",
- "Target key": "Chiave di destinazione",
- "Foreign key": "Chiave esterna",
- "One to one": "Uno a uno",
- "One to many": "Uno a molti",
- "Many to one": "Molti a uno",
- "Many to many": "Molti a molti",
- "Foreign key 1": "Chiave esterna 1",
- "Foreign key 2": "Chiave esterna 2",
- "One to one description": "Usato per creare relazioni one-to-one. Ad esempio, un utente ha un profilo.",
- "One to many description": "Utilizzato per creare una relazione da uno a molti. Ad esempio, un paese avrà molte città e una città può essere solo in un paese. Se presente come campo, è una sotto-tabella che mostra i record della raccolta associata. Se creato, un campo molti-a-uno viene generato automaticamente nella raccolta associata.",
- "Many to one description": "Utilizzato per creare relazioni molti-a-uno. Ad esempio, una città può appartenere a un solo paese e un paese può avere molte città. Se presente come campo, è una selezione a discesa utilizzata per selezionare il record dalla raccolta associata. Una volta creato, un campo da uno a molti viene generato automaticamente nella raccolta associata.",
- "Many to many description": "Utilizzato per creare relazioni molti-a-molti. Ad esempio, uno studente avrà molti insegnanti e un insegnante avrà molti studenti. Se presente come campo, è una selezione a discesa utilizzata per selezionare i record dalla raccolta associata.",
- "Generated automatically if left blank": "Generato automaticamente se lasciato vuoto",
- "Display association fields": "Visualizza campi di associazione",
- "Display field title": "Visualizza titolo campo",
- "Field component": "Componente campo",
- "Allow multiple": "Consenti multipli",
- "Quick upload": "Caricamento rapido",
- "Select file": "Seleziona file",
- "Subtable": "Sotto-tabella",
- "Sub-form": "Sotto-modulo",
- "Field mode": "Modalità campo",
- "Allow add new data": "Consenti aggiunta nuovi dati",
- "Record picker": "Record Picker",
- "Toggles the subfield mode": "Attiva la modalità Subfield",
- "Selector mode": "Modalità selettore",
- "Subtable mode": "Modalità sotto-tabella",
- "Subform mode": "Modalità sotto-modulo",
- "Edit block title": "Modifica titolo blocco",
- "Block title": "Titolo blocco",
- "Pattern": "Modello",
- "Operator": "Operatore",
- "Editable": "Modificabile",
- "Readonly": "Solo lettura",
- "Easy-reading": "Lettura facile",
- "Add filter": "Aggiungi filtro",
- "Add filter group": "Aggiungi gruppo di filtri",
- "Comparision": "Confronto",
- "is": "è",
- "is not": "non lo è",
- "contains": "contiene",
- "does not contain": "non contiene",
- "starts with": "inizia con",
- "not starts with": "non inizia con",
- "ends with": "termina con",
- "not ends with": "non termina con",
- "is empty": "è vuoto",
- "is not empty": "non è vuoto",
- "Edit chart": "Modifica grafico",
- "Add text": "Aggiungi testo",
- "Filterable fields": "Campi filtrabili",
- "Edit button": "Pulsante Modifica",
- "Hide": "Nascondi",
- "Enable actions": "Abilita operazioni",
- "Import": "Importa",
- "Export": "Esporta",
- "Customize": "Personalizza",
- "Custom": "Personalizzato",
- "Function": "Funzione",
- "Popup form": "Modulo Popup",
- "Flexible popup": "Popup flessibile",
- "Configure actions": "Configura operazioni",
- "Display order number": "Visualizza numero ordinamento",
- "Enable drag and drop sorting": "Abilita l'ordinamento con drag and drop",
- "Triggered when the row is clicked": "Attivato quando si fa clic sulla riga",
- "Add tab": "Aggiungi scheda",
- "Disable tabs": "Disabilita le schede",
- "Details": "Dettagli",
- "Edit form": "Modifica modulo",
- "Create form": "Crea modulo",
- "Form (Edit)": "Modulo (modifica)",
- "Form (Add new)": "Modulo (aggiungi nuovo)",
- "Edit tab": "Modifica scheda",
- "Relationship blocks": "Blocchi di relazione",
- "Select record": "Seleziona Record",
- "Display name": "Visualizza nome",
- "Select icon": "Seleziona icona",
- "Custom column name": "Nome colonna personalizzato",
- "Edit description": "Modifica descrizione",
- "Required": "Richiesto",
- "Unique": "Unico",
- "Primary": "Primario",
- "Auto increment": "Incremento automatico",
- "Label field": "Campo etichetta",
- "Default is the ID field": "L'impostazione predefinita è il campo ID",
- "Set default sorting rules": "Imposta le regole di ordinamento predefinite",
- "Set validation rules": "Imposta le regole di convalida",
- "Max length": "Lunghezza massima",
- "Min length": "Lunghezza minima",
- "Maximum": "Massimo",
- "Minimum": "Minimo",
- "Max length must greater than min length": "La lunghezza massima deve essere maggiore della lunghezza minima",
- "Min length must less than max length": "La lunghezza minima deve essere inferiore della lunghezza massima",
- "Maximum must greater than minimum": "Il massimo deve essere maggiore del minimo",
- "Minimum must less than maximum": "Il minimo deve essere minore del massimo",
- "Validation rule": "Regola di convalida",
- "Add validation rule": "Aggiungi regola di convalida",
- "Format": "Formato",
- "Regular expression": "Espressione regolare",
- "Error message": "Messaggio di errore",
- "Length": "Lunghezza",
- "The field value cannot be greater than ": "Il valore del campo non può essere maggiore di",
- "The field value cannot be less than ": "Il valore del campo non può essere inferiore a",
- "The field value is not an integer number": "Il valore del campo non è un numero intero",
- "Set default value": "Imposta valore predefinito",
- "Default value": "Valore predefinito",
- "is before": "è prima",
- "is after": "è dopo",
- "is on or after": "a partire dal",
- "is on or before": "entro il",
- "is between": "è tra",
- "Upload": "Upload",
- "Select level": "Seleziona livello",
- "Province": "Provincia",
- "City": "Città",
- "Area": "Zona",
- "Street": "Strada",
- "Village": "Villaggio",
- "Must select to the last level": "Deve selezionare all'ultimo livello",
- "Move {{title}} to": "Sposta {{title}} a",
- "Target position": "Posizione di destinazione",
- "After": "Dopo",
- "Before": "Prima",
- "Add {{type}} before \"{{title}}\"": "Aggiungi {{type}} prima di \"{{title}}\"",
- "Add {{type}} after \"{{title}}\"": "Aggiungi {{type}} dopo \"{{title}}\"",
- "Add {{type}} in \"{{title}}\"": "Aggiungi {{type}} in \"{{title}}\"",
- "Original name": "Nome originale",
- "Custom name": "Nome personalizzato",
- "Custom Title": "Titolo personalizzato",
- "Options": "Opzioni",
- "Option value": "Valore opzione",
- "Option label": "Etichetta opzione",
- "Color": "Colore",
- "Background Color": "Colore sfondo",
- "Text Align": "Allineamento testo",
- "Add option": "Aggiungi opzione",
- "Related collection": "Raccolta correlata",
- "Allow linking to multiple records": "Consenti il collegamento a più record",
- "Allow uploading multiple files": "Consenti il caricamento di più file",
- "Configure calendar": "Configura calendario",
- "Title field": "Campo titolo",
- "Custom title": "Titolo personalizzato",
- "Daily": "Quotidiano",
- "Weekly": "Settimanale",
- "Monthly": "Mensile",
- "Yearly": "Annuale",
- "Repeats": "Ripeti",
- "Show lunar": "Mostra lunare",
- "Start date field": "Campo data di inizio",
- "End date field": "Campo data di fine",
- "Navigate": "Naviga",
- "Title": "Titolo",
- "Description": "Descrizione",
- "Select view": "Seleziona vista",
- "Reset": "Reset",
- "Importable fields": "Campi importabili",
- "Exportable fields": "Campi esportabili",
- "Saved successfully": "Salvataggio riuscito",
- "Nickname": "Soprannome",
- "Sign in": "Registrazione",
- "Sign in via account": "Accedi tramite account",
- "Sign in via phone": "Accedi via telefono",
- "Create an account": "Crea un account",
- "Sign up": "Iscrizione",
- "Confirm password": "Conferma password",
- "Log in with an existing account": "Accedi con account esistente",
- "Signed up successfully. It will jump to the login page.": "Registrazione riuscita. Reindirizzamento alla pagina di accesso.",
- "Password mismatch": "Password non corretta",
- "Users": "Utenti",
- "Verification code": "Codice di verifica",
- "Send code": "Invia codice",
- "Retry after {{count}} seconds": "Riprova dopo {{count}} secondi",
- "Roles": "Ruoli",
- "Add role": "Aggiungi ruolo",
- "Role name": "Nome ruolo",
- "Configure": "Configura",
- "Configure permissions": "Configura permessi",
- "Edit role": "Modifica ruolo",
- "Action permissions": "Permessi su operazioni",
- "Menu permissions": "Permessi su menu",
- "Menu item name": "Nome voce di menu",
- "Allow access": "Consenti accesso",
- "Action name": "Nome operazione",
- "Allow action": "Consenti operazione",
- "Action scope": "Ambito operazione",
- "Operate on new data": "Operare su nuovi dati",
- "Operate on existing data": "Operare su dati esistenti",
- "Yes": "Si",
- "No": "No",
- "Red": "Rosso",
- "Magenta": "Magenta",
- "Volcano": "Vulcano",
- "Orange": "Arancione",
- "Gold": "Oro",
- "Lime": "Lime",
- "Green": "Verde",
- "Cyan": "Ciano",
- "Blue": "Blu",
- "Geek blue": "Geek Blue",
- "Purple": "Viola",
- "Default": "Predefinito",
- "Add card": "Aggiungi scheda",
- "edit title": "modifica titolo",
- "Turn pages": "Volta pagine",
- "Others": "Altri",
- "Other records": "Altri record",
- "Save as template": "Salva come modello",
- "Save as block template": "Salva come modello blocco",
- "Block templates": "Modelli blocco",
- "Block template": "Modello blocco",
- "Convert reference to duplicate": "Converti il riferimento a duplicato",
- "Template name": "Nome modello",
- "Block type": "Tipo blocco",
- "No blocks to connect": "Nessun blocco per connettersi",
- "Action column": "Colonna operazioni",
- "Records per page": "Record per pagina",
- "(Fields only)": "(Solo campi)",
- "Button title": "Titolo pulsante",
- "Button icon": "Icona pulsante",
- "Submitted successfully": "Invio riuscito",
- "Operation succeeded": "L'operazione è riuscita",
- "Operation failed": "Operazione non riuscita",
- "Open mode": "Modalità aperta",
- "Popup size": "Dimensione popup",
- "Small": "Piccolo",
- "Middle": "Medio",
- "Large": "Grande",
- "Size": "Misura",
- "Oversized": "Oversize",
- "Auto": "Auto",
- "Object Fit": "Adattato all'oggetto",
- "Cover": "Cover",
- "Fill": "Riempi",
- "Contain": "Contiene",
- "Scale Down": "Ridimensiona",
- "Menu item title": "Titolo voce di menu",
- "Menu item icon": "Icona voce di menu",
- "Target": "Destinazione",
- "Position": "Posizione",
- "Insert before": "Inserire prima",
- "Insert after": "Inserire dopo",
- "UI Editor": "Editor UI",
- "ASC": "Asc",
- "DESC": "Desc",
- "Add sort field": "Aggiungi campo di ordinamento",
- "ID": "ID",
- "Identifier for program usage. Support letters, numbers and underscores, must start with an letter.": "Identificatore per l'utilizzo del programma. Supporta lettere, numeri e underscore, deve iniziare con una lettera.",
- "Drawer": "Cassetto",
- "Dialog": "Dialogo",
- "Delete action": "Elimina operazione",
- "Custom column title": "Titolo colonna personalizzata",
- "Column title": "Titolo colonna",
- "Original title: ": "Titolo originale: ",
- "Delete table column": "Elimina colonna della tabella",
- "Skip required validation": "Salta convalida richiesta",
- "Form values": "Valori modulo",
- "Fields values": "Valori campi",
- "The field has been deleted": "Il campo è stato eliminato",
- "When submitting the following fields, the saved values are": "Quando si inviano i seguenti campi, i valori salvati sono",
- "After successful submission": "Dopo una invio riuscito",
- "Then": "Poi",
- "Stay on current page": "Resta sulla pagina corrente",
- "Redirect to": "Reindirizza a",
- "Save action": "Salva operazione",
- "Exists": "Esiste",
- "Add condition": "Aggiungi condizione",
- "Add condition group": "Aggiungi gruppo di condizioni",
- "exists": "esiste",
- "not exists": "non esiste",
- "Style": "Stile",
- "=": "=",
- "≠": "≠",
- ">": ">",
- "≥": "≥",
- "<": "<",
- "≤": "≤",
- "Role UID": "Ruolo UID",
- "Precision": "Precisione",
- "Formula mode": "Modalità formula",
- "Expression": "Espressione",
- "Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "Input +, -, *, /, () per calcolare, input @ per aprire le variabili campo.",
- "Formula error.": "Errore formula.",
- "Rich Text": "Testo ricco",
- "Junction collection": "Raccolta giunzione",
- "Leave it blank, unless you need a custom intermediate table": "Lascialo vuoto, a meno che tu non abbia bisogno di una tabella intermedia personalizzata",
- "Fields": "Campi",
- "Edit field title": "Modifica titolo campo",
- "Field title": "Titolo campo",
- "Original field title: ": "Titolo campo originale:",
- "Edit tooltip": "Modifica suggerimento",
- "Delete field": "Elimina campo",
- "Select collection": "Seleziona raccolta",
- "Blank block": "Blocco vuoto",
- "Duplicate template": "Modello duplicato",
- "Reference template": "Modello di riferimento",
- "Create calendar block": "Crea blocco calendario",
- "Create kanban block": "Crea blocco kanban",
- "Grouping field": "Campo di raggruppamento",
- "Single select and radio fields can be used as the grouping field": "I campi di selezione singoli e radio possono essere utilizzati come campo di raggruppamento",
- "Tab name": "Nome della scheda",
- "Current record blocks": "Blocchi record attuale",
- "Popup message": "Messaggio popup",
- "Delete role": "Elimina il ruolo",
- "Role display name": "Nome visualizzato ruolo",
- "Default role": "Ruolo predefinito",
- "All collections use general action permissions by default; permission configured individually will override the default one.": "Tutte le raccolte utilizzano i permessi di operazioni generali per impostazione predefinita; I permessi configurati individualmente sovrascriveranno quelli predefiniti.",
- "Allows configuration of the whole system, including UI, collections, permissions, etc.": "Consente la configurazione dell'intero sistema, tra cui interfaccia utente, raccolte, permessi, ecc.",
- "New menu items are allowed to be accessed by default.": "È possibile accedere a nuove voci di menu per impostazione predefinita.",
- "Global permissions": "Permessi globali",
- "General permissions": "Permessi generali",
- "Global action permissions": "Permessi operazioni globali",
- "General action permissions": "Permessi operazioni generali",
- "Plugin settings permissions": "Permessi impostazioni plugin",
- "Allow to desgin pages": "Consenti progettazione pagine",
- "Allow to manage plugins": "Consenti gestione plugin",
- "Allow to configure plugins": "Consenti configurazione plugin",
- "Allows to configure interface": "Consente di configurare l'interfaccia",
- "Allows to install, activate, disable plugins": "Consente di installare, attivare, disabilitare i plugin",
- "Allows to configure plugins": "Consente di configurare i plugin",
- "Action display name": "Nome visualizzato operazione",
- "Allow": "Permetti",
- "Data scope": "Ambito dei dati",
- "Action on new records": "Operazione su nuovi record",
- "Action on existing records": "Operazione su record esistenti",
- "All records": "Tutti i record",
- "Own records": "Record propri",
- "Permission policy": "Policy di autorizzazione",
- "Individual": "Individuale",
- "General": "Generale",
- "Accessible": "Accessibile",
- "Configure permission": "Configura permesso",
- "Action permission": "Permesso operazione",
- "Field permission": "Permesso campo",
- "Scope name": "Nome ambito",
- "Unsaved changes": "Modifiche non salvate",
- "Are you sure you don't want to save?": "Sei sicuro di non voler salvare?",
- "Dragging": "Trascina",
- "Popup": "Popup",
- "Trigger workflow": "Trigger flusso di lavoro",
- "Request API": "Richiesta API",
- "Assign field values": "Assegna valori del campo",
- "Constant value": "Valore costante",
- "Dynamic value": "Valore dinamico",
- "Current user": "Utente attuale",
- "Current role": "Ruolo attuale",
- "Current record": "Record attuale",
- "Current collection": "Raccolta attuale",
- "Other collections": "Altre raccolte",
- "Current popup record": "Record popup attuale",
- "Parent popup record": "Record popup padre",
- "Associated records": "Record associati",
- "Parent record": "Record padre",
- "Current time": "Ora attuale",
- "System variables": "Variabili di sistema",
- "Date variables": "Variabili della data",
- "Message popup close method": "Metodo di chiusura popup di messaggio",
- "Automatic close": "Chiudi automaticamente",
- "Manually close": "Chiudi manualmente",
- "After successful update": "Dopo un aggiornamento riuscito",
- "Save record": "Salva record",
- "Updated successfully": "Aggiornamento riuscito",
- "After successful save": "Dopo un salvataggio riuscito",
- "After clicking the custom button, the following field values will be assigned according to the following form.": "Dopo aver fatto clic sul pulsante personalizza, i seguenti valori verranno assegnati in base al seguente modulo.",
- "After clicking the custom button, the following fields of the current record will be saved according to the following form.": "Dopo aver fatto clic sul pulsante personalizza, i seguenti campi del record corrente verranno salvati in base al seguente modulo.",
- "Button background color": "Colore sfondo del pulsante",
- "Highlight": "Evidenzia",
- "Danger red": "Pericolo rosso",
- "Custom request": "Personalizza richiesta",
- "Request settings": "Impostazioni richiesta",
- "Request URL": "URL richiesta",
- "Request method": "Metodo richiesta",
- "Request query parameters": "Parametri richiesta query",
- "Request headers": "Intestazioni richiesta",
- "Request body": "Corpo richiesta",
- "Request success": "Successo richiesta",
- "Invalid JSON format": "Formato JSON non valido",
- "After successful request": "Dopo una richiesta riuscita",
- "Add exportable field": "Aggiungi campo esportabile",
- "Audit logs": "Registri audit",
- "Record ID": "ID record",
- "User": "Utente",
- "Field": "Campo",
- "Select": "Seleziona",
- "Select field": "Seleziona campo",
- "Field value changes": "Modifiche valore del campo",
- "One to one (has one)": "Uno a uno (ne ha uno)",
- "One to one (belongs to)": "Uno a uno (appartiene a)",
- "Use the same time zone (GMT) for all users": "Usa lo stesso fuso orario (GMT) per tutti gli utenti",
- "Province/city/area name": "Nome provincia/città/area",
- "Enabled languages": "Lingue abilitate",
- "View all plugins": "Visualizza tutti i plugin",
- "Print": "Stampa",
- "Done": "Fatto",
- "Sign up successfully, and automatically jump to the sign in page": "Iscriviti correttamente e reindirizza automaticamente alla pagina di accesso",
- "File manager": "File Manager",
- "ACL": "ACL",
- "Collection manager": "Responsabile della raccolta",
- "Plugin manager": "Plugin Manager",
- "Local": "Locale",
- "Built-in": "Incorporato",
- "Marketplace": "Marketplace",
- "Add plugin": "Aggiungi plugin",
- "Plugin source": "Sorgente plugin",
- "Upgrade": "Aggiornamento",
- "Plugin dependencies check failed": "Controllo delle dipendenze del plugin non riuscito",
- "More details": "Maggiori dettagli",
- "Upload new version": "Carica nuova versione",
- "Version": "Versione",
- "Npm package": "Pacchetto Npm",
- "Npm package name": "Nome pacchetto Npm",
- "Upload plugin": "Carica plugin",
- "Official plugin": "Plugin ufficiale",
- "Add type": "Aggiungi tipo",
- "Changelog": "Changelog",
- "Dependencies check": "Controllo delle dipendenze",
- "Update plugin": "Aggiorna plugin",
- "Installing": "Installazione",
- "The deletion was successful.": "Cancellazione riuscita.",
- "Plugin Zip File": "File zip plugin",
- "Compressed file url": "URL file compresso",
- "Last updated": "Ultimo aggiornamento",
- "PackageName": "Nome pacchetto",
- "DisplayName": "Nome da visualizzare",
- "Readme": "Readme",
- "Dependencies compatibility check": "Controllo compatibilità delle dipendenze",
- "Plugin dependencies check failed, you should change the dependent version to meet the version requirements.": "Controllo delle dipendenze del plugin non riuscito, è necessario modificare la versione dipendente per soddisfare i requisiti della versione.",
- "Version range": "Range versione",
- "Plugin's version": "Versione plugin",
- "Result": "Risultato",
- "No CHANGELOG.md file": "Nessun file Changelog.md",
- "No README.md file": "Nessun file readme.md",
- "Homepage": "Homepage",
- "Drag and drop the file here or click to upload, file size should not exceed 30M": "Trascina e rilascia il file qui o fai clic per caricare, la dimensione del file non deve superare i 30M",
- "Dependencies check failed, can't enable.": "Il controllo delle dipendenze non è riuscito, impossibile abilitare.",
- "Plugin starting...": "Avvio plugin...",
- "Plugin stopping...": "Interruzione plugin ...",
- "Are you sure to delete this plugin?": "Sei sicuro di eliminare questo plugin?",
- "Are you sure to disable this plugin?": "Sei sicuro di disabilitare questo plugin?",
- "re-download file": "ri-scarica file",
- "Not enabled": "Non abilitato",
- "Search plugin": "Ricerca plugin",
- "Author": "Autore",
- "Plugin loading failed. Please check the server logs.": "Il caricamento del plugin non è riuscito. Si prega di controllare i registri del server.",
- "Coming soon...": "Prossimamente...",
- "All plugin settings": "Tutte le impostazioni del plugin",
- "Bookmark": "Segnalibro",
- "Manage all settings": "Gestisci tutte le impostazioni",
- "Create inverse field in the target collection": "Crea campo inverso nella raccolta di destinazione",
- "Inverse field name": "Nome campo inverso",
- "Inverse field display name": "Nome visualizzato campo inverso",
- "Bulk update": "Aggiornamento di massa",
- "After successful bulk update": "Dopo un aggiornamento di massa riuscito",
- "Bulk edit": "Modifica di massa",
- "Data will be updated": "I dati verranno aggiornati",
- "Selected": "Selezionato",
- "All": "Tutto",
- "Update selected data?": "Aggiornare i dati selezionati?",
- "Update all data?": "Aggiornare tutti i dati?",
- "Remains the same": "Rimane lo stesso",
- "Changed to": "Cambiato in",
- "Clear": "Pulisci",
- "Add attach": "Aggiungi allegato",
- "Please select the records to be updated": "Si prega di selezionare i record da aggiornare",
- "Selector": "Selettore",
- "Inner": "Interno",
- "Search and select collection": "Cerca e seleziona la raccolta",
- "Please fill in the iframe URL": "Si prega di compilare l'URL iFrame",
- "Fix block": "Fissa blocco",
- "Plugin name": "Nome plugin",
- "Plugin tab name": "Nome scheda plugin",
- "AutoGenId": "Campo ID generato automaticamente",
- "CreatedBy": "Creato da",
- "UpdatedBy": "Aggiornato da",
- "CreatedAt": "Creato il",
- "UpdatedAt": "Aggiornato il",
- "Column width": "Larghezza colonna",
- "Sortable": "Ordinabile",
- "Enable link": "Abilita link",
- "This is likely a NocoBase internals bug. Please open an issue at <1>here1>": "Questo sembra un bug interno di NocoBase. Si prega di aprire un ticket <1>qui1>",
- "Render Failed": "Rendering non riuscito",
- "App error": "Errore app",
- "Feedback": "Feedback",
- "Try again": "Riprova",
- "Download logs": "Download registri",
- "Data template": "Modello dati",
- "Duplicate": "Duplica",
- "Duplicating": "Duplicazione",
- "Duplicate mode": "Modalità duplicazione",
- "Quick duplicate": "Duplicazione veloce",
- "Duplicate and continue": "Duplica e continua",
- "Please configure the duplicate fields": "Si prega di configurare i campi duplicati",
- "Add": "Aggiungi",
- "Add new mode": "Modalità aggiungi nuovo",
- "Quick add": "Aggiunta rapida",
- "Modal add": "Aggiunta modale",
- "Save mode": "Modalità salvataggio",
- "First or create": "Prima o crea",
- "Update or create": "Aggiorna o crea",
- "Find by the following fields": "Trova dai seguenti campi",
- "Create": "Crea",
- "Current form": "Modulo corrente",
- "Current object": "Oggetto corrente",
- "Linkage with form fields": "Collegamento con i campi del modulo",
- "Allow add new, update and delete actions": "Consenti operazioni aggiungi nuovo, aggiorna ed elimina",
- "Date display format": "Formato di visualizzazione della data",
- "Assign data scope for the template": "Assegna l'ambito dei dati per il modello",
- "Table selected records": "Tabella record selezionati",
- "Tag": "Etichetta",
- "Tag color field": "Campo colore etichetta",
- "Sync successfully": "Sincronizzazione riuscita",
- "Sync from form fields": "Sincronizzazione dai campi del modulo",
- "Select all": "Seleziona tutto",
- "Restart": "Ricomincia",
- "Restart application": "Riavvia applicazione",
- "Cascade Select": "Seleziona in cascata",
- "Execute": "Esegui",
- "Please use a valid SELECT or WITH AS statement": "Si prega di utilizzare un' istruzione SELECT o WITH AS valida",
- "Please confirm the SQL statement first": "Si prega di confermare prima l'istruzione SQL",
- "Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects": "Elimina automaticamente gli oggetti che dipendono dalla raccolta (come le viste) e, a loro volta, tutti gli oggetti che dipendono da tali oggetti",
- "Sign in with another account": "Accedi con un altro account",
- "Return to the main application": "Torna alla applicazione principale",
- "Permission deined": "Permesso negato",
- "loading": "caricamento",
- "name is required": "nome richiesto",
- "data source": "sorgente dati",
- "Data source": "Sorgente dati",
- "DataSource": "Sorgente Dati",
- "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "Il {{type}} \"{{name}}\" potrebbe essere stato eliminato. Si prega di rimuovere {{blockType}}.",
- "Preset fields": "Campi preimpostati",
- "Home page": "Home page",
- "Handbook": "Manuale",
- "License": "Licenza",
- "Generic properties": "Proprietà generiche",
- "Specific properties": "Proprietà specifiche",
- "Used for drag and drop sorting scenarios, supporting grouping sorting": "Utilizzato per scenari con drag and drop, supporta ordinamento raggruppato",
- "Grouped sorting": "Ordinamento raggruppato",
- "When a field is selected for grouping, it will be grouped first before sorting.": "Quando viene selezionato un campo per il raggruppamento, verrà raggruppato prima dell'ordinamento.",
- "Departments": "Dipartimenti",
- "Main department": "Dipartimento principale",
- "Department name": "Nome del dipartimento",
- "Superior department": "Dipartimento superiore",
- "Owners": "Proprietari",
- "Plugin settings": "Impostazioni plugin",
- "Menu": "Menu",
- "Drag and drop sorting field": "Campi ordinamento drag and drop",
- "This variable has been deprecated and can be replaced with \"Current form\"": "Questa variabile è stata deprecata e può essere sostituita con \"Current form\"",
- "The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "Il valore di questa variabile deriva dalla stringa di ricerca nell'URL della pagina. Questa variabile può essere utilizzata normalmente solo quando la pagina ha una stringa di ricerca.",
- "URL search params": "Parametri di ricerca URL",
- "Expand All": "Espandere tutto",
- "Search": "Ricerca",
- "Clear default value": "Cancella il valore predefinito",
- "Open in new window": "Apri in una nuova finestra",
- "Sorry, the page you visited does not exist.": "Spiacente, la pagina che hai visitato non esiste.",
- "is none of": "non è nessuno di",
- "is any of": "è uno di",
- "Plugin dependency version mismatch": "Mancata corrispondenza della versione della dipendenza del 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?": "L'attuale versione della dipendenza del plugin non corrisponde alla versione dell'applicazione e potrebbe non funzionare correttamente. Sei sicuro di voler continuare a abilitare il plugin?",
- "Allow multiple selection": "Consenti selezione multipla",
- "Parent object": "Oggetto padre",
- "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": "Ometti calcolo del numero totale di record della tabella durante l'impaginazione per accelerare il caricamento. Si consiglia di abilitare questa opzione per tabelle con grandi quantità di dati",
- "Enable secondary confirmation": "Abilita conferma secondaria",
- "Notification": "Notifica",
- "Ellipsis overflow content": "Contenuto Ellipsis overflow",
- "Hide column": "Nascondi colonna",
- "In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "In modalità di configurazione, l'intera colonna diventa trasparente. In modalità non di configurazione, l'intera colonna verrà nascosta. Anche se l'intera colonna è nascosta, i suoi valori predefiniti configurati e le altre impostazioni avranno comunque effetto."
-}
+{
+ "Display <1><0>100><1>201><2>502><3>1003>1> items per page": "Visualizza <1><0>100><1>201><2>502><3>1003>1> articoli per pagina",
+ "Meet <1><0>All0><1>Any1>1> conditions in the group": "Soddisfa<1><0>Tutte0><1>Qualsiasi1>1>condizioni nel gruppo",
+ "Open in<1><0>Modal0><1>Drawer1><2>Window2>1>": "Apri in<1><0>Modale0><1>Cassetto1><2>Finestra2>1>",
+ "{{count}} filter items": "{{Count}} filtri elementi",
+ "{{count}} more items": "{{Count}} altri elementi",
+ "Total {{count}} items": "{{count}} elementi totali",
+ "Today": "Oggi",
+ "Yesterday": "Ieri",
+ "Tomorrow": "Domani",
+ "Month": "Mese",
+ "Week": "Settimana",
+ "This week": "Questa settimana",
+ "This month": "Questo mese",
+ "This year": "Quest'anno",
+ "Next year": "Anno prossimo",
+ "Last week": "Settimana scorsa",
+ "Next week": "Prossima settimana",
+ "Last month": "Mese scorso",
+ "Next month": "Mese prossimo",
+ "Last quarter": "Ultimo trimestre",
+ "This quarter": "Questo trimestre",
+ "Next quarter": "Prossimo trimestre",
+ "Last year": "Anno scorso",
+ "Last 7 days": "Ultimi 7 giorni",
+ "Last 30 days": "Ultimi 30 giorni",
+ "Last 90 days": "Ultimi 90 giorni",
+ "Next 7 days": "Prossimi 7 giorni",
+ "Next 30 days": "Prossimi 30 giorni",
+ "Next 90 days": "Prossimi 90 giorni",
+ "Work week": "Settimana lavorativa",
+ "Day": "Giorno",
+ "Agenda": "Agenda",
+ "Date": "Data",
+ "Time": "Tempo",
+ "Event": "Evento",
+ "None": "Nessuno",
+ "Unconnected": "Non collegato",
+ "System settings": "Impostazioni di sistema",
+ "System title": "Titolo del sistema",
+ "Settings": "Impostazioni",
+ "Logo": "Logo",
+ "Add menu item": "Aggiungi voce di menu",
+ "Page": "Pagina",
+ "Name": "Nome",
+ "Icon": "Icona",
+ "Group": "Gruppo",
+ "Link": "Collegamento",
+ "Save conditions": "Salva condizioni",
+ "Edit menu item": "Modifica voce di menu",
+ "Move to": "Passa a",
+ "Insert left": "Inserisci a sinistra",
+ "Insert right": "Inserire a destra",
+ "Insert inner": "Inserire dentro",
+ "Delete": "Eliminare",
+ "Disassociate": "Dissociare",
+ "Disassociate record": "Dissociare record",
+ "Are you sure you want to disassociate it?": "Sei sicuro di voler dissociare?",
+ "UI editor": "Editor UI",
+ "Collection": "Raccolta",
+ "Collection selector": "Selettore di raccolta",
+ "Providing certain collections as options for users, typically used in polymorphic or inheritance scenarios": "Fornire alcune raccolte come opzioni per gli utenti, in genere utilizzati negli scenari polimorfici o ereditari",
+ "Collections & Fields": "Raccolte e campi",
+ "All collections": "Tutte le raccolte",
+ "Add category": "Aggiungi categoria",
+ "Enable child collections": "Abilita raccolte figlie",
+ "Allow adding records to the current collection": "Consenti l'aggiunta di record alla raccolta corrente",
+ "Delete category": "Elimina categoria",
+ "Edit category": "Modifica categoria",
+ "Collection category": "Categoria raccolta",
+ "Collection template": "Modello raccolta",
+ "Sort": "Ordina",
+ "Categories": "Categorie",
+ "Visible": "Visibile",
+ "Read only": "Solo lettura",
+ "Easy reading": "Lettura facile",
+ "Hidden": "Nascosto",
+ "Hidden(reserved value)": "Nascosto (valore riservato)",
+ "Not required": "Non richiesto",
+ "Value": "Valore",
+ "Disabled": "Disabilitato",
+ "Enabled": "Abilitato",
+ "Problematic": "Con problemi",
+ "Setting": "Impostazioni",
+ "On": "Acceso",
+ "Off": "Spento",
+ "Empty": "Vuoto",
+ "Linkage rule": "Regola di collegamento",
+ "Linkage rules": "Regole di collegamento",
+ "Condition": "Condizione",
+ "Properties": "Proprietà",
+ "Add linkage rule": "Aggiungi regola di collegamento",
+ "Add property": "Aggiungi proprietà",
+ "Category name": "Nome della categoria",
+ "Roles & Permissions": "Ruoli e autorizzazioni",
+ "Edit profile": "Modifica profilo",
+ "Change password": "Cambia password",
+ "Old password": "Vecchia password",
+ "New password": "Nuova password",
+ "Switch role": "Cambia ruolo",
+ "Super admin": "Super Admin",
+ "Language": "Lingua",
+ "Allow sign up": "Consenti iscrizione",
+ "Enable SMS authentication": "Abilita autenticazione SMS",
+ "Sign out": "Disconnessione",
+ "Cancel": "Annulla",
+ "Submit": "Invia",
+ "Close": "Chiudi",
+ "Set the data scope": "Imposta l'ambito dei dati",
+ "Set data loading mode": "Imposta modalità di caricamento dei dati",
+ "Load all data when filter is empty": "Carica tutti i dati quando il filtro è vuoto",
+ "Do not load data when filter is empty": "Non caricare i dati quando il filtro è vuoto",
+ "Data loading mode": "Modalità di caricamento dei dati",
+ "Data blocks": "Blocchi dati",
+ "Filter blocks": "Blocchi filtro",
+ "Table": "Tabella",
+ "Table OID(Inheritance)": "Tabella OID (eredità)",
+ "Form": "Modulo",
+ "List": "Elenco",
+ "Grid Card": "Scheda griglia",
+ "pixels": "pixel",
+ "Screen size": "Dimensione dello schermo",
+ "Display title": "Visualizza titolo",
+ "Set the count of columns displayed in a row": "Imposta conteggio delle colonne visualizzate in una riga",
+ "Column": "Colonna",
+ "Phone device": "Telefono",
+ "Tablet device": "Tablet",
+ "Desktop device": "Desktop",
+ "Large screen device": "Schermo di grandi dimensioni",
+ "Collapse": "Comprimi",
+ "Select data source": "Seleziona origine dati",
+ "Calendar": "Calendario",
+ "Delete events": "Elimina eventi",
+ "This event": "Questo evento",
+ "This and following events": "Questo e seguenti eventi",
+ "All events": "Tutti gli eventi",
+ "Delete this event?": "Eliminare questo evento?",
+ "Delete Event": "Elimina evento",
+ "Kanban": "Kanban",
+ "Gantt": "Gantt",
+ "Create gantt block": "Crea blocco Gantt",
+ "Progress field": "Campo avanzamento",
+ "Time scale": "Scala del tempo",
+ "Hour": "Ora",
+ "Quarter of day": "Quarto del giorno",
+ "Half of day": "Metà del giorno",
+ "Year": "Anno",
+ "QuarterYear": "Quarto dell' anno",
+ "Select grouping field": "Seleziona campo di raggruppamento",
+ "Media": "Media",
+ "Markdown": "Markdown",
+ "Wysiwyg": "Wysiwyg",
+ "Chart blocks": "Blocchi grafici",
+ "Column chart": "Grafico a colonne",
+ "Bar chart": "Grafico a barre",
+ "Line chart": "Grafico a linee",
+ "Pie chart": "Grafico a torta",
+ "Area chart": "Grafico ad area",
+ "Other chart": "Altro grafico",
+ "Other blocks": "Altri blocchi",
+ "In configuration": "In configurazione",
+ "Chart title": "Titolo grafico",
+ "Chart type": "Tipo grafico",
+ "Chart config": "Configurazione grafico",
+ "Templates": "Modelli",
+ "Select template": "Seleziona modello",
+ "Action logs": "Registri eventi",
+ "Create template": "Crea modello",
+ "Edit markdown": "Modifica Markdown",
+ "Add block": "Aggiungi blocco",
+ "Add new": "Aggiungi nuovo",
+ "Add record": "Aggiungi record",
+ "Add child": "Aggiungi figlio",
+ "Collapse all": "Comprimi tutto",
+ "Expand all": "Espandi tutto",
+ "Expand/Collapse": "Espandi/Comprimi",
+ "Default collapse": "Comprimi di default",
+ "Tree table": "Tabella struttura ad albero",
+ "Custom field display name": "Nome visualizzato campo personalizzato ",
+ "Display fields": "Visualizza campi",
+ "Edit record": "Modifica record",
+ "Delete menu item": "Elimina voce di menu",
+ "Add page": "Aggiungi pagina",
+ "Add group": "Aggiungi gruppo",
+ "Add link": "Aggiungi link",
+ "Insert above": "Inserisci sopra",
+ "Insert below": "Inserisci sotto",
+ "Save": "Salva",
+ "Delete block": "Elimina blocco",
+ "Are you sure you want to delete it?": "Sei sicuro di volerlo eliminare?",
+ "This is a demo text, **supports Markdown syntax**.": "Questo è un testo demo, ** supporta la sintassi di Markdown **.",
+ "Filter": "Filtro",
+ "Connect data blocks": "Collega blocchi di dati",
+ "Action type": "Tipo di azione",
+ "Actions": "Azioni",
+ "Insert": "Inserisci",
+ "Insert if not exists": "Inserisci se non esiste",
+ "Insert if not exists, or update": "Inserisci se non esiste o aggiorna",
+ "Determine whether a record exists by the following fields": "Determina se un record esiste dai seguenti campi",
+ "Update": "Aggiorna",
+ "Update record": "Aggiorna record",
+ "View": "Visualizza",
+ "View record": "Visualizza record",
+ "Refresh": "Refresh",
+ "Data changes": "Modifiche ai dati",
+ "Field name": "Nome campo",
+ "Before change": "Prima delle modifiche",
+ "After change": "Dopo le modifiche",
+ "Delete record": "Elimina record",
+ "Delete collection": "Elimina raccolta",
+ "Create collection": "Crea raccolta",
+ "Collection display name": "Nome visualizzato raccolta",
+ "Collection name": "Nome raccolta",
+ "Inherits": "Eredita",
+ "Primary key, unique identifier, self growth": "Chiave primaria, identificatore univoco, auto-incremento",
+ "Store the creation user of each record": "Memorizza l'utente della creazione di ogni record",
+ "Store the last update user of each record": "Memorizza l'ultimo utente di aggiornamento di ogni record",
+ "Store the creation time of each record": "Memorizza l'orario di creazione di ogni record",
+ "Store the last update time of each record": "Memorizza l'ultimo orario di aggiornamento di ogni record",
+ "More options": "Più opzioni",
+ "Records can be sorted": "I record possono essere ordinati",
+ "Calendar collection": "Raccolta calendario",
+ "General collection": "Raccolta generale",
+ "Connect to database view": "Connetti alla vista del database",
+ "Sync from database": "Sincronizza dal database",
+ "Source collections": "Sorgente raccolte",
+ "Field source": "Sorgente campo",
+ "Preview": "Anteprima",
+ "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Generato casualmente e può essere modificato. Supporta lettere, numeri e underscore, deve iniziare con una lettera.",
+ "Edit": "Modifica",
+ "Edit collection": "Modifica raccolta",
+ "Configure fields": "Configura campi",
+ "Configure columns": "Configura colonne",
+ "Edit field": "Modifica campo",
+ "Override": "Forza",
+ "Override field": "Forza campo",
+ "Configure fields of {{title}}": "Configura campi di {{title}}",
+ "Association fields filter": "Filtro associazione campi",
+ "PK & FK fields": "Campi PK e FK",
+ "Association fields": "Campi associazione",
+ "Choices fields": "Campi scelte",
+ "System fields": "Campi di sistema",
+ "General fields": "Campi generali",
+ "Inherited fields": "Campi ereditati",
+ "Parent collection fields": "Campi raccolta padre",
+ "Basic": "Di base",
+ "Single line text": "Testo a riga singola",
+ "Long text": "Testo lungo",
+ "Phone": "Telefono",
+ "Email": "E-mail",
+ "Number": "Numero",
+ "Integer": "Intero",
+ "Percent": "Percentuale",
+ "Password": "Password",
+ "Advanced type": "Avanzato",
+ "Choices": "Scelte",
+ "Checkbox": "Casella di controllo",
+ "Single select": "Selezione singola",
+ "Multiple select": "Selezione multipla",
+ "Radio group": "Gruppo radio",
+ "Checkbox group": "Gruppo cassella di controllo",
+ "China region": "Regione cinese",
+ "Date & Time": "Data e ora",
+ "Datetime": "DateTime",
+ "Relation": "Relazione",
+ "Link to": "Collegamento a",
+ "Link to description": "Utilizzato per creare relazioni tra raccolte in modo rapido e compatibile con gli scenari più comuni. Adatto per un uso da non sviluppatore. Se presente come campo, è una selezione a discesa utilizzata per selezionare i record dalla raccolta di destinazione. Una volta creato, genererà contemporaneamente i campi associati dell'attuale raccolta nella raccolta di destinazione.",
+ "Sub-table": "Sotto-tabella",
+ "Sub-details": "Sotto-dettagli",
+ "Sub-form(Popover)": "Sotto-modulo (Popover)",
+ "System info": "Informazioni di sistema",
+ "Created at": "Creato il",
+ "Last updated at": "Ultimo aggiornamento il",
+ "Created by": "Creato da",
+ "Last updated by": "Ultimo aggiornamento da",
+ "Add field": "Aggiungi campo",
+ "Field display name": "Nome visualizzato campo",
+ "Field type": "Tipo campo",
+ "Field interface": "Interfaccia campo",
+ "Date format": "Formato data",
+ "Year/Month/Day": "Anno/Mese/Giorno",
+ "Year-Month-Day": "Anno-Mese-Giorno",
+ "Day/Month/Year": "Giorno/Mese/Anno",
+ "Show time": "Mostra orario",
+ "Time format": "Formato tempo",
+ "12 hour": "12 ore",
+ "24 hour": "24 ore",
+ "Relationship type": "Tipo di relazione",
+ "Inverse relationship type": "Tipo di relazione inversa",
+ "Source collection": "Raccolta sorgente",
+ "Source key": "Chiave sorgente",
+ "Target collection": "Raccolta di destinazione",
+ "Through collection": "Attraverso la raccolta",
+ "Target key": "Chiave di destinazione",
+ "Foreign key": "Chiave esterna",
+ "One to one": "Uno a uno",
+ "One to many": "Uno a molti",
+ "Many to one": "Molti a uno",
+ "Many to many": "Molti a molti",
+ "Foreign key 1": "Chiave esterna 1",
+ "Foreign key 2": "Chiave esterna 2",
+ "One to one description": "Usato per creare relazioni one-to-one. Ad esempio, un utente ha un profilo.",
+ "One to many description": "Utilizzato per creare una relazione da uno a molti. Ad esempio, un paese avrà molte città e una città può essere solo in un paese. Se presente come campo, è una sotto-tabella che mostra i record della raccolta associata. Se creato, un campo molti-a-uno viene generato automaticamente nella raccolta associata.",
+ "Many to one description": "Utilizzato per creare relazioni molti-a-uno. Ad esempio, una città può appartenere a un solo paese e un paese può avere molte città. Se presente come campo, è una selezione a discesa utilizzata per selezionare il record dalla raccolta associata. Una volta creato, un campo da uno a molti viene generato automaticamente nella raccolta associata.",
+ "Many to many description": "Utilizzato per creare relazioni molti-a-molti. Ad esempio, uno studente avrà molti insegnanti e un insegnante avrà molti studenti. Se presente come campo, è una selezione a discesa utilizzata per selezionare i record dalla raccolta associata.",
+ "Generated automatically if left blank": "Generato automaticamente se lasciato vuoto",
+ "Display association fields": "Visualizza campi di associazione",
+ "Display field title": "Visualizza titolo campo",
+ "Field component": "Componente campo",
+ "Allow multiple": "Consenti multipli",
+ "Quick upload": "Caricamento rapido",
+ "Select file": "Seleziona file",
+ "Subtable": "Sotto-tabella",
+ "Sub-form": "Sotto-modulo",
+ "Field mode": "Modalità campo",
+ "Allow add new data": "Consenti aggiunta nuovi dati",
+ "Record picker": "Record Picker",
+ "Toggles the subfield mode": "Attiva la modalità Subfield",
+ "Selector mode": "Modalità selettore",
+ "Subtable mode": "Modalità sotto-tabella",
+ "Subform mode": "Modalità sotto-modulo",
+ "Block title": "Titolo blocco",
+ "Pattern": "Modello",
+ "Operator": "Operatore",
+ "Editable": "Modificabile",
+ "Readonly": "Solo lettura",
+ "Easy-reading": "Lettura facile",
+ "Add filter": "Aggiungi filtro",
+ "Add filter group": "Aggiungi gruppo di filtri",
+ "Comparision": "Confronto",
+ "is": "è",
+ "is not": "non lo è",
+ "contains": "contiene",
+ "does not contain": "non contiene",
+ "starts with": "inizia con",
+ "not starts with": "non inizia con",
+ "ends with": "termina con",
+ "not ends with": "non termina con",
+ "is empty": "è vuoto",
+ "is not empty": "non è vuoto",
+ "Edit chart": "Modifica grafico",
+ "Filterable fields": "Campi filtrabili",
+ "Edit button": "Modifica pulsante",
+ "Hide": "Nascondi",
+ "Enable actions": "Abilita azioni",
+ "Import": "Importa",
+ "Export": "Esporta",
+ "Customize": "Personalizza",
+ "Custom": "Personalizzato",
+ "Function": "Funzione",
+ "Popup form": "Modulo Popup",
+ "Flexible popup": "Popup flessibile",
+ "Configure actions": "Configura azioni",
+ "Display order number": "Visualizza numero ordinamento",
+ "Enable drag and drop sorting": "Abilita l'ordinamento con drag and drop",
+ "Triggered when the row is clicked": "Attivato quando si fa clic sulla riga",
+ "Add tab": "Aggiungi scheda",
+ "Disable tabs": "Disabilita schede",
+ "Details": "Dettagli",
+ "Edit form": "Modifica modulo",
+ "Create form": "Crea modulo",
+ "Form (Edit)": "Modulo (modifica)",
+ "Form (Add new)": "Modulo (aggiungi nuovo)",
+ "Edit tab": "Modifica scheda",
+ "Relationship blocks": "Blocchi di relazione",
+ "Select record": "Seleziona Record",
+ "Display name": "Visualizza nome",
+ "Select icon": "Seleziona icona",
+ "Custom column name": "Nome colonna personalizzato",
+ "Edit description": "Modifica descrizione",
+ "Required": "Richiesto",
+ "Unique": "Univoco",
+ "Primary": "Primario",
+ "Auto increment": "Incremento automatico",
+ "Label field": "Campo etichetta",
+ "Default is the ID field": "L'impostazione predefinita è il campo ID",
+ "Set default sorting rules": "Imposta regole di ordinamento predefinite",
+ "Set validation rules": "Imposta regole di convalida",
+ "Max length": "Lunghezza massima",
+ "Min length": "Lunghezza minima",
+ "Maximum": "Massimo",
+ "Minimum": "Minimo",
+ "Max length must greater than min length": "La lunghezza massima deve essere maggiore della lunghezza minima",
+ "Min length must less than max length": "La lunghezza minima deve essere inferiore della lunghezza massima",
+ "Maximum must greater than minimum": "Il massimo deve essere maggiore del minimo",
+ "Minimum must less than maximum": "Il minimo deve essere minore del massimo",
+ "Validation rule": "Regola di convalida",
+ "Add validation rule": "Aggiungi regola di convalida",
+ "Format": "Formato",
+ "Regular expression": "Espressione regolare",
+ "Error message": "Messaggio di errore",
+ "Length": "Lunghezza",
+ "The field value cannot be greater than ": "Il valore del campo non può essere maggiore di",
+ "The field value cannot be less than ": "Il valore del campo non può essere inferiore a",
+ "The field value is not an integer number": "Il valore del campo non è un numero intero",
+ "Set default value": "Imposta valore predefinito",
+ "Default value": "Valore predefinito",
+ "is before": "è prima",
+ "is after": "è dopo",
+ "is on or after": "a partire dal",
+ "is on or before": "entro il",
+ "is between": "è tra",
+ "Upload": "Upload",
+ "Select level": "Seleziona livello",
+ "Province": "Provincia",
+ "City": "Città",
+ "Area": "Zona",
+ "Street": "Strada",
+ "Village": "Villaggio",
+ "Must select to the last level": "Deve selezionare all'ultimo livello",
+ "Move {{title}} to": "Sposta {{title}} a",
+ "Target position": "Posizione di destinazione",
+ "After": "Dopo",
+ "Before": "Prima",
+ "Add {{type}} before \"{{title}}\"": "Aggiungi {{type}} prima di \"{{title}}\"",
+ "Add {{type}} after \"{{title}}\"": "Aggiungi {{type}} dopo \"{{title}}\"",
+ "Add {{type}} in \"{{title}}\"": "Aggiungi {{type}} in \"{{title}}\"",
+ "Original name": "Nome originale",
+ "Custom name": "Nome personalizzato",
+ "Custom Title": "Titolo personalizzato",
+ "Options": "Opzioni",
+ "Option value": "Valore opzione",
+ "Option label": "Etichetta opzione",
+ "Color": "Colore",
+ "Background Color": "Colore sfondo",
+ "Text Align": "Allineamento testo",
+ "Add option": "Aggiungi opzione",
+ "Related collection": "Raccolta correlata",
+ "Allow linking to multiple records": "Consenti collegamento a più record",
+ "Configure calendar": "Configura calendario",
+ "Title field": "Campo titolo",
+ "Custom title": "Titolo personalizzato",
+ "Daily": "Quotidiano",
+ "Weekly": "Settimanale",
+ "Monthly": "Mensile",
+ "Yearly": "Annuale",
+ "Repeats": "Ripeti",
+ "Show lunar": "Mostra lunare",
+ "Start date field": "Campo data di inizio",
+ "End date field": "Campo data di fine",
+ "Navigate": "Naviga",
+ "Title": "Titolo",
+ "Description": "Descrizione",
+ "Select view": "Seleziona vista",
+ "Reset": "Reset",
+ "Importable fields": "Campi importabili",
+ "Exportable fields": "Campi esportabili",
+ "Saved successfully": "Salvataggio riuscito",
+ "Nickname": "Soprannome",
+ "Sign in": "Registrazione",
+ "Sign in via account": "Accedi tramite account",
+ "Sign in via phone": "Accedi via telefono",
+ "Create an account": "Crea un account",
+ "Sign up": "Iscrizione",
+ "Confirm password": "Conferma password",
+ "Log in with an existing account": "Accedi con account esistente",
+ "Signed up successfully. It will jump to the login page.": "Registrazione riuscita. Reindirizzamento alla pagina di accesso.",
+ "Password mismatch": "Password non corretta",
+ "Users": "Utenti",
+ "Verification code": "Codice di verifica",
+ "Send code": "Invia codice",
+ "Retry after {{count}} seconds": "Riprova dopo {{count}} secondi",
+ "Roles": "Ruoli",
+ "Add role": "Aggiungi ruolo",
+ "Role name": "Nome ruolo",
+ "Configure": "Configura",
+ "Configure permissions": "Configura permessi",
+ "Edit role": "Modifica ruolo",
+ "Action permissions": "Permessi su azioni",
+ "Menu permissions": "Permessi su menu",
+ "Menu item name": "Nome voce di menu",
+ "Allow access": "Consenti accesso",
+ "Action name": "Nome azione",
+ "Allow action": "Consenti azione",
+ "Action scope": "Ambito azione",
+ "Operate on new data": "Operare su nuovi dati",
+ "Operate on existing data": "Operare su dati esistenti",
+ "Yes": "Si",
+ "No": "No",
+ "Red": "Rosso",
+ "Magenta": "Magenta",
+ "Volcano": "Vulcano",
+ "Orange": "Arancione",
+ "Gold": "Oro",
+ "Lime": "Lime",
+ "Green": "Verde",
+ "Cyan": "Ciano",
+ "Blue": "Blu",
+ "Geek blue": "Geek Blue",
+ "Purple": "Viola",
+ "Default": "Predefinito",
+ "Add card": "Aggiungi scheda",
+ "edit title": "modifica titolo",
+ "Turn pages": "Volta pagine",
+ "Others": "Altri",
+ "Other records": "Altri record",
+ "Save as reference template": "Salva come modello di riferimento",
+ "Save as inherited template": "Salva come modello ereditato",
+ "Save as block template": "Salva come modello blocco",
+ "Block templates": "Modelli blocco",
+ "Block template": "Modello blocco",
+ "Convert reference to duplicate": "Converti riferimento a duplicato",
+ "Template name": "Nome modello",
+ "Block type": "Tipo blocco",
+ "No blocks to connect": "Nessun blocco per connettersi",
+ "Action column": "Colonna azioni",
+ "Records per page": "Record per pagina",
+ "(Fields only)": "(Solo campi)",
+ "Button title": "Titolo pulsante",
+ "Button icon": "Icona pulsante",
+ "Submitted successfully": "Invio riuscito",
+ "Operation succeeded": "L'operazione è riuscita",
+ "Operation failed": "Operazione non riuscita",
+ "Open mode": "Modalità aperta",
+ "Popup size": "Dimensione popup",
+ "Small": "Piccolo",
+ "Middle": "Medio",
+ "Large": "Grande",
+ "Size": "Misura",
+ "Oversized": "Oversize",
+ "Auto": "Auto",
+ "Object Fit": "Adattato all'oggetto",
+ "Cover": "Cover",
+ "Fill": "Riempi",
+ "Contain": "Contiene",
+ "Scale Down": "Ridimensiona",
+ "Menu item title": "Titolo voce di menu",
+ "Menu item icon": "Icona voce di menu",
+ "Target": "Destinazione",
+ "Position": "Posizione",
+ "Insert before": "Inserire prima",
+ "Insert after": "Inserire dopo",
+ "UI Editor": "Editor UI",
+ "ASC": "Asc",
+ "DESC": "Desc",
+ "Add sort field": "Aggiungi campo di ordinamento",
+ "ID": "ID",
+ "Identifier for program usage. Support letters, numbers and underscores, must start with an letter.": "Identificatore per l'utilizzo del programma. Supporta lettere, numeri e underscore, deve iniziare con una lettera.",
+ "Drawer": "Cassetto",
+ "Dialog": "Dialogo",
+ "Delete action": "Elimina azione",
+ "Custom column title": "Titolo colonna personalizzata",
+ "Column title": "Titolo colonna",
+ "Original title: ": "Titolo originale: ",
+ "Delete table column": "Elimina colonna della tabella",
+ "Skip required validation": "Salta convalida richiesta",
+ "Form values": "Valori modulo",
+ "Fields values": "Valori campi",
+ "The field has been deleted": "Il campo è stato eliminato",
+ "When submitting the following fields, the saved values are": "Quando si inviano i seguenti campi, i valori salvati sono",
+ "After successful submission": "Dopo un invio riuscito",
+ "Then": "Poi",
+ "Stay on current page": "Resta sulla pagina corrente",
+ "Redirect to": "Reindirizza a",
+ "Save action": "Salva azione",
+ "Exists": "Esiste",
+ "Add condition": "Aggiungi condizione",
+ "Add condition group": "Aggiungi gruppo di condizioni",
+ "exists": "esiste",
+ "not exists": "non esiste",
+ "Style": "Stile",
+ "=": "=",
+ "≠": "≠",
+ ">": ">",
+ "≥": "≥",
+ "<": "<",
+ "≤": "≤",
+ "Role UID": "Ruolo UID",
+ "Precision": "Precisione",
+ "Expression": "Espressione",
+ "Rich Text": "Testo ricco",
+ "Junction collection": "Raccolta giunzione",
+ "Leave it blank, unless you need a custom intermediate table": "Lascialo vuoto, a meno che tu non abbia bisogno di una tabella intermedia personalizzata",
+ "Fields": "Campi",
+ "Edit field title": "Modifica titolo campo",
+ "Field title": "Titolo campo",
+ "Original field title: ": "Titolo campo originale:",
+ "Edit tooltip": "Modifica suggerimento",
+ "Delete field": "Elimina campo",
+ "Select collection": "Seleziona raccolta",
+ "Blank block": "Blocco vuoto",
+ "Duplicate template": "Modello duplicato",
+ "Reference template": "Modello di riferimento",
+ "Inherited template": "Modello ereditato",
+ "Create calendar block": "Crea blocco calendario",
+ "Create kanban block": "Crea blocco kanban",
+ "Grouping field": "Campo di raggruppamento",
+ "Single select and radio fields can be used as the grouping field": "I campi di selezione singoli e radio possono essere utilizzati come campo di raggruppamento",
+ "Tab name": "Nome della scheda",
+ "Current record blocks": "Blocchi record attuale",
+ "Popup message": "Messaggio popup",
+ "Delete role": "Elimina ruolo",
+ "Role display name": "Nome visualizzato ruolo",
+ "Default role": "Ruolo predefinito",
+ "All collections use general action permissions by default; permission configured individually will override the default one.": "Tutte le raccolte utilizzano i permessi di operazioni generali per impostazione predefinita; I permessi configurati individualmente sovrascriveranno quelli predefiniti.",
+ "Allows configuration of the whole system, including UI, collections, permissions, etc.": "Consente la configurazione dell'intero sistema, tra cui interfaccia utente, raccolte, permessi, ecc.",
+ "New menu items are allowed to be accessed by default.": "È possibile accedere a nuove voci di menu per impostazione predefinita.",
+ "Global permissions": "Permessi globali",
+ "General permissions": "Permessi generali",
+ "Global action permissions": "Permessi operazioni globali",
+ "General action permissions": "Permessi operazioni generali",
+ "Plugin settings permissions": "Permessi impostazioni plugin",
+ "Allow to desgin pages": "Consenti progettazione pagine",
+ "Allow to manage plugins": "Consenti gestione plugin",
+ "Allow to configure plugins": "Consenti configurazione plugin",
+ "Allows to configure interface": "Consente di configurare l'interfaccia",
+ "Allows to install, activate, disable plugins": "Consente di installare, attivare, disabilitare i plugin",
+ "Allows to configure plugins": "Consente di configurare i plugin",
+ "Action display name": "Nome visualizzato azione",
+ "Allow": "Permetti",
+ "Data scope": "Ambito dei dati",
+ "Action on new records": "Azione su nuovi record",
+ "Action on existing records": "Azione su record esistenti",
+ "All records": "Tutti i record",
+ "Own records": "Record propri",
+ "Permission policy": "Policy di autorizzazione",
+ "Individual": "Individuale",
+ "General": "Generale",
+ "Accessible": "Accessibile",
+ "Configure permission": "Configura permesso",
+ "Action permission": "Permesso azione",
+ "Field permission": "Permesso campo",
+ "Scope name": "Nome ambito",
+ "Unsaved changes": "Modifiche non salvate",
+ "Are you sure you don't want to save?": "Sei sicuro di non voler salvare?",
+ "Dragging": "Trascina",
+ "Popup": "Popup",
+ "Trigger workflow": "Trigger workflow",
+ "Request API": "Richiesta API",
+ "Assign field values": "Assegna valori del campo",
+ "Constant value": "Valore costante",
+ "Dynamic value": "Valore dinamico",
+ "Current user": "Utente attuale",
+ "Current role": "Ruolo attuale",
+ "Current record": "Record attuale",
+ "Current collection": "Raccolta attuale",
+ "Other collections": "Altre raccolte",
+ "Current popup record": "Record popup attuale",
+ "Parent popup record": "Record popup padre",
+ "Associated records": "Record associati",
+ "Parent record": "Record padre",
+ "Current time": "Ora attuale",
+ "System variables": "Variabili di sistema",
+ "Date variables": "Variabili della data",
+ "Message popup close method": "Metodo di chiusura popup di messaggio",
+ "Automatic close": "Chiudi automaticamente",
+ "Manually close": "Chiudi manualmente",
+ "After successful update": "Dopo un aggiornamento riuscito",
+ "Save record": "Salva record",
+ "Updated successfully": "Aggiornamento riuscito",
+ "After successful save": "Dopo un salvataggio riuscito",
+ "After clicking the custom button, the following field values will be assigned according to the following form.": "Dopo aver fatto clic sul pulsante personalizza, i seguenti valori verranno assegnati in base al seguente modulo.",
+ "After clicking the custom button, the following fields of the current record will be saved according to the following form.": "Dopo aver fatto clic sul pulsante personalizza, i seguenti campi del record corrente verranno salvati in base al seguente modulo.",
+ "Button background color": "Colore sfondo del pulsante",
+ "Highlight": "Evidenziato",
+ "Danger red": "Rosso pericolo",
+ "Custom request": "Richiesta personalizzata",
+ "Request settings": "Impostazioni richiesta",
+ "Request URL": "URL richiesta",
+ "Request method": "Metodo richiesta",
+ "Request query parameters": "Parametri richiesta query",
+ "Request headers": "Intestazioni richiesta",
+ "Request body": "Corpo richiesta",
+ "Request success": "Successo richiesta",
+ "Invalid JSON format": "Formato JSON non valido",
+ "After successful request": "Dopo una richiesta riuscita",
+ "Add exportable field": "Aggiungi campo esportabile",
+ "Audit logs": "Registri audit",
+ "Record ID": "ID record",
+ "User": "Utente",
+ "Field": "Campo",
+ "Select": "Seleziona",
+ "Select field": "Seleziona campo",
+ "Field value changes": "Modifiche valore del campo",
+ "One to one (has one)": "Uno a uno (ne ha uno)",
+ "One to one (belongs to)": "Uno a uno (appartiene a)",
+ "Use the same time zone (GMT) for all users": "Usa lo stesso fuso orario (GMT) per tutti gli utenti",
+ "Province/city/area name": "Nome provincia/città/area",
+ "Enabled languages": "Lingue abilitate",
+ "View all plugins": "Visualizza tutti i plugin",
+ "Print": "Stampa",
+ "Done": "Fatto",
+ "Sign up successfully, and automatically jump to the sign in page": "Iscriviti correttamente e reindirizza automaticamente alla pagina di accesso",
+ "File manager": "File Manager",
+ "ACL": "ACL",
+ "Collection manager": "Responsabile della raccolta",
+ "Plugin manager": "Plugin Manager",
+ "Local": "Locale",
+ "Built-in": "Incorporato",
+ "Marketplace": "Marketplace",
+ "Add plugin": "Aggiungi plugin",
+ "Plugin source": "Sorgente plugin",
+ "Upgrade": "Aggiornamento",
+ "Plugin dependencies check failed": "Controllo delle dipendenze del plugin non riuscito",
+ "More details": "Maggiori dettagli",
+ "Upload new version": "Carica nuova versione",
+ "Version": "Versione",
+ "Npm package": "Pacchetto Npm",
+ "Npm package name": "Nome pacchetto Npm",
+ "Upload plugin": "Carica plugin",
+ "Official plugin": "Plugin ufficiale",
+ "Add type": "Aggiungi tipo",
+ "Changelog": "Changelog",
+ "Dependencies check": "Controllo delle dipendenze",
+ "Update plugin": "Aggiorna plugin",
+ "Installing": "Installazione",
+ "The deletion was successful.": "Cancellazione riuscita.",
+ "Plugin Zip File": "File zip plugin",
+ "Compressed file url": "URL file compresso",
+ "Last updated": "Ultimo aggiornamento",
+ "PackageName": "Nome pacchetto",
+ "DisplayName": "Nome da visualizzare",
+ "Readme": "Readme",
+ "Dependencies compatibility check": "Controllo compatibilità delle dipendenze",
+ "Plugin dependencies check failed, you should change the dependent version to meet the version requirements.": "Controllo delle dipendenze del plugin non riuscito, è necessario modificare la versione dipendente per soddisfare i requisiti della versione.",
+ "Version range": "Range versione",
+ "Plugin's version": "Versione plugin",
+ "Result": "Risultato",
+ "No CHANGELOG.md file": "Nessun file Changelog.md",
+ "No README.md file": "Nessun file readme.md",
+ "Homepage": "Homepage",
+ "Drag and drop the file here or click to upload, file size should not exceed 30M": "Trascina e rilascia il file qui o fai clic per caricare, la dimensione del file non deve superare i 30M",
+ "Dependencies check failed, can't enable.": "Il controllo delle dipendenze non è riuscito, impossibile abilitare.",
+ "Plugin starting...": "Avvio plugin...",
+ "Plugin stopping...": "Interruzione plugin ...",
+ "Are you sure to delete this plugin?": "Sei sicuro di eliminare questo plugin?",
+ "Are you sure to disable this plugin?": "Sei sicuro di disabilitare questo plugin?",
+ "re-download file": "ri-scarica file",
+ "Not enabled": "Non abilitato",
+ "Search plugin": "Ricerca plugin",
+ "Author": "Autore",
+ "Plugin loading failed. Please check the server logs.": "Il caricamento del plugin non è riuscito. Si prega di controllare i registri del server.",
+ "Coming soon...": "Prossimamente...",
+ "All plugin settings": "Tutte le impostazioni del plugin",
+ "Bookmark": "Segnalibro",
+ "Manage all settings": "Gestisci tutte le impostazioni",
+ "Create inverse field in the target collection": "Crea campo inverso nella raccolta di destinazione",
+ "Inverse field name": "Nome campo inverso",
+ "Inverse field display name": "Nome visualizzato campo inverso",
+ "Bulk update": "Aggiornamento di massa",
+ "After successful bulk update": "Dopo un aggiornamento di massa riuscito",
+ "Bulk edit": "Modifica di massa",
+ "Data will be updated": "I dati verranno aggiornati",
+ "Selected": "Selezionato",
+ "All": "Tutto",
+ "Update selected data?": "Aggiornare i dati selezionati?",
+ "Update all data?": "Aggiornare tutti i dati?",
+ "Remains the same": "Rimane lo stesso",
+ "Changed to": "Cambiato in",
+ "Clear": "Cancella",
+ "Add attach": "Aggiungi allegato",
+ "Please select the records to be updated": "Si prega di selezionare i record da aggiornare",
+ "Selector": "Selettore",
+ "Inner": "Interno",
+ "Search and select collection": "Cerca e seleziona la raccolta",
+ "Please fill in the iframe URL": "Si prega di compilare l'URL iFrame",
+ "Fix block": "Fissa blocco",
+ "Plugin name": "Nome plugin",
+ "Plugin tab name": "Nome scheda plugin",
+ "Column width": "Larghezza colonna",
+ "Sortable": "Ordinabile",
+ "Enable link": "Abilita link",
+ "This is likely a NocoBase internals bug. Please open an issue at <1>here1>": "Questo sembra un bug interno di NocoBase. Si prega di aprire un ticket <1>qui1>",
+ "Render Failed": "Rendering non riuscito",
+ "App error": "Errore app",
+ "Feedback": "Feedback",
+ "Try again": "Riprova",
+ "Download logs": "Download registri",
+ "Data template": "Modello dati",
+ "Duplicate": "Duplica",
+ "Duplicating": "Duplicazione",
+ "Duplicate mode": "Modalità duplicazione",
+ "Quick duplicate": "Duplicazione veloce",
+ "Duplicate and continue": "Duplica e continua",
+ "Please configure the duplicate fields": "Si prega di configurare i campi duplicati",
+ "Add": "Aggiungi",
+ "Add new mode": "Modalità aggiungi nuovo",
+ "Quick add": "Aggiunta rapida",
+ "Modal add": "Aggiunta modale",
+ "Save mode": "Modalità salvataggio",
+ "First or create": "Prima o crea",
+ "Update or create": "Aggiorna o crea",
+ "Find by the following fields": "Trova dai seguenti campi",
+ "Create": "Crea",
+ "Current form": "Modulo corrente",
+ "Current object": "Oggetto corrente",
+ "Linkage with form fields": "Collegamento con i campi del modulo",
+ "Allow add new, update and delete actions": "Consenti azioni aggiungi nuovo, aggiorna ed elimina",
+ "Date display format": "Formato di visualizzazione della data",
+ "Table selected records": "Tabella record selezionati",
+ "Tag": "Etichetta",
+ "Tag color field": "Campo colore etichetta",
+ "Sync successfully": "Sincronizzazione riuscita",
+ "Sync from form fields": "Sincronizzazione dai campi del modulo",
+ "Select all": "Seleziona tutto",
+ "Restart": "Ricomincia",
+ "Restart application": "Riavvia applicazione",
+ "Cascade Select": "Seleziona in cascata",
+ "Execute": "Esegui",
+ "Please use a valid SELECT or WITH AS statement": "Si prega di utilizzare un' istruzione SELECT o WITH AS valida",
+ "Please confirm the SQL statement first": "Si prega di confermare prima l'istruzione SQL",
+ "Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects": "Elimina automaticamente gli oggetti che dipendono dalla raccolta (come le viste) e, a loro volta, tutti gli oggetti che dipendono da tali oggetti",
+ "Sign in with another account": "Accedi con un altro account",
+ "Return to the main application": "Torna alla applicazione principale",
+ "loading": "caricamento",
+ "name is required": "nome richiesto",
+ "data source": "sorgente dati",
+ "Data source": "Sorgente dati",
+ "DataSource": "Sorgente Dati",
+ "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "Il {{type}} \"{{name}}\" potrebbe essere stato eliminato. Si prega di rimuovere {{blockType}}.",
+ "Preset fields": "Campi preimpostati",
+ "Home page": "Home page",
+ "Handbook": "Manuale",
+ "License": "Licenza",
+ "Generic properties": "Proprietà generiche",
+ "Specific properties": "Proprietà specifiche",
+ "Used for drag and drop sorting scenarios, supporting grouping sorting": "Utilizzato per scenari con drag and drop, supporta ordinamento raggruppato",
+ "Grouped sorting": "Ordinamento raggruppato",
+ "When a field is selected for grouping, it will be grouped first before sorting.": "Quando viene selezionato un campo per il raggruppamento, verrà raggruppato prima dell'ordinamento.",
+ "Departments": "Dipartimenti",
+ "Main department": "Dipartimento principale",
+ "Department name": "Nome del dipartimento",
+ "Superior department": "Dipartimento superiore",
+ "Owners": "Proprietari",
+ "Plugin settings": "Impostazioni plugin",
+ "Menu": "Menu",
+ "Drag and drop sorting field": "Campi ordinamento drag and drop",
+ "This variable has been deprecated and can be replaced with \"Current form\"": "Questa variabile è stata deprecata e può essere sostituita con \"Current form\"",
+ "The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "Il valore di questa variabile deriva dalla stringa di ricerca nell'URL della pagina. Questa variabile può essere utilizzata normalmente solo quando la pagina ha una stringa di ricerca.",
+ "URL search params": "Parametri di ricerca URL",
+ "Expand All": "Espandi tutto",
+ "Search": "Ricerca",
+ "Clear default value": "Cancella valore predefinito",
+ "Open in new window": "Apri in una nuova finestra",
+ "Sorry, the page you visited does not exist.": "Spiacente, la pagina che hai visitato non esiste.",
+ "is none of": "non è nessuno di",
+ "is any of": "è uno di",
+ "Plugin dependency version mismatch": "Mancata corrispondenza della versione della dipendenza del 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?": "L'attuale versione della dipendenza del plugin non corrisponde alla versione dell'applicazione e potrebbe non funzionare correttamente. Sei sicuro di voler continuare a abilitare il plugin?",
+ "Allow multiple selection": "Consenti selezione multipla",
+ "Parent object": "Oggetto padre",
+ "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": "Ometti calcolo del numero totale di record della tabella durante l'impaginazione per accelerare il caricamento. Si consiglia di abilitare questa opzione per tabelle con grandi quantità di dati",
+ "Enable secondary confirmation": "Abilita conferma secondaria",
+ "Notification": "Notifica",
+ "Ellipsis overflow content": "Contenuto Ellipsis overflow",
+ "Hide column": "Nascondi colonna",
+ "In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "In modalità di configurazione, l'intera colonna diventa trasparente. In modalità non di configurazione, l'intera colonna verrà nascosta. Anche se l'intera colonna è nascosta, i suoi valori predefiniti configurati e le altre impostazioni avranno comunque effetto.",
+ "Page number": "Numero di pagina",
+ "Page size": "Numero di voci per pagina",
+ "Enable": "Abilita",
+ "Disable": "Disabilita",
+ "Tab": "Scheda",
+ "Calculation engine": "Motore di calcolo",
+ "Expression collection": "Raccolta espressioni",
+ "Tree collection": "Raccolta struttura ad albero",
+ "Parent ID": "ID record padre",
+ "Parent": "Record padre",
+ "Children": "Record figlio",
+ "Confirm": "Conferma",
+ "Block": "Blocco",
+ "Unnamed": "Senza nome",
+ "SQL collection": "Raccolta dati SQL",
+ "Configure field": "Configura campo",
+ "Username": "Nome utente",
+ "Null": "Null",
+ "Boolean": "Booleano",
+ "String": "Stringa",
+ "Syntax references": "Riferimenti sintassi",
+ "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 include un ampio set di funzioni e costanti integrate e offre una soluzione integrata per lavorare con diversi tipi di dati.",
+ "Formula.js supports most Microsoft Excel formula functions.": "Formula.js supporta la maggior parte delle funzioni delle formule di Microsoft Excel.",
+ "String template": "Modello stringa",
+ "Simple string replacement, can be used to interpolate variables in a string.": "Sostituzione semplice di stringhe, può essere utilizzata per interpolare variabili in una stringa.",
+ "https://docs.nocobase.com/handbook/calculation-engines/formula": "https://docs-cn.nocobase.com/handbook/calculation-engines/formula",
+ "https://docs.nocobase.com/handbook/calculation-engines/mathjs": "https://docs-cn.nocobase.com/handbook/calculation-engines/mathjs",
+ "Display when unchecked": "Visualizza quando deselezionato",
+ "Allow dissociate": "Consenti dissociazione",
+ "Edit block title & description": "Modifica titolo e descrizione blocco",
+ "Add Markdown": "Aggiungi Markdown",
+ "Must be 1-50 characters in length (excluding @.<>\"'/)": "Deve essere lungo 1-50 caratteri (esclusi @.<>\"'/)",
+ "Data source permissions": "Permessi origine dati",
+ "Now": "Adesso",
+ "Access control": "Controllo accessi",
+ "Remove": "Rimuovi",
+ "Docs": "Documenti",
+ "Enable page header": "Abilita intestazione pagina",
+ "Display page title": "Visualizza titolo pagina",
+ "Edit page title": "Modifica titolo pagina",
+ "Enable page tabs": "Abilita schede pagina",
+ "Constant": "Costante",
+ "Select a variable": "Seleziona una variabile",
+ "Double click to choose entire object": "Doppio clic per scegliere l'intero oggetto",
+ "True": "Vero",
+ "False": "Falso",
+ "Prettify": "Formatta",
+ "Theme": "Tema",
+ "Default theme": "Tema predefinito",
+ "Compact theme": "Tema compatto",
+ "Download": "Scarica",
+ "File type is not supported for previewing, please download it to preview.": "Il tipo di file non è supportato per l'anteprima, scaricalo per visualizzarlo.",
+ "Click or drag file to this area to upload": "Clicca o trascina il file in quest'area per caricarlo",
+ "Support for a single or bulk upload.": "Supporta il caricamento singolo o in blocco.",
+ "File size should not exceed {{size}}.": "La dimensione del file non deve superare {{size}}.",
+ "File size exceeds the limit": "La dimensione del file supera il limite",
+ "File type is not allowed": "Il tipo di file non è consentito",
+ "Incomplete uploading files need to be resolved": "I caricamenti incompleti dei file devono essere completati",
+ "Default title for each record": "Titolo predefinito per ogni record",
+ "If collection inherits, choose inherited collections as templates": "Se la raccolta eredita, scegli le raccolte ereditate come modelli",
+ "Select an existing piece of data as the initialization data for the form": "Seleziona un dato esistente come dati di inizializzazione per il modulo",
+ "Only the selected fields will be used as the initialization data for the form": "Solo i campi selezionati verranno utilizzati come dati di inizializzazione per il modulo",
+ "Template Data": "Dati modello",
+ "Data fields": "Campi dati",
+ "Add template": "Aggiungi modello",
+ "Enable form data template": "Abilita modello dati modulo",
+ "Form data templates": "Modelli dati modulo",
+ "No configuration available.": "Nessuna configurazione disponibile.",
+ "Reload application": "Ricarica applicazione",
+ "The application is reloading, please do not close the page.": "L'applicazione si sta ricaricando, non chiudere la pagina.",
+ "Application reloading": "Ricaricamento applicazione",
+ "Allows to clear cache, reboot application": "Consente di cancellare la cache, riavviare l'applicazione",
+ "The will interrupt service, it may take a few seconds to restart. Are you sure to continue?": "Il riavvio interromperà il servizio, potrebbero essere necessari alcuni secondi. Sei sicuro di continuare?",
+ "Clear cache": "Cancella cache",
+ "Quick create": "Creazione rapida",
+ "Dropdown": "Menu a discesa",
+ "Pop-up": "Popup",
+ "Direct duplicate": "Duplicazione diretta",
+ "Copy into the form and continue to fill in": "Copia nel modulo e continua a compilare",
+ "Failed to load plugin": "Caricamento plugin fallito",
+ "Date range limit": "Limite intervallo date",
+ "MinDate": "Data minima",
+ "MaxDate": "Data massima",
+ "Please select time or variable": "Seleziona ora o variabile",
+ "Filter out a single piece or a group of records as a template": "Filtra un singolo dato o un gruppo di record come modello",
+ "The title field is used to identify the template record": "Il campo titolo è utilizzato per identificare il record modello",
+ "Template fields": "Campi modello",
+ "The selected fields will automatically populate the form": "I campi selezionati popoleranno automaticamente il modulo",
+ "UnSelect all": "Deseleziona tutto",
+ "Secondary confirmation": "Conferma secondaria",
+ "Perform the {{title}}": "Esegui {{title}}",
+ "Are you sure you want to perform the {{title}} action?": "Sei sicuro di voler eseguire l'azione {{title}}?",
+ "Permission denied": "Permesso negato",
+ "Allow add new": "Consenti aggiunta",
+ "Data model": "Modello dati",
+ "Security": "Sicurezza",
+ "Action": "Azione",
+ "System": "Sistema",
+ "Other": "Altro",
+ "Allow selection of existing records": "Consenti selezione di record esistenti",
+ "Data Model": "Modello dati",
+ "Blocks": "Blocchi",
+ "Users & permissions": "Utenti e permessi",
+ "System management": "Gestione sistema",
+ "System & security": "Sistema e sicurezza",
+ "Workflow": "Workflow",
+ "Third party services": "Servizi di terze parti",
+ "Data model tools": "Strumenti modello dati",
+ "Data sources": "Origini dati",
+ "Collections": "Raccolte",
+ "Collection fields": "Campi raccolta",
+ "Authentication": "Autenticazione",
+ "Logging and monitoring": "Registrazione e monitoraggio",
+ "Main": "Principale",
+ "Index": "Indice",
+ "Field values must be unique.": "I valori dei campi devono essere univoci.",
+ "Alphabet": "Alfabeto",
+ "Accuracy": "Precisione",
+ "Millisecond": "Millisecondo",
+ "Second": "Secondo",
+ "Unix Timestamp": "Timestamp Unix",
+ "Field value do not meet the requirements": "Il valore del campo non soddisfa i requisiti",
+ "Field value size is": "La dimensione del valore del campo è",
+ "Unit conversion": "Conversione unità",
+ "Separator": "Separatore",
+ "Prefix": "Prefisso",
+ "Suffix": "Suffisso",
+ "Record unique key": "Chiave univoca record",
+ "Filter target key": "Chiave target filtro",
+ "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.": "Se una raccolta non ha una chiave primaria, devi configurare un record come chiave univoca per individuare i record di ogni riga all'interno di un blocco, la mancata configurazione impedirà la creazione di blocchi di dati per la raccolta.",
+ "Filter data based on the specific field, with the requirement that the field value must be unique.": "Filtra i dati in base a un campo specifico, con il requisito che il valore del campo deve essere univoco.",
+ "Multiply by": "Moltiplica per",
+ "Divide by": "Dividi per",
+ "Scientifix notation": "Notazione scientifica",
+ "Normal": "Normale",
+ "Automatically generate default values": "Genera automaticamente valori predefiniti",
+ "Refresh data on close": "Refresh dei dati alla chiusura",
+ "Refresh data on action": "Refresh dei dati su azione",
+ "Unknown field type": "Tipo di campo sconosciuto",
+ "The following field types are not compatible and do not support output and display": "I seguenti tipi di campo non sono compatibili e non supportano l'output e la visualizzazione",
+ "Not fixed": "Non fissato",
+ "Left fixed": "Fissato a sinistra",
+ "Right fixed": "Fissato a destra",
+ "Fixed": "Colonna fissa",
+ "Set block height": "Imposta altezza del blocco",
+ "Specify height": "Specifica altezza",
+ "Full height": "Altezza completa",
+ "Please configure the URL": "Configura l'URL",
+ "URL": "URL",
+ "Search parameters": "Parametri di ricerca URL",
+ "Do not concatenate search params in the URL": "Non concatenare i parametri di ricerca nell'URL",
+ "Edit link": "Modifica link",
+ "Add parameter": "Aggiungi parametro",
+ "Use simple pagination mode": "Usa modalità di paginazione semplice",
+ "Set Template Engine": "Imposta motore template",
+ "Template engine": "Motore template",
+ "The current user only has the UI configuration permission, but don't have view permission for collection \"{{name}}\"": "L'utente corrente ha solo il permesso di configurazione dell'interfaccia utente, ma non ha il permesso di visualizzazione per la raccolta \"{{name}}\"",
+ "Default value to current time": "Imposta il valore predefinito del campo all'ora corrente",
+ "Automatically update timestamp on update": "Aggiorna automaticamente il timestamp all'aggiornamento",
+ "Default value to current server time": "Imposta il valore predefinito del campo all'ora corrente del server",
+ "Automatically update timestamp to the current server time on update": "Aggiorna automaticamente il timestamp all'ora corrente del server all'aggiornamento",
+ "Datetime (with time zone)": "Data e ora (con fuso orario)",
+ "Datetime (without time zone)": "Data e ora (senza fuso orario)",
+ "DateOnly": "Solo data",
+ "Content": "Contenuto",
+ "Perform the Update record": "Esegui aggiornamento record",
+ "Are you sure you want to perform the Update record action?": "Sei sicuro di voler eseguire l'azione di aggiornamento record?",
+ "Perform the Custom request": "Esegui richiesta personalizzata",
+ "Are you sure you want to perform the Custom request action": "Sei sicuro di voler eseguire l'azione di richiesta personalizzata?",
+ "Perform the Refresh": "Esegui refresh",
+ "Are you sure you want to perform the Refresh action?": "Sei sicuro di voler eseguire l'azione di refresh?",
+ "Perform the Submit": "Esegui l'invio",
+ "Are you sure you want to perform the Submit action?": "Sei sicuro di voler eseguire l'azione di invio?",
+ "Perform the Trigger workflow": "Esegui il trigger del workflow",
+ "Are you sure you want to perform the Trigger workflow action?": "Sei sicuro di voler eseguire l'azione di trigger del workflow?",
+ "Picker": "Selettore",
+ "Quarter": "Trimestre",
+ "Switching the picker, the value and default value will be cleared": "Cambiando il selettore, il valore e il valore predefinito verranno cancellati",
+ "Stay on the current popup or page": "Rimani nel popup o nella pagina corrente",
+ "Return to the previous popup or page": "Torna al popup o alla pagina precedente",
+ "Action after successful submission": "Azione dopo l'invio riuscito",
+ "Allow disassociation": "Consenti la dissociazione",
+ "Layout": "Layout",
+ "Vertical": "Verticale",
+ "Horizontal": "Orizzontale",
+ "Edit group title": "Modifica titolo del gruppo",
+ "Title position": "Posizione titolo",
+ "Dashed": "Tratteggiato",
+ "Left": "Sinistra",
+ "Center": "Centro",
+ "Right": "Destra",
+ "Divider line color": "Colore linea di divisione",
+ "Label align": "Allineamento etichetta",
+ "Label width": "Larghezza etichetta",
+ "When the Label exceeds the width": "Quando l'etichetta supera la larghezza",
+ "Line break": "A capo",
+ "Ellipsis": "Ellipsis",
+ "Set block layout": "Imposta layout del blocco",
+ "Add & Update": "Aggiungi e aggiorna",
+ "Table size": "Dimensione della tabella",
+ "Plugin": "Plugin",
+ "Bulk enable": "Abilitazione in blocco",
+ "Search plugin...": "Cerca plugin...",
+ "Package name": "Nome pacchetto",
+ "Associate": "Associa",
+ "Please add or select record": "Aggiungi o seleziona record",
+ "No data": "Nessun dato",
+ "Fields can only be used correctly if they are defined with an interface.": "I campi possono essere utilizzati correttamente solo se sono definiti con un'interfaccia.",
+ "Unauthenticated. Please sign in to continue.": "Non autenticato. Accedi per continuare.",
+ "User not found. Please sign in again to continue.": "Impossibile trovare l'utente. Accedi nuovamente per continuare.",
+ "Your session has expired. Please sign in again.": "La tua sessione è scaduta. Accedi nuovamente.",
+ "User password changed, please signin again.": "La password dell'utente è stata modificata, accedi di nuovo.",
+ "Show file name": "Mostra nome del file",
+ "Outlined": "Contornato",
+ "Filled": "Riempito",
+ "Two tone": "Due toni",
+ "Desktop routes": "Percorsi desktop",
+ "Route permissions": "Permessi percorso",
+ "New routes are allowed to be accessed by default": "I nuovi percorsi sono accessibili per impostazione predefinita",
+ "Route name": "Nome percorso",
+ "Mobile routes": "Percorsi mobile",
+ "Show in menu": "Mostra nel menu",
+ "Hide in menu": "Nascondi nel menu",
+ "Path": "Percorso",
+ "Type": "Tipo",
+ "Access": "Accesso",
+ "Routes": "Percorsi",
+ "Add child route": "Aggiungi percorso figlio",
+ "Delete routes": "Elimina percorsi",
+ "Delete route": "Elimina percorso",
+ "Are you sure you want to hide these routes in menu?": "Sei sicuro di voler nascondere questi percorsi nel menu?",
+ "Are you sure you want to show these routes in menu?": "Sei sicuro di voler mostrare questi percorsi nel menu?",
+ "Are you sure you want to hide this menu?": "Sei sicuro di voler nascondere questo menu?",
+ "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Dopo averlo nascosto, questo menu non apparirà più nella barra dei menu. Per mostrarlo di nuovo, devi andare alla pagina di gestione dei percorsi per configurarlo.",
+ "If selected, the page will display Tab pages.": "Se selezionato, la pagina visualizzerà le pagine schede.",
+ "If selected, the route will be displayed in the menu.": "Se selezionato, il percorso verrà visualizzato nel menu.",
+ "Are you sure you want to hide this tab?": "Sei sicuro di voler nascondere questa scheda?",
+ "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Dopo averla nascosta, questa scheda non apparirà più nella barra delle schede. Per mostrarla di nuovo, devi andare alla pagina di gestione dei percorsi per configurarlo.",
+ "No pages yet, please configure first": "Nessuna pagina ancora, si prega di configurare prima",
+ "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur",
+ "Refresh data blocks": "Aggiorna blocchi di dati",
+ "Select data blocks to refresh": "Aggiorna blocchi di dati",
+ "After successful submission, the selected data blocks will be automatically refreshed.": "Dopo una soumission réussie, les blocs de données sélectionnés seront automatiquement actualisés."
+}
diff --git a/packages/core/client/src/locale/ja-JP.json b/packages/core/client/src/locale/ja-JP.json
index 7013d30af2..2d190cd566 100644
--- a/packages/core/client/src/locale/ja-JP.json
+++ b/packages/core/client/src/locale/ja-JP.json
@@ -145,6 +145,7 @@
"Chart type": "チャートタイプ",
"Chart config": "チャート設定",
"Templates": "テンプレート",
+ "Template": "テンプレート",
"Select template": "テンプレートを選択してください",
"Action logs": "操作履歴",
"Create template": "テンプレートを作成",
@@ -396,7 +397,8 @@
"Turn pages": "ページをめくる",
"Others": "その他",
"Other records": "他のレコード",
- "Save as template": "テンプレートとして保存",
+ "Save as reference template": "参照テンプレートとして保存",
+ "Save as inherited template": "継承テンプレートとして保存",
"Save as block template": "ブロックテンプレートとして保存",
"Block templates": "ブロックテンプレート",
"Block template": "ブロックテンプレート",
@@ -471,6 +473,7 @@
"Blank block": "空のブロック",
"Duplicate template": "テンプレートをコピー",
"Reference template": "テンプレートを参照",
+ "Inherited template": "継承テンプレート",
"Create calendar block": "カレンダーブロックの作成",
"Create kanban block": "かんばんブロックの作成",
"Grouping field": "グループフィールド",
@@ -1039,5 +1042,12 @@
"If selected, the page will display Tab pages.": "選択されている場合、ページはタブページを表示します。",
"If selected, the route will be displayed in the menu.": "選択されている場合、ルートはメニューに表示されます。",
"Are you sure you want to hide this tab?": "このタブを非表示にしますか?",
- "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "非表示にすると、このタブはタブバーに表示されなくなります。再表示するには、ルート管理ページで設定する必要があります。"
+ "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "非表示にすると、このタブはタブバーに表示されなくなります。再表示するには、ルート管理ページで設定する必要があります。",
+ "No pages yet, please configure first": "まだページがありません。最初に設定してください",
+ "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "ユーザーインターフェースエディターモードに入るには、右上隅の「UIエディタ」アイコンをクリックしてください",
+ "Deprecated": "非推奨",
+ "Full permissions": "すべての権限",
+ "Refresh data blocks": "データブロックを更新",
+ "Select data blocks to refresh": "データブロックを選択して更新",
+ "After successful submission, the selected data blocks will be automatically refreshed.": "送信後、選択したデータブロックが自動的に更新されます。"
}
diff --git a/packages/core/client/src/locale/ko-KR.json b/packages/core/client/src/locale/ko-KR.json
index 9e17f29876..1866fc8486 100644
--- a/packages/core/client/src/locale/ko-KR.json
+++ b/packages/core/client/src/locale/ko-KR.json
@@ -183,6 +183,7 @@
"Chart type": "차트 유형",
"Chart config": "차트 구성",
"Templates": "템플릿",
+ "Template": "템플릿",
"Select template": "템플릿 선택",
"Action logs": "작업 로그",
"Create template": "템플릿 생성",
@@ -516,7 +517,8 @@
"Turn pages": "페이지 넘김",
"Others": "기타",
"Other records": "기타 레코드",
- "Save as template": "템플릿으로 저장",
+ "Save as reference template": "참조 템플릿으로 저장",
+ "Save as inherited template": "상속 템플릿으로 저장",
"Save as block template": "블록 템플릿으로 저장",
"Block templates": "블록 템플릿",
"Block template": "블록 템플릿",
@@ -600,6 +602,7 @@
"Blank block": "빈 블록",
"Duplicate template": "템플릿 복제",
"Reference template": "참조 템플릿",
+ "Inherited template": "상속 템플릿",
"Create calendar block": "캘린더 블록 생성",
"Create kanban block": "칸반 블록 생성",
"Grouping field": "그루핑 필드",
@@ -912,5 +915,12 @@
"If selected, the page will display Tab pages.": "선택되면 페이지는 탭 페이지를 표시합니다.",
"If selected, the route will be displayed in the menu.": "선택되면 라우트는 메뉴에 표시됩니다.",
"Are you sure you want to hide this tab?": "이 탭을 숨기시겠습니까?",
- "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "숨기면 이 탭은 탭 바에 더 이상 표시되지 않습니다. 다시 표시하려면 라우트 관리 페이지에서 설정해야 합니다."
+ "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "숨기면 이 탭은 탭 바에 더 이상 표시되지 않습니다. 다시 표시하려면 라우트 관리 페이지에서 설정해야 합니다.",
+ "No pages yet, please configure first": "아직 페이지가 없습니다. 먼저 설정하십시오",
+ "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "사용자 인터페이스 편집기 모드에 들어가려면 오른쪽 상단의 \"UI 편집기\" 아이콘을 클릭하십시오",
+ "Deprecated": "사용 중단됨",
+ "Full permissions": "모든 권한",
+ "Refresh data blocks": "데이터 블록 새로 고침",
+ "Select data blocks to refresh": "데이터 블록을 선택하여 새로 고침",
+ "After successful submission, the selected data blocks will be automatically refreshed.": "전송 후, 선택한 데이터 블록이 자동으로 새로 고쳐집니다."
}
diff --git a/packages/core/client/src/locale/nl-NL.json b/packages/core/client/src/locale/nl-NL.json
new file mode 100644
index 0000000000..ab31e6f767
--- /dev/null
+++ b/packages/core/client/src/locale/nl-NL.json
@@ -0,0 +1,1063 @@
+{
+ "Display <1><0>100><1>201><2>502><3>1003>1> items per page": "Toon <1><0>100><1>201><2>502><3>1003>1> items per pagina",
+ "Page number": "Paginanummer",
+ "Page size": "Paginagrootte",
+ "Meet <1><0>All0><1>Any1>1> conditions in the group": "Voldoe aan <1><0>Alle0><1>Een1>1> voorwaarde(n) in de groep",
+ "Open in<1><0>Modal0><1>Drawer1><2>Window2>1>": "Open in<1><0>Modal0><1>Drawer1><2>Venster2>1>",
+ "{{count}} filter items": "{{count}} filter items",
+ "{{count}} more items": "{{count}} meer items",
+ "Total {{count}} items": "Totaal {{count}} items",
+ "Today": "Vandaag",
+ "Yesterday": "Gisteren",
+ "Tomorrow": "Morgen",
+ "Month": "Maand",
+ "Week": "Week",
+ "This week": "Deze week",
+ "This month": "Deze maand",
+ "This year": "Dit jaar",
+ "Next year": "Volgend jaar",
+ "Last week": "Vorige week",
+ "Next week": "Volgende week",
+ "Last month": "Vorige maand",
+ "Next month": "Volgende maand",
+ "Last quarter": "Vorig kwartaal",
+ "This quarter": "Dit kwartaal",
+ "Next quarter": "Volgend kwartaal",
+ "Last year": "Vorig jaar",
+ "Last 7 days": "Laatste 7 dagen",
+ "Last 30 days": "Laatste 30 dagen",
+ "Last 90 days": "Laatste 90 dagen",
+ "Next 7 days": "Volgende 7 dagen",
+ "Next 30 days": "Volgende 30 dagen",
+ "Next 90 days": "Volgende 90 dagen",
+ "Work week": "Werkweek",
+ "Day": "Dag",
+ "Agenda": "Agenda",
+ "Date": "Datum",
+ "Time": "Tijd",
+ "Event": "Gebeurtenis",
+ "None": "Geen",
+ "Unconnected": "Niet verbonden",
+ "System settings": "Systeeminstellingen",
+ "System title": "Systeemtitel",
+ "Setting" : "Instelling",
+ "Settings": "Instellingen",
+ "Enable": "Inschakelen",
+ "Disable": "Uitschakelen",
+ "On": "Aan",
+ "Off": "Uit",
+ "Logo": "Logo",
+ "Add menu item": "Menu-item toevoegen",
+ "Page": "Pagina",
+ "Tab": "Tab",
+ "Name": "Naam",
+ "Icon": "Icoon",
+ "Group": "Groep",
+ "Link": "Link",
+ "Save conditions": "Voorwaarden opslaan",
+ "Edit menu item": "Menu-item bewerken",
+ "Move to": "Verplaats naar",
+ "Insert left": "Links invoegen",
+ "Insert right": "Rechts invoegen",
+ "Insert inner": "Binnenkant invoegen",
+ "Delete": "Verwijder",
+ "Disassociate": "Loskoppelen",
+ "Disassociate record": "Record loskoppelen",
+ "Are you sure you want to disassociate it?": "Weet je zeker dat je het wil loskoppelen?",
+ "UI editor": "UI-editor",
+ "Collection": "Collectie",
+ "Collection selector": "Collectie selector",
+ "Providing certain collections as options for users, typically used in polymorphic or inheritance scenarios": "Bepaalde collecties aanbieden als opties voor gebruikers, meestal gebruikt in polymorfe of erfelijkheidsscenario's",
+ "Collections & Fields": "Collecties & Velden",
+ "All collections": "Alle collecties",
+ "Add category": "Categorie toevoegen",
+ "Enable child collections": "Onderliggende collecties inschakelen",
+ "Allow adding records to the current collection": "Toestaan dat records aan de huidige collectie worden toegevoegd",
+ "Delete category": "Categorie verwijderen",
+ "Edit category": "Categorie bewerken",
+ "Collection category": "Collectie categorie",
+ "Collection template": "Collectie sjabloon",
+ "Sort": "Sorteren",
+ "Categories": "Categorieën",
+ "Visible": "Zichtbaar",
+ "Read only": "Alleen-lezen",
+ "Easy reading": "Gemakkelijk te lezen",
+ "Hidden": "Verborgen",
+ "Hidden(reserved value)": "Verborgen (gereserveerde waarde)",
+ "Not required": "Niet vereist",
+ "Value": "Waarde",
+ "Disabled": "Uitgeschakeld",
+ "Enabled": "Ingeschakeld",
+ "Problematic": "Problematisch",
+ "Empty": "Leeg",
+ "Linkage rule": "Koppelingregel",
+ "Linkage rules": "Koppelingregels",
+ "Condition": "Voorwaarde",
+ "Properties": "Eigenschappen",
+ "Add linkage rule": "Koppelingregel toevoegen",
+ "Add property": "Eigenschap toevoegen",
+ "Category name": "Categorie naam",
+ "Roles & Permissions": "Rollen & Machtigingen",
+ "Edit profile": "Profiel bewerken",
+ "Change password": "Wachtwoord wijzigen",
+ "Old password": "Oud wachtwoord",
+ "New password": "Nieuw wachtwoord",
+ "Switch role": "Rol wisselen",
+ "Super admin": "Superbeheerder",
+ "Language": "Taal",
+ "Allow sign up": "Registratie toestaan",
+ "Enable SMS authentication": "SMS-authenticatie inschakelen",
+ "Sign out": "Afmelden",
+ "Cancel": "Annuleren",
+ "Submit": "Indienen",
+ "Close": "Sluiten",
+ "Set the data scope": "Stel de gegevensomvang in",
+ "Set data loading mode": "Stel de gegevenslaadmodus in",
+ "Load all data when filter is empty": "Laad alle gegevens wanneer filter leeg is",
+ "Do not load data when filter is empty": "Laad geen gegevens wanneer filter leeg is",
+ "Data loading mode": "Gegevenslaadmodus",
+ "Data blocks": "Gegevensblokken",
+ "Filter blocks": "Filterblokken",
+ "Table": "Tabel",
+ "Table OID(Inheritance)": "Tabel OID (Erfelijkheid)",
+ "Form": "Formulier",
+ "List": "Lijst",
+ "Grid Card": "Rasterkaart",
+ "pixels": "pixels",
+ "Screen size": "Schermgrootte",
+ "Display title": "Titel weergeven",
+ "Set the count of columns displayed in a row": "Stel het aantal kolommen in dat in een rij wordt weergegeven",
+ "Column": "Kolom",
+ "Phone device": "Telefoonapparaat",
+ "Tablet device": "Tabletapparaat",
+ "Desktop device": "Desktopapparaat",
+ "Large screen device": "Groot schermapparaat",
+ "Collapse": "Inklappen",
+ "Select data source": "Gegevensbron selecteren",
+ "Calendar": "Kalender",
+ "Delete events": "Gebeurtenissen verwijderen",
+ "This event": "Deze gebeurtenis",
+ "This and following events": "Deze en volgende gebeurtenissen",
+ "All events": "Alle gebeurtenissen",
+ "Delete this event?": "Deze gebeurtenis verwijderen?",
+ "Delete Event": "Verwijder gebeurtenis",
+ "Kanban": "Kanban",
+ "Gantt": "Gantt",
+ "Create gantt block": "Maak Gantt-blok",
+ "Progress field": "Voortgangsveld",
+ "Time scale": "Tijdschaal",
+ "Hour": "Uur",
+ "Quarter of day": "Kwartaal van de dag",
+ "Half of day": "Helft van de dag",
+ "Year": "Jaar",
+ "QuarterYear": "Kwartaaljaar",
+ "Select grouping field": "Selecteer groeperingsveld",
+ "Media": "Media",
+ "Markdown": "Markdown",
+ "Wysiwyg": "Wysiwyg",
+ "Chart blocks": "Grafiekblokken",
+ "Column chart": "Kolomgrafiek",
+ "Bar chart": "Staafgrafiek",
+ "Line chart": "Lijngrafiek",
+ "Pie chart": "Taartgrafiek",
+ "Area chart": "Oppervlaktegrafiek",
+ "Other chart": "Andere grafiek",
+ "Other blocks": "Andere blokken",
+ "In configuration": "In configuratie",
+ "Chart title": "Grafiektitel",
+ "Chart type": "Grafiektype",
+ "Chart config": "Grafiekconfiguratie",
+ "Templates": "Sjablonen",
+ "Select template": "Selecteer sjabloon",
+ "Action logs": "Actielogs",
+ "Create template": "Sjabloon maken",
+ "Edit markdown": "Bewerk markdown",
+ "Add block": "Blok toevoegen",
+ "Add new": "Nieuw toevoegen",
+ "Add record": "Record toevoegen",
+ "Add child": "Onderliggend toevoegen",
+ "Collapse all": "Alles inklappen",
+ "Expand all": "Alles uitbreiden",
+ "Expand/Collapse": "Uitbreiden/Inklappen",
+ "Default collapse": "Standaard inklappen",
+ "Tree table": "Boomtabel",
+ "Custom field display name": "Aangepaste veldweergavenaam",
+ "Display fields": "Toon collectie velden",
+ "Edit record": "Record bewerken",
+ "Delete menu item": "Menu-item verwijderen",
+ "Add page": "Pagina toevoegen",
+ "Add group": "Groep toevoegen",
+ "Add link": "Link toevoegen",
+ "Insert above": "Boven invoegen",
+ "Insert below": "Onder invoegen",
+ "Save": "Opslaan",
+ "Delete block": "Blok verwijderen",
+ "Are you sure you want to delete it?": "Weet je zeker dat je het wil verwijderen?",
+ "This is a demo text, **supports Markdown syntax**.": "Dit is een demo tekst, **ondersteunt Markdown syntax**.",
+ "Filter": "Filter",
+ "Connect data blocks": "Verbind gegevensblokken",
+ "Action type": "Actietype",
+ "Actions": "Acties",
+ "Insert": "Invoegen",
+ "Insert if not exists": "Invoegen als het niet bestaat",
+ "Insert if not exists, or update": "Invoegen als het niet bestaat, of bijwerken",
+ "Determine whether a record exists by the following fields": "Bepaal of een record bestaat op basis van de volgende velden",
+ "Update": "Bijwerken",
+ "Update record": "Record bijwerken",
+ "View": "Bekijken",
+ "View record": "Record bekijken",
+ "Refresh": "Vernieuwen",
+ "Data changes": "Gegevenswijzigingen",
+ "Field name": "Veldnaam",
+ "Before change": "Voor wijziging",
+ "After change": "Na wijziging",
+ "Delete record": "Record verwijderen",
+ "Delete collection": "Collectie verwijderen",
+ "Create collection": "Collectie maken",
+ "Collection display name": "Weergavenaam collectie",
+ "Collection name": "Naam collectie",
+ "Inherits": "Erft",
+ "Primary key, unique identifier, self growth": "Primaire sleutel, unieke identifier, zelfgroei",
+ "Store the creation user of each record": "Sla de gebruiker die de record aanmaakte op",
+ "Store the last update user of each record": "Sla de gebruiker van de laatste update op",
+ "Store the creation time of each record": "Sla de creatietijd van elke record op",
+ "Store the last update time of each record": "Sla de laatste update tijd van elke record op",
+ "More options": "Meer opties",
+ "Records can be sorted": "Records kunnen worden gesorteerd",
+ "Calendar collection": "Kalendercollectie",
+ "General collection": "Algemene collectie",
+ "Connect to database view": "Verbind met databaseweergave",
+ "Sync from database": "Synchroniseren vanuit database",
+ "Source collections": "Broncollecties",
+ "Field source": "Veldbron",
+ "Preview": "Voorbeeld",
+ "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Willekeurig gegenereerd en kan worden aangepast. Ondersteunt letters, cijfers en onderstrepingstekens, moet beginnen met een letter.",
+ "Edit": "Bewerken",
+ "Edit collection": "Collectie bewerken",
+ "Configure fields": "Velden configureren",
+ "Configure columns": "Kolommen configureren",
+ "Edit field": "Veld bewerken",
+ "Override": "Overschrijven",
+ "Override field": "Veld overschrijven",
+ "Configure fields of {{title}}": "Configureer velden van {{title}}",
+ "Association fields filter": "Filter voor associatievelden",
+ "PK & FK fields": "PK & FK velden",
+ "Association fields": "Associatievelden",
+ "Choices fields": "Keuzevelden",
+ "System fields": "Systeemvelden",
+ "General fields": "Algemene velden",
+ "Inherited fields": "Geërfde velden",
+ "Parent collection fields": "Velden van bovenliggende collectie",
+ "Basic": "Basis",
+ "Single line text": "Enkele regel tekst",
+ "Long text": "Lange tekst",
+ "Phone": "Telefoon",
+ "Email": "E-mail",
+ "Number": "Nummer",
+ "Integer": "Geheel getal",
+ "Percent": "Percentage",
+ "Password": "Wachtwoord",
+ "Advanced type": "Geavanceerd",
+ "Formula": "Formule",
+ "Formula description": "Bereken een waarde in elk record op basis van andere velden in hetzelfde record.",
+ "Choices": "Keuzes",
+ "Checkbox": "Selectievak",
+ "Single select": "Enkele selectie",
+ "Multiple select": "Meerdere selecteren",
+ "Radio group": "Radiogroep",
+ "Checkbox group": "Checkboxgroep",
+ "China region": "China regio",
+ "Date & Time": "Datum & Tijd",
+ "Datetime": "Datumtijd",
+ "Relation": "Relatie",
+ "Link to": "Koppel aan",
+ "Link to description": "Gebruikt om snel collectie-relaties te maken en compatibel met de meest voorkomende scenario's. Geschikt voor niet-ontwikkelaars. Wanneer aanwezig als veld, is het een keuzelijst die wordt gebruikt om records uit de doelcollectie te selecteren. Eenmaal aangemaakt, genereert het tegelijkertijd de bijbehorende velden van de huidige collectie in de doelcollectie.",
+ "Sub-table": "Subtabel",
+ "Sub-details": "Subdetails",
+ "Sub-form(Popover)": "Subformulier (Popover)",
+ "System info": "Systeeminfo",
+ "Created at": "Aangemaakt op",
+ "Last updated at": "Laatst bijgewerkt op",
+ "Created by": "Aangemaakt door",
+ "Last updated by": "Laatst bijgewerkt door",
+ "Add field": "Veld toevoegen",
+ "Field display name": "Weergavenaam veld",
+ "Field type": "Veldtype",
+ "Field interface": "Veldinterface",
+ "Date format": "Datumformaat",
+ "Year/Month/Day": "Jaar/Maand/Dag",
+ "Year-Month-Day": "Jaar-Maand-Dag",
+ "Day/Month/Year": "Dag/Maand/Jaar",
+ "Show time": "Toon tijd",
+ "Time format": "Tijdformaat",
+ "12 hour": "12 uur",
+ "24 hour": "24 uur",
+ "Relationship type": "Relatietype",
+ "Inverse relationship type": "Omgekeerd relatietype",
+ "Source collection": "Broncollectie",
+ "Source key": "Bron sleutel",
+ "Target collection": "Doelcollectie",
+ "Through collection": "Via collectie",
+ "Target key": "Doel sleutel",
+ "Foreign key": "Buitenlandse sleutel",
+ "One to one": "Een-op-een",
+ "One to many": "Een-op-veel",
+ "Many to one": "Veel-op-een",
+ "Many to many": "Veel-op-veel",
+ "Foreign key 1": "Buitenlandse sleutel 1",
+ "Foreign key 2": "Buitenlandse sleutel 2",
+ "One to one description": "Gebruikt om een-op-een relaties te maken. For example, a user has a profile.",
+ "One to many description": "Gebruikt om een een-op-veel-relatie te maken. Bijvoorbeeld, een land heeft veel steden en een stad kan slechts in één land zijn. Wanneer aanwezig als veld, is het een sub-tabel die de records van de geassocieerde collectie weergeeft. Wanneer aangemaakt, wordt automatisch een Veel-op-een veld gegenereerd in de geassocieerde collectie.",
+ "Many to one description": "Gebruikt om een veel-op-een-relatie te maken. Bijvoorbeeld, een stad kan slechts tot één land behoren en een land kan veel steden hebben. Wanneer aanwezig als veld, is het een keuzelijst die wordt gebruikt om records uit de geassocieerde collectie te selecteren. Eenmaal aangemaakt, wordt automatisch een Een-op-veel veld gegenereerd in de geassocieerde collectie.",
+ "Many to many description": "Gebruikt om veel-op-veel-relaties te maken. Bijvoorbeeld, een student heeft veel leraren en een leraar heeft veel studenten. Wanneer aanwezig als veld, is het een keuzelijst die wordt gebruikt om records uit de geassocieerde collectie te selecteren.",
+ "Generated automatically if left blank": "Wordt automatisch gegenereerd als het leeg wordt gelaten",
+ "Display association fields": "Associatievelden tonen",
+ "Display field title": "Veldtitel tonen",
+ "Field component": "Veldcomponent",
+ "Allow multiple": "Meerdere toestaan",
+ "Quick upload": "Snelle upload",
+ "Select file": "Bestand selecteren",
+ "Subtable": "Subtabel",
+ "Sub-form": "Subformulier",
+ "Field mode": "Veldmodus",
+ "Allow add new data": "Sta toe nieuwe data toe te voegen",
+ "Record picker": "Selecteer record",
+ "Toggles the subfield mode": "Wisselt de subveldmodus",
+ "Selector mode": "Selectormodus",
+ "Subtable mode": "Subtabel modus",
+ "Subform mode": "Subformulier modus",
+ "Edit block title": "Bloktitel bewerken",
+ "Block title": "Bloktitel",
+ "Pattern": "Patroon",
+ "Operator": "Operator",
+ "Editable": "Bewerkbaar",
+ "Readonly": "Alleen-lezen",
+ "Easy-reading": "Gemakkelijk te lezen",
+ "Add filter": "Filter toevoegen",
+ "Add filter group": "Filtergroep toevoegen",
+ "Comparision": "Vergelijking",
+ "is": "is",
+ "is not": "is niet",
+ "contains": "bevat",
+ "does not contain": "bevat niet",
+ "starts with": "begint met",
+ "not starts with": "begint niet met",
+ "ends with": "eindigt met",
+ "not ends with": "eindigt niet met",
+ "is empty": "is leeg",
+ "is not empty": "is niet leeg",
+ "Edit chart": "Bewerk grafiek",
+ "Add text": "Voeg tekst toe",
+ "Filterable fields": "Filterbare velden",
+ "Edit button": "Bewerk knop",
+ "Hide": "Verbergen",
+ "Enable actions": "Zet acties aan",
+ "Import": "Importeer",
+ "Export": "Exporteer",
+ "Customize": "Pas aan",
+ "Custom": "Aangepast",
+ "Function": "Functie",
+ "Popup form": "Pop-up formulier",
+ "Flexible popup": "Flexibele pop-up",
+ "Configure actions": "Configureer acties",
+ "Display order number": "Toon volgnummer",
+ "Enable drag and drop sorting": "Zet drag and drop sorteren aan",
+ "Triggered when the row is clicked": "Geactiveerd wanneer de rij wordt aangeklikt",
+ "Add tab": "Voeg tabblad toe",
+ "Disable tabs": "Schakel tabbladen uit",
+ "Details": "Details",
+ "Edit form": "Bewerk formulier",
+ "Create form": "Maak formulier",
+ "Form (Edit)": "Formulier (Bewerken)",
+ "Form (Add new)": "Formulier (Nieuw toevoegen)",
+ "Edit tab": "Bewerk tabblad",
+ "Relationship blocks": "Relatieblokken",
+ "Select record": "Selecteer record",
+ "Display name": "Weergavenaam",
+ "Select icon": "Selecteer icoon",
+ "Custom column name": "Aangepaste kolomnaam",
+ "Edit description": "Bewerk beschrijving",
+ "Required": "Verplicht",
+ "Unique": "Uniek",
+ "Primary": "Primaire",
+ "Auto increment": "Auto-increment",
+ "Label field": "Label veld",
+ "Default is the ID field": "Standaard is het ID-veld",
+ "Set default sorting rules": "Stel standaard sorteerrregels in",
+ "Set validation rules": "Stel validatieregels in",
+ "Max length": "Maximale lengte",
+ "Min length": "Minimale lengte",
+ "Maximum": "Maximum",
+ "Minimum": "Minimum",
+ "Max length must greater than min length": "Maximale lengte moet groter zijn dan minimale lengte",
+ "Min length must less than max length": "Minimale lengte moet kleiner zijn dan maximale lengte",
+ "Maximum must greater than minimum": "Maximum moet groter zijn dan minimum",
+ "Minimum must less than maximum": "Minimum moet kleiner zijn dan maximum",
+ "Validation rule": "Validatieregel",
+ "Add validation rule": "Voeg validatieregel toe",
+ "Format": "Formaat",
+ "Regular expression": "Patroon",
+ "Error message": "Foutmelding",
+ "Length": "Lengte",
+ "The field value cannot be greater than ": "De veldwaarde mag niet groter zijn dan ",
+ "The field value cannot be less than ": "De veldwaarde mag niet kleiner zijn dan ",
+ "The field value is not an integer number": "De veldwaarde is geen geheel getal",
+ "Set default value": "Stel standaardwaarde in",
+ "Default value": "Standaardwaarde",
+ "is before": "is voor",
+ "is after": "is na",
+ "is on or after": "is op of na",
+ "is on or before": "is op of voor",
+ "is between": "is tussen",
+ "Upload": "Uploaden",
+ "Select level": "Selecteer niveau",
+ "Province": "Provincie",
+ "City": "Stad",
+ "Area": "Gebied",
+ "Street": "Straat",
+ "Village": "Dorp",
+ "Must select to the last level": "Moet tot het laatste niveau selecteren",
+ "Move {{title}} to": "Verplaats {{title}} naar",
+ "Target position": "Doelpositie",
+ "After": "Na",
+ "Before": "Voor",
+ "Add {{type}} before \"{{title}}\"": "Voeg {{type}} toe voor \"{{title}}\"",
+ "Add {{type}} after \"{{title}}\"": "Voeg {{type}} toe na \"{{title}}\"",
+ "Add {{type}} in \"{{title}}\"": "Voeg {{type}} toe in \"{{title}}\"",
+ "Original name": "Originele naam",
+ "Custom name": "Aangepaste naam",
+ "Custom Title": "Aangepaste titel",
+ "Options": "Opties",
+ "Option value": "Optiewaarde",
+ "Option label": "Optielabel",
+ "Color": "Kleur",
+ "Background Color": "Achtergrondkleur",
+ "Text Align": "Tekstuitlijning",
+ "Add option": "Optie toevoegen",
+ "Related collection": "Gerelateerde collectie",
+ "Allow linking to multiple records": "Koppelen aan meerdere records toestaan",
+ "Allow uploading multiple files": "Meerdere bestanden uploaden toestaan",
+ "Configure calendar": "Kalender configureren",
+ "Title field": "Titelveld",
+ "Custom title": "Aangepaste titel",
+ "Daily": "Dagelijks",
+ "Weekly": "Wekelijks",
+ "Monthly": "Maandelijks",
+ "Yearly": "Jaarlijks",
+ "Repeats": "Herhalingen",
+ "Show lunar": "Maankalender tonen",
+ "Start date field": "Begindatumveld",
+ "End date field": "Einddatumveld",
+ "Navigate": "Navigeren",
+ "Title": "Titel",
+ "Description": "Beschrijving",
+ "Select view": "Weergave selecteren",
+ "Reset": "Resetten",
+ "Importable fields": "Importeerbare velden",
+ "Exportable fields": "Exporteerbare velden",
+ "Saved successfully": "Succesvol opgeslagen",
+ "Nickname": "Bijnaam",
+ "Sign in": "Inloggen",
+ "Sign in via account": "Inloggen via account",
+ "Sign in via phone": "Inloggen via telefoon",
+ "Create an account": "Account aanmaken",
+ "Sign up": "Registreren",
+ "Confirm password": "Wachtwoord bevestigen",
+ "Log in with an existing account": "Inloggen met een bestaand account",
+ "Signed up successfully. It will jump to the login page.": "Succesvol geregistreerd. Je wordt doorgestuurd naar de inlogpagina.",
+ "Password mismatch": "Wachtwoorden komen niet overeen",
+ "Users": "Gebruikers",
+ "Verification code": "Verificatiecode",
+ "Send code": "Code verzenden",
+ "Retry after {{count}} seconds": "Probeer opnieuw over {{count}} seconden",
+ "Roles": "Rollen",
+ "Add role": "Rol toevoegen",
+ "Role name": "Rolnaam",
+ "Configure": "Configureren",
+ "Configure permissions": "Rechten configureren",
+ "Edit role": "Rol bewerken",
+ "Action permissions": "Actierechten",
+ "Menu permissions": "Menurechten",
+ "Menu item name": "Menunaam",
+ "Allow access": "Toegang toestaan",
+ "Action name": "Actienaam",
+ "Allow action": "Actie toestaan",
+ "Action scope": "Actieomvang",
+ "Operate on new data": "Werken met nieuwe data",
+ "Operate on existing data": "Werken met bestaande data",
+ "Yes": "Ja",
+ "No": "Nee",
+ "Red": "Rood",
+ "Magenta": "Magenta",
+ "Volcano": "Vulkaan",
+ "Orange": "Oranje",
+ "Gold": "Goud",
+ "Lime": "Limoen",
+ "Green": "Groen",
+ "Cyan": "Cyaan",
+ "Blue": "Blauw",
+ "Geek blue": "Geek blauw",
+ "Purple": "Paars",
+ "Default": "Standaard",
+ "Add card": "Kaart toevoegen",
+ "edit title": "titel bewerken",
+ "Turn pages": "Pagina's omslaan",
+ "Others": "Overigen",
+ "Other records": "Andere records",
+ "Save as reference template": "Opslaan als referentiesjabloon",
+ "Save as inherited template": "Opslaan als overerfde sjabloon",
+ "Save as block template": "Opslaan als bloksjabloon",
+ "Block templates": "Bloksjablonen",
+ "Block template": "Bloksjabloon",
+ "Convert reference to duplicate": "Referentie omzetten naar duplicaat",
+ "Template name": "Sjabloonnaam",
+ "Block type": "Bloktype",
+ "No blocks to connect": "Geen blokken om te verbinden",
+ "Action column": "Actiekolom",
+ "Records per page": "Records per pagina",
+ "(Fields only)": "(Alleen velden)",
+ "Button title": "Knoptekst",
+ "Button icon": "Knoppictogram",
+ "Submitted successfully": "Succesvol ingediend",
+ "Operation succeeded": "Bewerking geslaagd",
+ "Operation failed": "Bewerking mislukt",
+ "Open mode": "Openingsmodus",
+ "Popup size": "Popupgrootte",
+ "Small": "Klein",
+ "Middle": "Middel",
+ "Large": "Groot",
+ "Size": "Grootte",
+ "Oversized": "Te groot",
+ "Auto": "Automatisch",
+ "Object Fit": "Objectaanpassing",
+ "Cover": "Bedekken",
+ "Fill": "Vullen",
+ "Contain": "Bevatten",
+ "Scale Down": "Schalen naar beneden",
+ "Menu item title": "Menutitel",
+ "Menu item icon": "Menupictogram",
+ "Target": "Doel",
+ "Position": "Positie",
+ "Insert before": "Invoegen voor",
+ "Insert after": "Invoegen na",
+ "UI Editor": "UI Bewerker",
+ "ASC": "Oplopend",
+ "DESC": "Aflopend",
+ "Add sort field": "Sorteerveld toevoegen",
+ "ID": "ID",
+ "Identifier for program usage. Support letters, numbers and underscores, must start with an letter.": "Identificator voor programma gebruik. Ondersteunt letters, cijfers en underscores, moet met een letter beginnen.",
+ "Drawer": "Lade",
+ "Dialog": "Dialoog",
+ "Delete action": "Actie verwijderen",
+ "Custom column title": "Aangepaste kolomtitel",
+ "Column title": "kolomtitel",
+ "Original title: ": "Originele titel: ",
+ "Delete table column": "Tabelkolom verwijderen",
+ "Skip required validation": "Vereiste validatie overslaan",
+ "Form values": "Formuliervelden",
+ "Fields values": "Veldenwaarden",
+ "The field has been deleted": "Het veld is verwijderd",
+ "When submitting the following fields, the saved values are": "Bij het indienen van de volgende velden worden de waarden opgeslagen als",
+ "After successful submission": "Na succesvolle indiening",
+ "Then": "Dan",
+ "Stay on current page": "Blijf op de huidige pagina",
+ "Redirect to": "Omleiden naar",
+ "Save action": "Actie opslaan",
+ "Exists": "Bestaat",
+ "Add condition": "Voorwaarde toevoegen",
+ "Add condition group": "Voorwaardengroep toevoegen",
+ "exists": "bestaat",
+ "not exists": "bestaat niet",
+ "Style": "Stijl",
+ "=": "=",
+ "≠": "≠",
+ ">": ">",
+ "≥": "≥",
+ "<": "<",
+ "≤": "≤",
+ "Role UID": "Rol-ID",
+ "Precision": "Precisie",
+ "Formula mode": "Formulemodus",
+ "Expression": "Expressie",
+ "Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "Voer +, -, *, /, ( ) in om te berekenen, voer @ in om veldvariabelen te openen.",
+ "Formula error.": "Formulefout.",
+ "Rich Text": "Opgemaakte tekst",
+ "Junction collection": "Koppelingstabel",
+ "Leave it blank, unless you need a custom intermediate table": "Laat leeg, tenzij je een aangepaste tussenliggende tabel nodig hebt",
+ "Fields": "Velden",
+ "Edit field title": "Veldtitel bewerken",
+ "Field title": "Veldtitel",
+ "Original field title: ": "Originele veldtitel: ",
+ "Edit tooltip": "Tooltip bewerken",
+ "Delete field": "Veld verwijderen",
+ "Select collection": "Selecteer collectie",
+ "Blank block": "Leeg blok",
+ "Duplicate template": "Sjabloon dupliceren",
+ "Reference template": "Sjabloon refereren",
+ "Inherited template": "Overerfde sjabloon",
+ "Create calendar block": "Kalenderblok maken",
+ "Create kanban block": "Kanbanblok maken",
+ "Grouping field": "Groepeer veld",
+ "Single select and radio fields can be used as the grouping field": "Enkelvoudige selecteer- en keuzerondjevelden kunnen worden gebruikt als groepeerveld",
+ "Tab name": "Tabbladnaam",
+ "Current record blocks": "Huidige recordblokken",
+ "Popup message": "Popupbericht",
+ "Delete role": "Rol verwijderen",
+ "Role display name": "Rolweergavenaam",
+ "Default role": "Standaardrol",
+ "All collections use general action permissions by default; permission configured individually will override the default one.": "Alle collecties gebruiken standaard algemene actierechten; individueel geconfigureerde rechten overschrijven de standaardinstellingen.",
+ "Allows configuration of the whole system, including UI, collections, permissions, etc.": "Maakt configuratie van het hele systeem mogelijk, inclusief gebruikersinterface, collecties, rechten, enz.",
+ "New menu items are allowed to be accessed by default.": "Nieuwe menu-items zijn standaard toegankelijk.",
+ "Global permissions": "Globale rechten",
+ "General permissions": "Algemene rechten",
+ "Global action permissions": "Globale actierechten",
+ "General action permissions": "Algemene actierechten",
+ "Plugin settings permissions": "Rechten voor plugin-instellingen",
+ "Allow to desgin pages": "Toestaan om pagina's te ontwerpen",
+ "Allow to manage plugins": "Toestaan om plugins te beheren",
+ "Allow to configure plugins": "Toestaan om plugins te configureren",
+ "Allows to configure interface": "Toestemming om de interface te configureren",
+ "Allows to install, activate, disable plugins": "Toestemming om plugins te installeren, activeren of uitschakelen",
+ "Allows to configure plugins": "Toestemming om plugins te configureren",
+ "Action display name": "Actieweergavenaam",
+ "Allow": "Toestaan",
+ "Data scope": "Databereik",
+ "Action on new records": "Actie op nieuwe records",
+ "Action on existing records": "Actie op bestaande records",
+ "All records": "Alle records",
+ "Own records": "Eigen records",
+ "Permission policy": "Rechtenbeleid",
+ "Individual": "Individueel",
+ "General": "Algemeen",
+ "Accessible": "Toegankelijk",
+ "Configure permission": "Rechten configureren",
+ "Action permission": "Actierecht",
+ "Field permission": "Veldrecht",
+ "Scope name": "Bereiksnaam",
+ "Unsaved changes": "Niet-opgeslagen wijzigingen",
+ "Are you sure you don't want to save?": "Weet je zeker dat je niet wil opslaan?",
+ "Dragging": "Slepen",
+ "Popup": "Popup",
+ "Trigger workflow": "Workflow activeren",
+ "Request API": "API-aanvraag",
+ "Assign field values": "Veldwaarden toewijzen",
+ "Constant value": "Constante waarde",
+ "Dynamic value": "Dynamische waarde",
+ "Current user": "Huidige gebruiker",
+ "Current role": "Huidige rol",
+ "Current record": "Huidig record",
+ "Current collection": "Huidige collectie",
+ "Other collections": "Andere collecties",
+ "Current popup record": "Huidig popup-record",
+ "Parent popup record": "Ouder-popuprecord",
+ "Associated records": "Gekoppelde records",
+ "Parent record": "Hoofdrecord",
+ "Current time": "Huidige tijd",
+ "System variables": "Systeemvariabelen",
+ "Date variables": "Datumvariabelen",
+ "Message popup close method": "Bericht-popup sluitmethode",
+ "Automatic close": "Automatisch sluiten",
+ "Manually close": "Handmatig sluiten",
+ "After successful update": "Na succesvolle update",
+ "Save record": "Record opslaan",
+ "Updated successfully": "Succesvol bijgewerkt",
+ "After successful save": "Na succesvol opslaan",
+ "After clicking the custom button, the following field values will be assigned according to the following form.": "Na het klikken op de aangepaste knop worden de volgende veldwaarden toegewezen volgens het onderstaande formulier.",
+ "After clicking the custom button, the following fields of the current record will be saved according to the following form.": "Na het klikken op de aangepaste knop worden de volgende velden van het huidige record opgeslagen volgens het onderstaande formulier.",
+ "Button background color": "Knopachtergrondkleur",
+ "Highlight": "Markeren",
+ "Danger red": "Waarschuwing rood",
+ "Custom request": "Aangepaste aanvraag",
+ "Request settings": "Aanvraaginstellingen",
+ "Request URL": "Aanvraag-URL",
+ "Request method": "Aanvraagmethode",
+ "Request query parameters": "Aanvraagqueryparameters",
+ "Request headers": "Aanvraagheaders",
+ "Request body": "Aanvraaginhoud",
+ "Request success": "Aanvraag geslaagd",
+ "Invalid JSON format": "Ongeldig JSON-formaat",
+ "After successful request": "Na succesvolle aanvraag",
+ "Add exportable field": "Exporteerbaar veld toevoegen",
+ "Audit logs": "Auditlogboeken",
+ "Record ID": "Record-ID",
+ "User": "Gebruiker",
+ "Field": "Veld",
+ "Select": "Selecteren",
+ "Select field": "Veld selecteren",
+ "Field value changes": "Wijzigingen in veldwaarden",
+ "One to one (has one)": "Eén op één (heeft één)",
+ "One to one (belongs to)": "Eén op één (behoort tot)",
+ "Use the same time zone (GMT) for all users": "Gebruik dezelfde tijdzone (GMT) voor alle gebruikers",
+ "Province/city/area name": "Provincie/stad/gebiedsnaam",
+ "Enabled languages": "Ingeschakelde talen",
+ "View all plugins": "Alle plugins bekijken",
+ "Print": "Afdrukken",
+ "Done": "Klaar",
+ "Sign up successfully, and automatically jump to the sign in page": "Succesvol geregistreerd, springt automatisch naar de inlogpagina",
+ "File manager": "Bestandsbeheer",
+ "ACL": "Toegangscontrolelijst",
+ "Collection manager": "Collectiebeheerder",
+ "Plugin manager": "Pluginbeheerder",
+ "Local": "Lokaal",
+ "Built-in": "Ingebouwd",
+ "Marketplace": "Marktplaats",
+ "Add plugin": "Plugin toevoegen",
+ "Plugin source": "Pluginbron",
+ "Upgrade": "Upgrade",
+ "Plugin dependencies check failed": "Controle van plug-in afhankelijkheden mislukt",
+ "More details": "Meer details",
+ "Upload new version": "Nieuwe versie uploaden",
+ "Version": "Versie",
+ "Npm package": "Npm-pakket",
+ "Npm package name": "Naam van npm-pakket",
+ "Upload plugin": "Plugin uploaden",
+ "Official plugin": "Officiële plugin",
+ "Add type": "Type toevoegen",
+ "Changelog": "Wijzigingslogboek",
+ "Dependencies check": "Afhankelijkhedencontrole",
+ "Update plugin": "Plugin bijwerken",
+ "Installing": "Installeren",
+ "The deletion was successful.": "De verwijdering is succesvol.",
+ "Plugin Zip File": "Plugin-zipbestand",
+ "Compressed file url": "URL van gecomprimeerd bestand",
+ "Last updated": "Laatst bijgewerkt",
+ "PackageName": "Pakketnaam",
+ "DisplayName": "Weergavenaam",
+ "Readme": "Lees mij",
+ "Dependencies compatibility check": "Compatibiliteitscontrole afhankelijkheden",
+ "Plugin dependencies check failed, you should change the dependent version to meet the version requirements.": "Controle van plug-in afhankelijkheden mislukt. Je moet de afhankelijke versie aanpassen aan de versievereisten.",
+ "Version range": "Versiebereik",
+ "Plugin's version": "Plug-in versie",
+ "Result": "Resultaat",
+ "No CHANGELOG.md file": "Geen CHANGELOG.md-bestand",
+ "No README.md file": "Geen README.md-bestand",
+ "Homepage": "Homepage",
+ "Drag and drop the file here or click to upload, file size should not exceed 30M": "Sleep het bestand hierheen of klik om te uploaden, bestandsgrootte mag niet groter zijn dan 30 MB",
+ "Dependencies check failed, can't enable.": "Controle van afhankelijkheden mislukt, inschakelen niet mogelijk.",
+ "Plugin starting...": "Plugin wordt gestart...",
+ "Plugin stopping...": "Plugin wordt gestopt...",
+ "Are you sure to delete this plugin?": "Weet je zeker dat je deze plugin wil verwijderen?",
+ "Are you sure to disable this plugin?": "Weet je zeker dat je deze plugin wil uitschakelen?",
+ "re-download file": "bestand opnieuw downloaden",
+ "Not enabled": "Niet ingeschakeld",
+ "Search plugin": "Plugin zoeken",
+ "Author": "Auteur",
+ "Plugin loading failed. Please check the server logs.": "Het laden van de plugin is mislukt. Controleer de serverlogs.",
+ "Coming soon...": "Binnenkort beschikbaar...",
+ "All plugin settings": "Alle plugininstellingen",
+ "Bookmark": "Bladwijzer",
+ "Manage all settings": "Beheer alle instellingen",
+ "Create inverse field in the target collection": "Creëer een omgekeerd veld in de doelcollectie",
+ "Inverse field name": "Omgekeerde veldnaam",
+ "Inverse field display name": "Weergavenaam omgekeerd veld",
+ "Bulk update": "Updaten in bulk",
+ "After successful bulk update": "Na succesvolle update in bulk",
+ "Bulk edit": "Bewerken in bulk",
+ "Data will be updated": "Gegevens worden bijgewerkt",
+ "Selected": "Geselecteerd",
+ "All": "Alles",
+ "Update selected data?": "Geselecteerde gegevens bijwerken?",
+ "Update all data?": "Alle gegevens bijwerken?",
+ "Remains the same": "Blijft hetzelfde",
+ "Changed to": "Veranderd in",
+ "Clear": "Wissen",
+ "Add attach": "Bijlage toevoegen",
+ "Please select the records to be updated": "Selecteer de records die moeten worden bijgewerkt",
+ "Selector": "Selector",
+ "Inner": "Intern",
+ "Search and select collection": "Zoek en selecteer collectie",
+ "Please fill in the iframe URL": "Vul de iframe-URL in",
+ "Fix block": "Blok vastzetten",
+ "Plugin name": "Pluginnaam",
+ "Plugin tab name": "Naam plugintabblad",
+ "AutoGenId": "Automatisch gegenereerd ID veld",
+ "CreatedBy": "Aangemaakt door",
+ "UpdatedBy": "Bijgewerkt door",
+ "CreatedAt": "Aangemaakt op",
+ "UpdatedAt": "Bijgewerkt op",
+ "Column width": "Kolombreedte",
+ "Sortable": "Sorteerbaar",
+ "Enable link": "Link inschakelen",
+ "This is likely a NocoBase internals bug. Please open an issue at <1>here1>": "Dit is waarschijnlijk een interne bug in NocoBase. Open een issue via <1>hier1>",
+ "Render Failed": "Renderen mislukt",
+ "App error": "App-fout",
+ "Feedback": "Feedback",
+ "Try again": "Probeer opnieuw",
+ "Download logs": "Logs downloaden",
+ "Data template": "Gegevenssjabloon",
+ "Duplicate": "Dupliceren",
+ "Duplicating": "Dupliceren",
+ "Duplicate mode": "Dupliceermodus",
+ "Quick duplicate": "Snel dupliceren",
+ "Duplicate and continue": "Dupliceren en doorgaan",
+ "Please configure the duplicate fields": "Configureer de te dupliceren velden",
+ "Add": "Toevoegen",
+ "Add new mode": "Nieuwe modus toevoegen",
+ "Quick add": "Snel toevoegen",
+ "Modal add": "Toevoegen via venster",
+ "Save mode": "Opslagmodus",
+ "First or create": "Eerste of aanmaken",
+ "Update or create": "Bijwerken of aanmaken",
+ "Find by the following fields": "Zoek op de volgende velden",
+ "Create": "Aanmaken",
+ "Current form": "Huidig formulier",
+ "Current object": "Huidig object",
+ "Linkage with form fields": "Koppeling met formulier velden",
+ "Allow add new, update and delete actions": "Toestaan om toe te voegen, bijwerken en verwijderen",
+ "Date display format": "Weergaveformaat datum",
+ "Assign data scope for the template": "Gegevensbereik toewijzen aan sjabloon",
+ "Table selected records": "Tabel geselecteerde records",
+ "Tag": "Label",
+ "Tag color field": "Labelkleurveld",
+ "Sync successfully": "Succesvol gesynchroniseerd",
+ "Sync from form fields": "Synchroniseren vanaf formulier velden",
+ "Select all": "Alles selecteren",
+ "Restart": "Herstarten",
+ "Restart application": "Applicatie herstarten",
+ "Cascade Select": "Cascade selectie",
+ "Execute": "Uitvoeren",
+ "Please use a valid SELECT or WITH AS statement": "Gebruik een geldige SELECT of WITH AS-verklaring",
+ "Please confirm the SQL statement first": "Bevestig eerst de SQL-verklaring",
+ "Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects": "Automatisch objecten verwijderen die afhankelijk zijn van de collectie (zoals weergaven) en ook alle objecten die daarvan afhankelijk zijn",
+ "Sign in with another account": "Aanmelden met een ander account",
+ "Return to the main application": "Terug naar de hoofdapplicatie",
+ "Permission deined": "Toegang geweigerd",
+ "loading": "laden",
+ "name is required": "naam is vereist",
+ "data source": "gegevensbron",
+ "Data source": "Gegevensbron",
+ "DataSource": "Gegevensbron",
+ "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "Het {{type}} \"{{name}}\" is mogelijk verwijderd. Verwijder dit {{blockType}}.",
+ "Preset fields": "Voorinstellingen",
+ "Home page": "Startpagina",
+ "Handbook": "Handleiding",
+ "License": "Licentie",
+ "Generic properties": "Algemene eigenschappen",
+ "Specific properties": "Specifieke eigenschappen",
+ "Used for drag and drop sorting scenarios, supporting grouping sorting": "Gebruikt voor drag and drop sorteren, met ondersteuning voor groeperen",
+ "Grouped sorting": "Gegroepeerd sorteren",
+ "When a field is selected for grouping, it will be grouped first before sorting.": "Als een veld wordt geselecteerd voor groepering, wordt dit eerst gegroepeerd voordat er wordt gesorteerd.",
+ "Departments": "Afdelingen",
+ "Main department": "Hoofdafdeling",
+ "Department name": "Afdelingsnaam",
+ "Superior department": "Overkoepelende afdeling",
+ "Owners": "Eigenaren",
+ "Plugin settings": "Plugin-instellingen",
+ "Menu": "Menu",
+ "Drag and drop sorting field": "Sorteren via drag and drop",
+ "This variable has been deprecated and can be replaced with \"Current form\"": "Deze variabele is verouderd en kan worden vervangen door \"Huidig formulier\"",
+ "The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.": "De waarde van deze variabele is afgeleid van de querystring in de URL. Deze variabele werkt alleen correct als de pagina een querystring bevat.",
+ "URL search params": "URL zoekparameters",
+ "Expand All": "Alles uitvouwen",
+ "Search": "Zoeken",
+ "Clear default value": "Standaardwaarde wissen",
+ "Open in new window": "Openen in nieuw venster",
+ "Sorry, the page you visited does not exist.": "Sorry, de pagina die je bezocht bestaat niet.",
+ "is none of": "is geen van",
+ "is any of": "is een van",
+ "Plugin dependency version mismatch": "Versieafhankelijkheid van plugin komt niet overeen",
+ "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?": "De huidige afhankelijkheidsversie van de plugin komt niet overeen met de versie van de applicatie en werkt mogelijk niet correct. Weet je zeker dat je de plugin wil blijven inschakelen?",
+ "Allow multiple selection": "Meerdere selecties toestaan",
+ "Parent object": "Bovenliggend object",
+ "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": "Sla het ophalen van het totale aantal tabelrecords over tijdens paginering om het laden te versnellen. Het wordt aanbevolen om deze optie in te schakelen voor datatabellen met veel gegevens.",
+ "Enable secondary confirmation": "Tweede bevestiging inschakelen",
+ "Notification": "Melding",
+ "Ellipsis overflow content": "Inhoud afkorten met ellips",
+ "Hide column": "Kolom verbergen",
+ "In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "In configuratiemodus wordt de hele kolom transparant. In niet-configuratiemodus wordt de hele kolom verborgen. Zelfs als de hele kolom verborgen is, blijven de geconfigureerde standaardwaarden en andere instellingen van kracht.",
+ "Unauthenticated. Please sign in to continue.": "Niet geauthenticeerd. Meld je aan om verder te gaan.",
+ "User not found. Please sign in again to continue.": "Gebruiker niet gevonden. Meld je opnieuw aan om verder te gaan.",
+ "Your session has expired. Please sign in again.": "Je sessie is verlopen. Meld je opnieuw aan.",
+ "User password changed, please signin again.": "Gebruikerswachtwoord gewijzigd, meld je opnieuw aan.",
+ "Desktop routes": "Desktop-routes",
+ "Route permissions": "Route-machtigingen",
+ "New routes are allowed to be accessed by default": "Nieuwe routes zijn standaard toegankelijk",
+ "Route name": "Routenaam",
+ "Mobile routes": "Mobiele routes",
+ "Show in menu": "Weergeven in menu",
+ "Hide in menu": "Verbergen in menu",
+ "Path": "Pad",
+ "Type": "Type",
+ "Access": "Toegang",
+ "Routes": "Routes",
+ "Add child route": "Subroute toevoegen",
+ "Delete routes": "Routes verwijderen",
+ "Delete route": "Route verwijderen",
+ "Are you sure you want to hide these routes in menu?": "Weet je zeker dat je deze routes in het menu wil verbergen?",
+ "Are you sure you want to show these routes in menu?": "Weet je zeker dat je deze routes in het menu wil weergeven?",
+ "Are you sure you want to hide this menu?": "Weet je zeker dat je dit menu wil verbergen?",
+ "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Na verbergen wordt dit menu niet meer weergegeven in de menubalk. Om het opnieuw te tonen, moet je naar de routebeheerpagina gaan om het in te stellen.",
+ "If selected, the page will display Tab pages.": "Indien geselecteerd, worden tabbladen op de pagina weergegeven.",
+ "If selected, the route will be displayed in the menu.": "Indien geselecteerd, wordt de route weergegeven in het menu.",
+ "Are you sure you want to hide this tab?": "Weet je zeker dat je dit tabblad wil verbergen?",
+ "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Na verbergen wordt dit tabblad niet meer weergegeven in de tabbalk. Om het opnieuw te tonen, moet je naar de routebeheerpagina gaan om het in te stellen.",
+ "Calculation engine": "Berekeningsengine",
+ "Expression collection": "Expressiecollectie",
+ "Tree collection": "Boomcollectie",
+ "Parent ID": "Ouder-ID",
+ "Parent": "Ouder",
+ "Children": "Kinderen",
+ "Confirm": "Bevestigen",
+ "Block": "Blok",
+ "Unnamed": "Naamloos",
+ "SQL collection": "SQL-collectie",
+ "Configure field": "Veld configureren",
+ "Username": "Gebruikersnaam",
+ "Null": "Leeg",
+ "Boolean": "Boolean",
+ "String": "Tekst",
+ "Syntax references": "Syntax-referenties",
+ "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 wordt geleverd met een groot aantal ingebouwde functies en constanten en biedt een geïntegreerde oplossing om met verschillende gegevenstypen te werken.",
+ "Formula.js supports most Microsoft Excel formula functions.": "Formula.js ondersteunt de meeste Microsoft Excel-formulefuncties.",
+ "String template": "Tekstsjabloon",
+ "Simple string replacement, can be used to interpolate variables in a string.": "Eenvoudige tekstvervanging, kan worden gebruikt om variabelen in een tekst te interpoleren.",
+ "https://docs.nocobase.com/handbook/calculation-engines/formula": "https://docs.nocobase.com/handbook/calculation-engines/formula",
+ "https://docs.nocobase.com/handbook/calculation-engines/mathjs": "https://docs.nocobase.com/handbook/calculation-engines/mathjs",
+ "Display when unchecked": "Toon wanneer niet aangevinkt",
+ "Allow dissociate": "Loskoppelen toestaan",
+ "Edit block title & description": "Blok titel & beschrijving bewerken",
+ "Add Markdown": "Markdown toevoegen",
+ "Must be 1-50 characters in length (excluding @.<>\"'/)": "Moet 1-50 tekens lang zijn (exclusief @.<>\"'/)",
+ "Original title: ": "Originele titel: ",
+ "Original field title: ": "Originele veldtitel: ",
+ "Data source permissions": "Datatoegangsrechten",
+ "Now": "Nu",
+ "Access control": "Toegangscontrole",
+ "Remove": "Verwijderen",
+ "Docs": "Documentatie",
+ "Enable page header": "Paginahoofd inschakelen",
+ "Display page title": "Paginatitel weergeven",
+ "Edit page title": "Paginatitel bewerken",
+ "Enable page tabs": "Pagina-tabbladen inschakelen",
+ "Constant": "Constant",
+ "Select a variable": "Selecteer een variabele",
+ "Double click to choose entire object": "Dubbelklik om het hele object te kiezen",
+ "TRUE": "WAAR",
+ "FALSE": "ONWAAR",
+ "Prettify": "Opknappen",
+ "Theme": "Thema",
+ "Default theme": "Standaardthema",
+ "Compact theme": "Compact thema",
+ "Download": "Downloaden",
+ "File type is not supported for previewing, please download it to preview.": "Bestandstype wordt niet ondersteund voor voorbeeldweergave, download het om te bekijken.",
+ "Click or drag file to this area to upload": "Klik of sleep een bestand naar dit gebied om te uploaden.",
+ "Support for a single or bulk upload.": "Ondersteuning voor enkele of bulk-upload.",
+ "File size should not exceed {{size}}.": "Bestandsgrootte mag {{size}} niet overschrijden.",
+ "File size exceeds the limit": "Bestandsgrootte overschrijdt de limiet.",
+ "File type is not allowed": "Bestandstype is niet toegestaan.",
+ "Incomplete uploading files need to be resolved": "Onvolledig geüploade bestanden moeten worden opgelost.",
+ "Default title for each record": "Standaardtitel voor elk record.",
+ "If collection inherits, choose inherited collections as templates": "Als de collectie overerft, kies geërfde collecties als sjablonen.",
+ "Select an existing piece of data as the initialization data for the form": "Selecteer een bestaand gegeven als initiële data voor het formulier.",
+ "Only the selected fields will be used as the initialization data for the form": "Alleen de geselecteerde velden worden gebruikt als initiële data voor het formulier.",
+ "Template Data": "Sjabloongegevens",
+ "Data fields": "Gegevensvelden",
+ "Add template": "Sjabloon toevoegen",
+ "Enable form data template": "Formuliersjabloon inschakelen",
+ "Form data templates": "Formuliersjablonen",
+ "No configuration available.": "Geen configuratie beschikbaar.",
+ "Reload application": "Herlaad applicatie",
+ "The application is reloading, please do not close the page.": "De applicatie wordt opnieuw geladen, sluit de pagina niet.",
+ "Application reloading": "Applicatie wordt opnieuw geladen",
+ "Allows to clear cache, reboot application": "Sta toe om cache te wissen en applicatie te herstarten.",
+ "The will interrupt service, it may take a few seconds to restart. Are you sure to continue?": "Dit onderbreekt de service en kan enkele seconden duren. Weet je zeker dat je wilt doorgaan?",
+ "Clear cache": "Cache wissen",
+ "Are you sure you want to clear cache ?": "Weet je zeker dat je de cache wilt wissen?",
+ "Quick create": "Snel aanmaken",
+ "Dropdown": "Keuzelijst",
+ "Pop-up": "Pop-up",
+ "Direct duplicate": "Direct dupliceren",
+ "Copy into the form and continue to fill in": "Kopieer in het formulier en vul verder in.",
+ "Failed to load plugin": "Kan plug-in niet laden.",
+ "Date range limit": "Datumbereiklimiet",
+ "MinDate": "Minimale datum",
+ "MaxDate": "Maximale datum",
+ "Please select time or variable": "Selecteer een tijd of variabele.",
+ "Filter out a single piece or a group of records as a template": "Filter een enkel item of een groep records als sjabloon.",
+ "The title field is used to identify the template record": "Het titelveld wordt gebruikt om het sjabloonrecord te identificeren.",
+ "Template fields": "Sjabloonvelden",
+ "The selected fields will automatically populate the form": "De geselecteerde velden vullen automatisch het formulier in.",
+ "UnSelect all": "Alles deselecteren",
+ "Secondary confirmation": "Secundaire bevestiging",
+ "Perform the {{title}}": "Voer {{title}} uit.",
+ "Are you sure you want to perform the {{title}} action?": "Weet je zeker dat je de actie {{title}} wilt uitvoeren?",
+ "Permission denied": "Toestemming geweigerd.",
+ "Allow add new": "Sta toe om nieuw toe te voegen.",
+ "Data model": "Gegevensmodel",
+ "Security": "Beveiliging",
+ "Action": "Actie",
+ "System": "Systeem",
+ "Other": "Overige",
+ "Allow selection of existing records": "Sta selectie van bestaande records toe.",
+ "Data Model": "Gegevensmodel",
+ "Blocks": "Blokken",
+ "Users & permissions": "Gebruikers en machtigingen",
+ "System management": "Systeembeheer",
+ "System & security": "Systeem en beveiliging",
+ "Workflow": "Werkstroom",
+ "Third party services": "Diensten van derden",
+ "Data model tools": "Gegevensmodeltools",
+ "Data sources": "Gegevensbronnen",
+ "Collections": "Collecties",
+ "Collection fields": "Collectievelden",
+ "Authentication": "Authenticatie",
+ "Logging and monitoring": "Logging en monitoring",
+ "Main": "Hoofd",
+ "Index": "Index",
+ "Field values must be unique.": "Veldwaarden moeten uniek zijn.",
+ "Alphabet": "Alfabet",
+ "Accuracy": "Nauwkeurigheid",
+ "Millisecond": "Milliseconde",
+ "Second": "Seconde",
+ "Unix Timestamp": "Unix-tijdstempel",
+ "Field value do not meet the requirements": "Veldwaarde voldoet niet aan de vereisten.",
+ "Field value size is": "Veldwaardegrootte is",
+ "Unit conversion": "Eenheidsconversie",
+ "Separator": "Scheidingsteken",
+ "Prefix": "Voorvoegsel",
+ "Suffix": "Achtervoegsel",
+ "Record unique key": "Unieke recordsleutel",
+ "Filter target key": "Filterdoelsleutel",
+ "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.": "Als een collectie geen primaire sleutel heeft, moet je een unieke recordsleutel configureren om rijen binnen een blok te lokaliseren. Anders kunnen er geen gegevensblokken worden aangemaakt.",
+ "Filter data based on the specific field, with the requirement that the field value must be unique.": "Filter gegevens op basis van een specifiek veld, waarbij de veldwaarde uniek moet zijn.",
+ "Multiply by": "Vermenigvuldigen met",
+ "Divide by": "Delen door",
+ "Scientifix notation": "Wetenschappelijke notatie",
+ "Normal": "Normaal",
+ "Automatically generate default values": "Automatisch standaardwaarden genereren.",
+ "Refresh data on close": "Ververs gegevens bij sluiten.",
+ "Refresh data on action": "Ververs gegevens bij actie.",
+ "Unknown field type": "Onbekend veldtype.",
+ "The following field types are not compatible and do not support output and display": "De volgende veldtypen zijn niet compatibel en ondersteunen geen uitvoer en weergave.",
+ "Not fixed": "Niet vastgezet",
+ "Left fixed": "Links vastgezet",
+ "Right fixed": "Rechts vastgezet",
+ "Fixed": "Vastgezet",
+ "Set block height": "Blokhoogte instellen",
+ "Specify height": "Hoogte specificeren",
+ "Full height": "Volledige hoogte",
+ "Please configure the URL": "Configureer de URL.",
+ "URL": "URL",
+ "Search parameters": "Zoekparameters",
+ "Do not concatenate search params in the URL": "Voeg zoekparameters niet samen in de URL.",
+ "Edit link": "Link bewerken",
+ "Add parameter": "Parameter toevoegen",
+ "Use simple pagination mode": "Gebruik eenvoudige paginering",
+ "Set Template Engine": "Sjabloonengine instellen",
+ "Template engine": "Sjabloonengine",
+ "Table size": "Tabelgrootte",
+ "No data": "Geen data",
+ "Show file name": "Toon bestandsnaam",
+ "Filled": "Gevuld",
+ "Enable index column": "Indexkolom inschakelen",
+ "Icon only": "Enkel icoon",
+ "Valid range: 100-900": "Geldige waarde: 100-900",
+ "Valid range: 10-40": "Geldige waarde: 10-40",
+ "Font Size(px)": "Lettergrootte(px)",
+ "Font Weight": "Letterdikte",
+ "Font Style": "Letterstijl",
+ "Italic": "Cursief",
+ "Refresh data blocks": "Vernieuw gegevensblokken",
+ "Select data blocks to refresh": "Selecteer gegevensblokken om te vernieuwen",
+ "After successful submission, the selected data blocks will be automatically refreshed.": "Na succesvolle indiening worden de geselecteerde gegevensblokken automatisch vernieuwd."
+}
diff --git a/packages/core/client/src/locale/pt-BR.json b/packages/core/client/src/locale/pt-BR.json
index 1bacded113..c9b15f6e99 100644
--- a/packages/core/client/src/locale/pt-BR.json
+++ b/packages/core/client/src/locale/pt-BR.json
@@ -112,6 +112,7 @@
"Chart type": "Tipo de gráfico",
"Chart config": "Configuração do gráfico",
"Templates": "Modelos",
+ "Template": "Modelo",
"Select template": "Selecione um modelo",
"Action logs": "Registros de ação",
"Create template": "Criar modelo",
@@ -432,7 +433,8 @@
"Turn pages": "Virar páginas",
"Others": "Outros",
"Other records": "Outros registros",
- "Save as template": "Salvar como modelo",
+ "Save as reference template": "Salvar como modelo de referência",
+ "Save as inherited template": "Salvar como modelo herdado",
"Save as block template": "Salvar como modelo de bloco",
"Block templates": "Modelos de bloco",
"Block template": "Modelo de bloco",
@@ -524,6 +526,7 @@
"Blank block": "Bloco em branco",
"Duplicate template": "Duplicar modelo",
"Reference template": "Modelo de referência",
+ "Inherited template": "Modelo herdado",
"Create calendar block": "Criar bloco de calendário",
"Create kanban block": "Criar bloco Kanban",
"Grouping field": "Campo de agrupamento",
@@ -778,5 +781,12 @@
"If selected, the page will display Tab pages.": "Se selecionado, a página exibirá páginas de abas.",
"If selected, the route will be displayed in the menu.": "Se selecionado, a rota será exibida no menu.",
"Are you sure you want to hide this tab?": "Tem certeza de que deseja ocultar esta guia?",
- "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Depois de ocultar, esta guia não aparecerá mais na barra de guias. Para mostrá-la novamente, você precisa ir à página de gerenciamento de rotas para configurá-la."
+ "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Depois de ocultar, esta guia não aparecerá mais na barra de guias. Para mostrá-la novamente, você precisa ir à página de gerenciamento de rotas para configurá-la.",
+ "Deprecated": "Descontinuado",
+ "Full permissions": "Todas as permissões",
+ "No pages yet, please configure first": "Ainda não há páginas, por favor configure primeiro",
+ "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur",
+ "Refresh data blocks": "Atualizar blocos de dados",
+ "Select data blocks to refresh": "Selecionar blocos de dados para atualizar",
+ "After successful submission, the selected data blocks will be automatically refreshed.": "Após a atualização em massa bem sucedida."
}
diff --git a/packages/core/client/src/locale/ru-RU.json b/packages/core/client/src/locale/ru-RU.json
index c85a74cdd1..65340c38ee 100644
--- a/packages/core/client/src/locale/ru-RU.json
+++ b/packages/core/client/src/locale/ru-RU.json
@@ -102,6 +102,7 @@
"Chart type": "Тип диаграммы",
"Chart config": "Конфиг. диаграммы",
"Templates": "Шаблоны",
+ "Template": "Шаблон",
"Select template": "Выбрать шаблон",
"Action logs": "Журналы действий",
"Create template": "Создать шаблон",
@@ -336,7 +337,8 @@
"Turn pages": "Перелистывать страницы",
"Others": "Другие",
"Other records": "Другие записи",
- "Save as template": "Сохранить как шаблон",
+ "Save as reference template": "Сохранить как шаблон ссылки",
+ "Save as inherited template": "Сохранить как шаблон наследования",
"Save as block template": "Сохранить как шаблон Блока",
"Block templates": "Шаблоны блоков",
"Convert reference to duplicate": "Преобразовать ссылку в дубликат",
@@ -410,6 +412,7 @@
"Blank block": "Пустой блок",
"Duplicate template": "Дублировать шаблон",
"Reference template": "Справочный шаблон",
+ "Inherited template": "Наследуемый шаблон",
"Create calendar block": "Создать блок календаря",
"Create kanban block": "Создать блок Канбан",
"Grouping field": "Поле группировки",
@@ -607,5 +610,12 @@
"If selected, the page will display Tab pages.": "Если выбран, страница будет отображать страницы с вкладками.",
"If selected, the route will be displayed in the menu.": "Если выбран, маршрут будет отображаться в меню.",
"Are you sure you want to hide this tab?": "Вы уверены, что хотите скрыть эту вкладку?",
- "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "После скрытия этой вкладки она больше не будет отображаться во вкладке. Чтобы снова отобразить ее, вам нужно будет перейти на страницу управления маршрутами, чтобы установить ее."
+ "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "После скрытия этой вкладки она больше не будет отображаться во вкладке. Чтобы снова отобразить ее, вам нужно будет перейти на страницу управления маршрутами, чтобы установить ее.",
+ "Deprecated": "Устаревший",
+ "Full permissions": "Полные права",
+ "No pages yet, please configure first": "Пока нет страниц, пожалуйста, настройте сначала",
+ "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Нажмите на значок \"Редактор пользовательского интерфейса\" в правом верхнем углу, чтобы войти в режим редактора пользовательского интерфейса",
+ "Refresh data blocks": "Обновить блоки данных",
+ "Select data blocks to refresh": "Выберите блоки данных для обновления",
+ "After successful submission, the selected data blocks will be automatically refreshed.": "После успешной отправки выбранные блоки данных будут автоматически обновлены."
}
diff --git a/packages/core/client/src/locale/tr-TR.json b/packages/core/client/src/locale/tr-TR.json
index 3845bc75a0..b6c66fe2c1 100644
--- a/packages/core/client/src/locale/tr-TR.json
+++ b/packages/core/client/src/locale/tr-TR.json
@@ -102,6 +102,7 @@
"Chart type": "Grafik türü",
"Chart config": "Grafik yapılandırması",
"Templates": "Şablonlar",
+ "Template": "Şablon",
"Select template": "Şablon seç",
"Action logs": "Eylem günlükleri",
"Create template": "Şablon oluştur",
@@ -335,7 +336,8 @@
"Turn pages": "Sayfaları çevir",
"Others": "Diğerleri",
"Other records": "Diğer kayıtlar",
- "Save as template": "Şablon olarak kaydet",
+ "Save as reference template": "Referans şablonu olarak kaydet",
+ "Save as inherited template": "Kalıtım şablonu olarak kaydet",
"Save as block template": "Blok şablonu olarak kaydet",
"Block templates": "Blok şablonları",
"Block template": "Blok şablonu",
@@ -410,6 +412,7 @@
"Blank block": "Boş blok",
"Duplicate template": "Şablonun kopyasını oluştur",
"Reference template": "Referans şablon",
+ "Inherited template": "Kalıtım şablonu",
"Create calendar block": "Takvim bloğu oluştur",
"Create kanban block": "Kanban bloğu oluştur",
"Grouping field": "Alan gruplandırma",
@@ -605,5 +608,12 @@
"If selected, the page will display Tab pages.": "Seçildiğinde, sayfa Tab sayfalarını görüntüleyecektir.",
"If selected, the route will be displayed in the menu.": "Seçildiğinde, yol menüde görüntülenecektir.",
"Are you sure you want to hide this tab?": "Bu sekmeyi gizlemek istediğinizden emin misiniz?",
- "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Gizlendikten sonra, bu sekme artık sekme çubuğunda görünmeyecek. Onu tekrar göstermek için, rotayı yönetim sayfasına gidip ayarlamanız gerekiyor."
+ "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Gizlendikten sonra, bu sekme artık sekme çubuğunda görünmeyecek. Onu tekrar göstermek için, rotayı yönetim sayfasına gidip ayarlamanız gerekiyor.",
+ "Deprecated": "Kullanımdan kaldırıldı",
+ "Full permissions": "Tüm izinler",
+ "No pages yet, please configure first": "Henüz sayfa yok, lütfen önce yapılandırın",
+ "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Kullanıcı arayüzü düzenleyici moduna girmek için sağ üst köşedeki \"Kullanıcı Arayüzü Düzenleyici\" simgesine tıklayın",
+ "Refresh data blocks": "Yenile veri blokları",
+ "Select data blocks to refresh": "Veri bloklarını yenilemek için seçin",
+ "After successful submission, the selected data blocks will be automatically refreshed.": "Başarılı bir şekilde gönderildikten sonra, seçilen veri blokları otomatik olarak yenilenecektir."
}
diff --git a/packages/core/client/src/locale/uk-UA.json b/packages/core/client/src/locale/uk-UA.json
index c381200d65..8f698134ba 100644
--- a/packages/core/client/src/locale/uk-UA.json
+++ b/packages/core/client/src/locale/uk-UA.json
@@ -159,6 +159,7 @@
"Chart type": "Тип діаграми",
"Chart config": "Налаштування діаграми",
"Templates": "Шаблони",
+ "Template": "Шаблон",
"Select template": "Вибрати шаблон",
"Action logs": "Журнал дій",
"Create template": "Створити шаблон",
@@ -486,7 +487,8 @@
"Turn pages": "Переключати сторінки",
"Others": "Інші",
"Other records": "Інші записи",
- "Save as template": "Зберегти як шаблон",
+ "Save as reference template": "Зберегти як шаблон посилання",
+ "Save as inherited template": "Зберегти як шаблон нащадка",
"Save as block template": "Зберегти як шаблон блока",
"Block templates": "Шаблони блоків",
"Block template": "Шаблон блока",
@@ -576,6 +578,7 @@
"Blank block": "Порожній блок",
"Duplicate template": "Дублювати шаблон",
"Reference template": "Посилання на шаблон",
+ "Inherited template": "Нащадковий шаблон",
"Create calendar block": "Створити блок календаря",
"Create kanban block": "Створити блок канбану",
"Grouping field": "Поле для групування",
@@ -821,5 +824,12 @@
"If selected, the page will display Tab pages.": "Якщо вибрано, сторінка відобразить сторінки з вкладками.",
"If selected, the route will be displayed in the menu.": "Якщо вибрано, маршрут буде відображений в меню.",
"Are you sure you want to hide this tab?": "Ви впевнені, що хочете приховати цю вкладку?",
- "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Після приховування цієї вкладки вона більше не з'явиться в панелі вкладок. Щоб знову показати її, вам потрібно перейти на сторінку керування маршрутами, щоб налаштувати її."
+ "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Після приховування цієї вкладки вона більше не з'явиться в панелі вкладок. Щоб знову показати її, вам потрібно перейти на сторінку керування маршрутами, щоб налаштувати її.",
+ "Deprecated": "Застаріло",
+ "Full permissions": "Повні права",
+ "No pages yet, please configure first": "Ще немає сторінок, будь ласка, спочатку налаштуйте",
+ "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Натисніть на значок \"Редактор користувацького інтерфейсу\" в правому верхньому куті, щоб увійти в режим редактора користувацького інтерфейсу.",
+ "Refresh data blocks": "Оновити дані блоків",
+ "Select data blocks to refresh": "Виберіть блоки даних для оновлення",
+ "After successful submission, the selected data blocks will be automatically refreshed.": "Після успішної подачі вибрані блоки даних будуть автоматично оновлені."
}
diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json
index ee7937d066..f2b35eee88 100644
--- a/packages/core/client/src/locale/zh-CN.json
+++ b/packages/core/client/src/locale/zh-CN.json
@@ -167,6 +167,8 @@
"Year": "年",
"QuarterYear": "季度",
"Select grouping field": "选择分组字段",
+ "Refresh data blocks": "刷新数据区块",
+ "Select data blocks to refresh": "选择要刷新的数据区块",
"Media": "多媒体",
"Markdown": "Markdown",
"Wysiwyg": "富文本",
@@ -183,6 +185,7 @@
"Chart type": "图表类型",
"Chart config": "图表配置",
"Templates": "模板",
+ "Template": "模板",
"Select template": "选择模板",
"Action logs": "操作日志",
"Create template": "创建模板",
@@ -258,6 +261,7 @@
"Parent collection fields": "父表字段",
"Basic": "基本类型",
"Single line text": "单行文本",
+ "Automatically remove heading and tailing spaces": "自动去除首尾空白字符",
"Long text": "多行文本",
"Phone": "手机号码",
"Email": "电子邮箱",
@@ -523,7 +527,8 @@
"Turn pages": "翻页",
"Others": "其他",
"Other records": "其他记录",
- "Save as template": "保存为模板",
+ "Save as reference template": "保存为引用模板",
+ "Save as inherited template": "保存为继承模板",
"Save as block template": "保存为区块模板",
"Block templates": "区块模板",
"Block template": "区块模板",
@@ -607,6 +612,7 @@
"Blank block": "空区块",
"Duplicate template": "复制模板",
"Reference template": "引用模板",
+ "Inherited template": "继承模板",
"Create calendar block": "创建日历区块",
"Create kanban block": "创建看板区块",
"Grouping field": "分组字段",
@@ -817,7 +823,8 @@
"File size should not exceed {{size}}.": "文件大小不能超过 {{size}}",
"File size exceeds the limit": "文件大小超过限制",
"File type is not allowed": "文件类型不允许",
- "Incomplete uploading files need to be resolved": "未完成上传的文件需要处理",
+ "Uploading": "上传中",
+ "Some files are not uploaded correctly, please check.": "部分文件未上传成功,请检查。",
"Default title for each record": "用作数据的默认标题",
"If collection inherits, choose inherited collections as templates": "当前表有继承关系时,可选择继承链路上的表作为模板来源",
"Select an existing piece of data as the initialization data for the form": "选择一条已有的数据作为表单的初始化数据",
@@ -837,6 +844,7 @@
"The will interrupt service, it may take a few seconds to restart. Are you sure to continue?": "重启将会中断当前服务,这个过程可能需要一点时间,确定要继续吗?",
"Restart": "重启",
"Clear cache": "清除缓存",
+ "Are you sure you want to clear cache ?": "你确定你想清除缓存吗",
"Duplicate": "复制",
"Duplicating": "复制中",
"Duplicate mode": "复制方式",
@@ -1080,5 +1088,23 @@
"If selected, the page will display Tab pages.": "如果选中,该页面将显示标签页。",
"If selected, the route will be displayed in the menu.": "如果选中,该路由将显示在菜单中。",
"Are you sure you want to hide this tab?": "你确定要隐藏该标签页吗?",
- "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "隐藏后,该标签将不再显示在标签栏中。要想再次显示它,你需要到路由管理页面进行设置。"
+ "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "隐藏后,该标签将不再显示在标签栏中。要想再次显示它,你需要到路由管理页面进行设置。",
+ "Deprecated": "已弃用",
+ "Full permissions": "全部权限",
+ "Enable index column": "启用序号列",
+ "Date scope": "日期范围",
+ "Icon only": "仅显示图标",
+ "Valid range: 100-900": "有效范围:100-900",
+ "Valid range: 10-40": "有效范围:10-40",
+ "Font Size(px)": "字体大小(像素)",
+ "Font Weight": "字体粗细",
+ "Font Style": "字体样式",
+ "Italic": "斜体",
+ "Response record":"响应结果记录",
+ "Colon":"冒号",
+ "No pages yet, please configure first": "暂无页面,请先配置",
+ "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "点击右上角的“界面配置”图标,进入界面配置模式",
+ "After successful submission, the selected data blocks will be automatically refreshed.": "提交成功后,会自动刷新这里选中的数据区块。",
+ "Block Linkage rules":"区块联动规则",
+ "Field Linkage rules":"字段联动规则"
}
diff --git a/packages/core/client/src/locale/zh-TW.json b/packages/core/client/src/locale/zh-TW.json
index fd825c88cf..01c31a2a1d 100644
--- a/packages/core/client/src/locale/zh-TW.json
+++ b/packages/core/client/src/locale/zh-TW.json
@@ -183,6 +183,7 @@
"Chart type": "圖表型別",
"Chart config": "圖表設定",
"Templates": "模板",
+ "Template": "模板",
"Select template": "選擇模板",
"Action logs": "動作日誌",
"Create template": "建立模板",
@@ -517,7 +518,8 @@
"Turn pages": "翻頁",
"Others": "其他",
"Other records": "其他記錄",
- "Save as template": "儲存為模板",
+ "Save as reference template": "儲存為引用模板",
+ "Save as inherited template": "儲存為繼承模板",
"Save as block template": "儲存為區塊模板",
"Block templates": "區塊模板",
"Block template": "區塊模板",
@@ -601,6 +603,7 @@
"Blank block": "空區塊",
"Duplicate template": "複製模板",
"Reference template": "引用模板",
+ "Inherited template": "繼承模板",
"Create calendar block": "建立日曆區塊",
"Create kanban block": "建立看板區塊",
"Grouping field": "群組欄位",
@@ -912,6 +915,12 @@
"If selected, the page will display Tab pages.": "如果選中,該頁面將顯示標籤頁。",
"If selected, the route will be displayed in the menu.": "如果選中,該路由將顯示在菜單中。",
"Are you sure you want to hide this tab?": "你確定要隱藏這個標籤嗎?",
- "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "隱藏後,這個標籤將不再出現在標籤欄中。要再次顯示它,你需要到路由管理頁面進行設置。"
+ "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "隱藏後,這個標籤將不再出現在標籤欄中。要再次顯示它,你需要到路由管理頁面進行設置。",
+ "Deprecated": "已棄用",
+ "Full permissions": "完全權限",
+ "No pages yet, please configure first": "尚未配置頁面,請先配置",
+ "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "點擊右上角的 \"介面設定\" 圖示進入介面設定模式",
+ "Refresh data blocks": "刷新數據區塊",
+ "Select data blocks to refresh": "選擇要刷新的數據區塊",
+ "After successful submission, the selected data blocks will be automatically refreshed.": "提交成功後,選中的數據區塊將自動刷新。"
}
-
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 777b9bb5c1..3a45772323 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
@@ -28,7 +28,7 @@ test.describe('bulk-destroy', () => {
// 3. 点击批量删除按钮,Table 显示无数据
await page.getByLabel('action-Action-Delete-destroy-').click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
- await expect(page.getByLabel('block-item-CardItem-general-').getByText('No data')).toBeVisible();
+ await expect(page.getByLabel('block-item-CardItem-general-').getByText('No data').last()).toBeVisible();
});
test('Secondary confirmation', async ({ page, mockPage, mockRecords }) => {
@@ -45,6 +45,7 @@ test.describe('bulk-destroy', () => {
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 expect(page.getByRole('button', { name: 'OK' })).toHaveCount(1);
await page.getByRole('button', { name: 'OK' }).click();
await page.mouse.move(500, 0);
@@ -53,6 +54,6 @@ test.describe('bulk-destroy', () => {
// 3. 点击批量删除按钮,Table 显示无数据
await page.getByLabel('action-Action-Delete-destroy-').click();
- await expect(page.getByLabel('block-item-CardItem-general-').getByText('No data')).toBeVisible();
+ await expect(page.getByLabel('block-item-CardItem-general-').getByText('No data').last()).toBeVisible();
});
});
diff --git a/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts b/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts
index ca11ae838b..0212e4abe5 100644
--- a/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts
+++ b/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts
@@ -28,10 +28,14 @@ test.describe('Link', () => {
// 2. config the Link button
await page.getByLabel('action-Action.Link-Link-customize:link-users-table-0').hover();
+ await expect(
+ page.getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:link-users' }),
+ ).toHaveCount(1);
await page.getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:link-users' }).hover();
await page.getByRole('menuitem', { name: 'Edit link' }).click();
+ await page.getByLabel('block-item-users-URL').getByLabel('textbox').click();
await page
- .getByLabel('block-item-users-table-URL')
+ .getByLabel('block-item-users-URL')
.getByLabel('textbox')
.fill(await nocoPage.getUrl());
await page.getByPlaceholder('Name').fill('id');
@@ -99,7 +103,7 @@ test.describe('Link', () => {
await page.getByLabel('action-Action.Link-Link-').hover();
await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:link-users').hover();
await page.getByRole('menuitem', { name: 'Edit link' }).click();
- await page.getByLabel('block-item-users-table-URL').getByLabel('textbox').fill(otherPageUrl);
+ await page.getByLabel('block-item-users-URL').getByLabel('textbox').fill(otherPageUrl);
await page.getByRole('button', { name: 'OK', exact: true }).click();
await page.getByLabel('action-Action.Link-Link-').click();
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 a2667087fe..02cc55f49c 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
@@ -15,7 +15,7 @@ import {
} from './templates';
test.describe('Submit: should refresh data after submit', () => {
- test('submit in reference template block', async ({ page, mockPage, clearBlockTemplates, mockRecord }) => {
+ test.skip('submit in reference template block', async ({ page, mockPage, clearBlockTemplates, mockRecord }) => {
const nocoPage = await mockPage(submitInReferenceTemplateBlock).waitForInit();
await mockRecord('collection', { nickname: 'abc' });
await nocoPage.goto();
diff --git a/packages/core/client/src/modules/actions/add-new/addNewActionSettings.tsx b/packages/core/client/src/modules/actions/add-new/addNewActionSettings.tsx
index f0e79b3eb7..886af15e42 100644
--- a/packages/core/client/src/modules/actions/add-new/addNewActionSettings.tsx
+++ b/packages/core/client/src/modules/actions/add-new/addNewActionSettings.tsx
@@ -15,6 +15,8 @@ import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/actio
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
import { SchemaSettingsEnableChildCollections } from '../../../schema-settings/SchemaSettings';
import { useOpenModeContext } from '../../popup/OpenModeProvider';
+import { SchemaSettingsLinkageRules } from '../../../schema-settings';
+import { useDataBlockProps } from '../../../data-source';
export const addNewActionSettings = new SchemaSettings({
name: 'actionSettings:addNew',
@@ -27,6 +29,16 @@ export const addNewActionSettings = new SchemaSettings({
return buttonEditorProps;
},
},
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { linkageRulesProps } = useSchemaToolbar();
+ return {
+ ...linkageRulesProps,
+ };
+ },
+ },
{
name: 'openMode',
Component: SchemaSettingOpenModeSchemaItems,
@@ -57,6 +69,21 @@ export const addNewActionSettings = new SchemaSettings({
return isChildCollectionAction;
},
},
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { association } = useDataBlockProps() || {};
+ const { name } = useCollection_deprecated();
+ const { getCollectionField } = useCollectionManager_deprecated();
+ const associationField = getCollectionField(association);
+ const { linkageRulesProps } = useSchemaToolbar();
+ return {
+ ...linkageRulesProps,
+ collectionName: associationField?.collectionName || name,
+ };
+ },
+ },
{
name: 'delete',
sort: 100,
diff --git a/packages/core/client/src/modules/actions/associate/AssociateActionProvider.tsx b/packages/core/client/src/modules/actions/associate/AssociateActionProvider.tsx
index 18be2d76b9..5e2a49c7c4 100644
--- a/packages/core/client/src/modules/actions/associate/AssociateActionProvider.tsx
+++ b/packages/core/client/src/modules/actions/associate/AssociateActionProvider.tsx
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import React, { useState, useContext } from 'react';
+import React, { useState, useContext, useEffect } from 'react';
import { RecordPickerProvider, RecordPickerContext } from '../../../schema-component/antd/record-picker';
import {
SchemaComponentOptions,
@@ -41,9 +41,16 @@ const useTableSelectorProps = () => {
export const AssociateActionProvider = (props) => {
const [selectedRows, setSelectedRows] = useState([]);
const collection = useCollection();
- const { resource, service, block, __parent } = useBlockRequestContext();
+ const { resource, block, __parent } = useBlockRequestContext();
const actionCtx = useActionContext();
const { isMobile } = useOpenModeContext() || {};
+ const [associationData, setAssociationData] = useState([]);
+ useEffect(() => {
+ resource?.list?.().then((res) => {
+ setAssociationData(res.data?.data || []);
+ });
+ }, [resource]);
+
const pickerProps = {
size: 'small',
onChange: props?.onChange,
@@ -73,8 +80,8 @@ export const AssociateActionProvider = (props) => {
};
const getFilter = () => {
const targetKey = collection?.filterTargetKey || 'id';
- if (service.data?.data) {
- const list = service.data?.data.map((option) => option[targetKey]).filter(Boolean);
+ if (associationData) {
+ const list = associationData.map((option) => option[targetKey]).filter(Boolean);
const filter = list.length ? { $and: [{ [`${targetKey}.$ne`]: list }] } : {};
return filter;
}
diff --git a/packages/core/client/src/modules/actions/associate/__e2e__/associate.test.ts b/packages/core/client/src/modules/actions/associate/__e2e__/associate.test.ts
index 5b22c812ff..6274bd3f37 100644
--- a/packages/core/client/src/modules/actions/associate/__e2e__/associate.test.ts
+++ b/packages/core/client/src/modules/actions/associate/__e2e__/associate.test.ts
@@ -29,6 +29,7 @@ test('basic', async ({ page, mockPage, mockRecord }) => {
await expect(page.getByRole('tooltip').getByText('Disassociate')).toBeVisible();
await page.getByLabel('block-item-CardItem-cc-table').hover();
+ await page.getByRole('menuitem', { name: 'Associate' }).waitFor({ state: 'detached' });
await page.getByLabel('schema-initializer-ActionBar-table:configureActions-cc').hover();
await page.getByRole('menuitem', { name: 'Associate' }).click();
//点击 associate 出现弹窗
diff --git a/packages/core/client/src/modules/actions/bulk-destroy/bulkDeleteActionSettings.tsx b/packages/core/client/src/modules/actions/bulk-destroy/bulkDeleteActionSettings.tsx
index be35280a42..8597a75659 100644
--- a/packages/core/client/src/modules/actions/bulk-destroy/bulkDeleteActionSettings.tsx
+++ b/packages/core/client/src/modules/actions/bulk-destroy/bulkDeleteActionSettings.tsx
@@ -15,6 +15,7 @@ import {
SecondConFirm,
RefreshDataBlockRequest,
} from '../../../schema-component/antd/action/Action.Designer';
+import { SchemaSettingsLinkageRules } from '../../../schema-settings';
export const bulkDeleteActionSettings = new SchemaSettings({
name: 'actionSettings:bulkDelete',
@@ -27,6 +28,16 @@ export const bulkDeleteActionSettings = new SchemaSettings({
return buttonEditorProps;
},
},
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { linkageRulesProps } = useSchemaToolbar();
+ return {
+ ...linkageRulesProps,
+ };
+ },
+ },
{
name: 'secondConFirm',
Component: SecondConFirm,
@@ -40,6 +51,16 @@ export const bulkDeleteActionSettings = new SchemaSettings({
};
},
},
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { linkageRulesProps } = useSchemaToolbar();
+ return {
+ ...linkageRulesProps,
+ };
+ },
+ },
{
name: 'remove',
sort: 100,
diff --git a/packages/core/client/src/modules/actions/disassociate/__e2e__/disassociate.test.ts b/packages/core/client/src/modules/actions/disassociate/__e2e__/disassociate.test.ts
index f9e2832325..ee224d48f6 100644
--- a/packages/core/client/src/modules/actions/disassociate/__e2e__/disassociate.test.ts
+++ b/packages/core/client/src/modules/actions/disassociate/__e2e__/disassociate.test.ts
@@ -18,7 +18,7 @@ test('basic', async ({ page, mockPage, mockRecord }) => {
await page.getByLabel('action-Action.Link-Edit record-update-collection1-table-0').click();
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Table right' }).hover();
- await page.getByRole('menuitem', { name: 'Associated records' }).hover();
+ await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'manyToMany' }).click();
// 2. Table 中显示 Role UID 字段
diff --git a/packages/core/client/src/modules/actions/disassociate/disassociateActionSettings.tsx b/packages/core/client/src/modules/actions/disassociate/disassociateActionSettings.tsx
index bfffdcbee2..1708fbc8ad 100644
--- a/packages/core/client/src/modules/actions/disassociate/disassociateActionSettings.tsx
+++ b/packages/core/client/src/modules/actions/disassociate/disassociateActionSettings.tsx
@@ -32,11 +32,9 @@ export const disassociateActionSettings = new SchemaSettings({
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
- const { name } = useCollection_deprecated();
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
- collectionName: name,
};
},
},
diff --git a/packages/core/client/src/modules/actions/expand-collapse/expendableActionSettings.tsx b/packages/core/client/src/modules/actions/expand-collapse/expendableActionSettings.tsx
index be6e5a8c9d..d8b5f45f79 100644
--- a/packages/core/client/src/modules/actions/expand-collapse/expendableActionSettings.tsx
+++ b/packages/core/client/src/modules/actions/expand-collapse/expendableActionSettings.tsx
@@ -14,7 +14,7 @@ import { useDesignable } from '../../..';
import { useSchemaToolbar } from '../../../application';
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
import { RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
-import { SchemaSettingsModalItem } from '../../../schema-settings';
+import { SchemaSettingsModalItem, SchemaSettingsLinkageRules } from '../../../schema-settings';
function ButtonEditor() {
const field = useField();
@@ -110,6 +110,17 @@ export const expendableActionSettings = new SchemaSettings({
return buttonEditorProps;
},
},
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { linkageRulesProps } = useSchemaToolbar();
+
+ return {
+ ...linkageRulesProps,
+ };
+ },
+ },
{
name: 'remove',
sort: 100,
diff --git a/packages/core/client/src/modules/actions/filter/filterActionSettings.tsx b/packages/core/client/src/modules/actions/filter/filterActionSettings.tsx
index 30e1197f13..542497f2d4 100644
--- a/packages/core/client/src/modules/actions/filter/filterActionSettings.tsx
+++ b/packages/core/client/src/modules/actions/filter/filterActionSettings.tsx
@@ -53,14 +53,33 @@ export const filterActionSettings = new SchemaSettings({
default: fieldSchema?.['x-component-props']?.icon,
'x-component-props': {},
},
+ onlyIcon: {
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Checkbox',
+ title: t('Icon only'),
+ default: fieldSchema?.['x-component-props']?.onlyIcon,
+ 'x-component-props': {},
+ 'x-reactions': [
+ {
+ dependencies: ['icon'],
+ fulfill: {
+ state: {
+ hidden: '{{!$deps[0]}}',
+ },
+ },
+ },
+ ],
+ },
},
} as ISchema,
- onSubmit: ({ title, icon }) => {
+ onSubmit: ({ title, icon, onlyIcon }) => {
fieldSchema.title = title;
field.title = title;
field.componentProps.icon = icon;
+ field.componentProps.onlyIcon = onlyIcon;
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props'].icon = icon;
+ fieldSchema['x-component-props'].onlyIcon = onlyIcon;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
diff --git a/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx b/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx
index c5a0cb87c8..a12f3abd0d 100644
--- a/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx
+++ b/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx
@@ -11,17 +11,18 @@ import { useField, useFieldSchema } from '@formily/react';
import _ from 'lodash';
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
-import { useCollectionRecord, useDesignable } from '../../../';
+import { useDesignable } from '../../../';
import { useSchemaToolbar } from '../../../application';
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
-import { useCollection_deprecated } from '../../../collection-manager';
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
+import { useCollectionManager_deprecated } from '../../../collection-manager';
import {
SchemaSettingsLinkageRules,
SchemaSettingsModalItem,
SchemaSettingAccessControl,
} from '../../../schema-settings';
import { useURLAndHTMLSchema } from './useURLAndHTMLSchema';
+import { useDataBlockProps } from '../../../data-source';
export const SchemaSettingsActionLinkItem: FC = () => {
const field = useField();
@@ -94,16 +95,13 @@ export const customizeLinkActionSettings = new SchemaSettings({
{
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
- useVisible() {
- const record = useCollectionRecord();
- return !_.isEmpty(record?.data);
- },
useComponentProps() {
- const { name } = useCollection_deprecated();
const { linkageRulesProps } = useSchemaToolbar();
+ const { association } = useDataBlockProps() || {};
+ const { getCollectionField } = useCollectionManager_deprecated();
+ const associationField = getCollectionField(association);
return {
...linkageRulesProps,
- collectionName: name,
};
},
},
diff --git a/packages/core/client/src/modules/actions/link/useURLAndHTMLSchema.tsx b/packages/core/client/src/modules/actions/link/useURLAndHTMLSchema.tsx
index c29fa72b9d..0463d50757 100644
--- a/packages/core/client/src/modules/actions/link/useURLAndHTMLSchema.tsx
+++ b/packages/core/client/src/modules/actions/link/useURLAndHTMLSchema.tsx
@@ -15,8 +15,8 @@ import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
import { useRecord } from '../../../record-provider';
import { Variable } from '../../../schema-component/antd/variable/Variable';
import { useVariableOptions } from '../../../schema-settings/VariableInput/hooks/useVariableOptions';
-
-export const getVariableComponentWithScope = (Com) => {
+import { useGlobalVariable } from '../../../application/hooks/useGlobalVariable';
+export const getVariableComponentWithScope = (Com, data = []) => {
return (props) => {
const fieldSchema = useFieldSchema();
const { form } = useFormBlockContext();
@@ -28,13 +28,26 @@ export const getVariableComponentWithScope = (Com) => {
uiSchema: fieldSchema,
noDisabled: true,
});
- return ;
+ return ;
};
};
+const useEvnVariable = () => {
+ const environmentVariables = useGlobalVariable('$env');
+ if (environmentVariables) {
+ const { children } = environmentVariables;
+ return {
+ ...environmentVariables,
+ children: children.filter((v) => v.type === 'default'),
+ };
+ }
+ return null;
+};
+
export const useURLAndHTMLSchema = () => {
const { t } = useTranslation();
- const Com = useMemo(() => getVariableComponentWithScope(Variable.TextArea), []);
+ const environmentVariables = useEvnVariable();
+ const Com = useMemo(() => getVariableComponentWithScope(Variable.TextArea, [environmentVariables] || []), []);
const urlSchema = useMemo(() => {
return {
diff --git a/packages/core/client/src/modules/actions/refresh/refreshActionSettings.tsx b/packages/core/client/src/modules/actions/refresh/refreshActionSettings.tsx
index b8b31acada..cb7c94ecb7 100644
--- a/packages/core/client/src/modules/actions/refresh/refreshActionSettings.tsx
+++ b/packages/core/client/src/modules/actions/refresh/refreshActionSettings.tsx
@@ -10,7 +10,7 @@
import { useSchemaToolbar } from '../../../application';
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
import { ButtonEditor, RemoveButton, SecondConFirm } from '../../../schema-component/antd/action/Action.Designer';
-
+import { SchemaSettingsLinkageRules } from '../../../schema-settings';
export const refreshActionSettings = new SchemaSettings({
name: 'actionSettings:refresh',
items: [
@@ -22,6 +22,17 @@ export const refreshActionSettings = new SchemaSettings({
return buttonEditorProps;
},
},
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { linkageRulesProps } = useSchemaToolbar();
+
+ return {
+ ...linkageRulesProps,
+ };
+ },
+ },
{
name: 'secondConFirm',
Component: SecondConFirm,
diff --git a/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx b/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx
index 34bcb7cdef..0c48ebfc7d 100644
--- a/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx
+++ b/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx
@@ -29,6 +29,7 @@ import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks
import { SchemaSettingsModalItem } from '../../../schema-settings/SchemaSettings';
import { useParentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider';
import { useDataBlockProps } from '../../../data-source';
+import { SchemaSettingsLinkageRules } from '../../../schema-settings';
const Tree = connect(
AntdTree,
@@ -149,6 +150,16 @@ export const createSubmitActionSettings = new SchemaSettings({
return buttonEditorProps;
},
},
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { linkageRulesProps } = useSchemaToolbar();
+ return {
+ ...linkageRulesProps,
+ };
+ },
+ },
{
name: 'secondConfirmation',
Component: SecondConFirm,
diff --git a/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx b/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx
index 560785611e..f1441344b4 100644
--- a/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx
+++ b/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx
@@ -46,10 +46,6 @@ export const updateSubmitActionSettings = new SchemaSettings({
collectionName: name,
};
},
- useVisible() {
- const fieldSchema = useFieldSchema();
- return !fieldSchema.parent['x-initializer'].includes('bulkEditForm');
- },
},
{
name: 'secondConfirmation',
diff --git a/packages/core/client/src/modules/actions/view-edit-popup/customizePopupActionSettings.tsx b/packages/core/client/src/modules/actions/view-edit-popup/customizePopupActionSettings.tsx
index 4e86e008d8..dbec03441c 100644
--- a/packages/core/client/src/modules/actions/view-edit-popup/customizePopupActionSettings.tsx
+++ b/packages/core/client/src/modules/actions/view-edit-popup/customizePopupActionSettings.tsx
@@ -6,17 +6,12 @@
* 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 { useSchemaToolbar } from '../../../application';
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
-import { useCollection_deprecated } from '../../../collection-manager';
-import { useCollection } from '../../../data-source';
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
import { SchemaSettingsLinkageRules, SchemaSettingAccessControl } from '../../../schema-settings';
import { useOpenModeContext } from '../../popup/OpenModeProvider';
-import { useCurrentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider';
export const customizePopupActionSettings = new SchemaSettings({
name: 'actionSettings:popup',
@@ -33,18 +28,11 @@ export const customizePopupActionSettings = new SchemaSettings({
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
- const { name } = useCollection_deprecated();
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
- collectionName: name,
};
},
- useVisible() {
- const { collection } = useCurrentPopupRecord() || {};
- const currentCollection = useCollection();
- return !collection || collection?.name === currentCollection?.name;
- },
},
{
name: 'openMode',
diff --git a/packages/core/client/src/modules/blocks/BlockLinkageRuleProvider.tsx b/packages/core/client/src/modules/blocks/BlockLinkageRuleProvider.tsx
new file mode 100644
index 0000000000..7bb04fcbcd
--- /dev/null
+++ b/packages/core/client/src/modules/blocks/BlockLinkageRuleProvider.tsx
@@ -0,0 +1,100 @@
+/**
+ * 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, { useMemo, useEffect, useState } from 'react';
+import { useFieldSchema, useForm } from '@formily/react';
+import { last, isEqual } from 'lodash';
+import { uid } from '@formily/shared';
+import { reaction } from '@formily/reactive';
+import { useLocalVariables, useVariables } from '../../variables';
+import { useReactiveLinkageEffect } from './utils';
+import { useDesignable } from '../../';
+import { forEachLinkageRule } from '../../schema-settings/LinkageRules/forEachLinkageRule';
+import {
+ getVariableValuesInCondition,
+ getVariableValuesInExpression,
+} from '../../schema-settings/LinkageRules/bindLinkageRulesToFiled';
+
+const getLinkageRules = (fieldSchema) => {
+ if (!fieldSchema) {
+ return [];
+ }
+ let linkageRules = fieldSchema?.['x-block-linkage-rules'] || [];
+ fieldSchema.mapProperties((schema) => {
+ if (schema['x-block-linkage-rules']) {
+ linkageRules = schema['x-block-linkage-rules'];
+ }
+ });
+ return linkageRules?.filter((k) => !k.disabled);
+};
+
+export const BlockLinkageRuleProvider = (props) => {
+ const schema = useFieldSchema();
+ const variables = useVariables();
+ const localVariables = useLocalVariables();
+ const { designable } = useDesignable();
+ const form = useForm();
+ const linkageRules = useMemo(() => getLinkageRules(schema), [schema]);
+ const [triggerLinkageUpdate, setTriggerLinkageUpdate] = useState(null);
+ const displayResult = useReactiveLinkageEffect(linkageRules, variables, localVariables, triggerLinkageUpdate);
+ const shouldCalculateFormLinkage = schema?.['x-decorator'] === 'FormItem' && !form.readPretty && linkageRules.length;
+
+ useEffect(() => {
+ if (shouldCalculateFormLinkage) {
+ const id = uid();
+ const disposes = [];
+
+ // 延迟执行,防止一开始获取到的 form.values 值是旧的
+ setTimeout(() => {
+ form.addEffects(id, () => {
+ forEachLinkageRule(linkageRules, (action, rule) => {
+ return reaction(
+ () => {
+ // 获取条件中的变量值
+ const variableValuesInCondition = getVariableValuesInCondition({ linkageRules, localVariables });
+ // 获取 value 表达式中的变量值
+ const variableValuesInExpression = getVariableValuesInExpression({ action, localVariables });
+ const result = [variableValuesInCondition, variableValuesInExpression]
+ .map((item) => JSON.stringify(item))
+ .join(',');
+ return result;
+ },
+ () => {
+ setTriggerLinkageUpdate(uid());
+ },
+ { fireImmediately: true, equals: isEqual },
+ );
+ });
+ });
+ });
+
+ // 清理副作用
+ return () => {
+ form.removeEffects(id);
+ disposes.forEach((dispose) => {
+ dispose();
+ });
+ };
+ }
+ }, [linkageRules, shouldCalculateFormLinkage]);
+ if (!linkageRules.length) {
+ return props.children;
+ }
+
+ if (displayResult === null) return null;
+ if (last(displayResult) === 'hidden') {
+ if (designable) {
+ return {props.children}
;
+ } else {
+ return null;
+ }
+ }
+
+ return props.children;
+};
diff --git a/packages/core/client/src/modules/blocks/BlockSchemaToolbar.tsx b/packages/core/client/src/modules/blocks/BlockSchemaToolbar.tsx
index 30dfacee5a..6e019f64ab 100644
--- a/packages/core/client/src/modules/blocks/BlockSchemaToolbar.tsx
+++ b/packages/core/client/src/modules/blocks/BlockSchemaToolbar.tsx
@@ -14,6 +14,7 @@ import { useCollection } from '../../data-source/collection/CollectionProvider';
import { useCompile } from '../../schema-component';
import { SchemaToolbar } from '../../schema-settings/GeneralSchemaDesigner';
import { useSchemaTemplate } from '../../schema-templates';
+import { useMobileLayout } from '../../route-switch/antd/admin-layout';
export const BlockSchemaToolbar = (props) => {
const { t } = useTranslation();
@@ -22,6 +23,7 @@ export const BlockSchemaToolbar = (props) => {
const template = useSchemaTemplate();
const { association, collection } = useDataBlockProps() || {};
const compile = useCompile();
+ const { isMobileLayout } = useMobileLayout();
if (association) {
const [collectionName] = association.split('.');
@@ -51,7 +53,7 @@ export const BlockSchemaToolbar = (props) => {
].filter(Boolean);
}, [currentCollectionTitle, currentCollectionName, associationField, associationCollection, compile, templateName]);
- return ;
+ return ;
};
export function getCollectionTitle(arg: {
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 42f32a51c8..54684025a7 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
@@ -12,6 +12,7 @@ import { oneEmptyTableWithUsers } from './templatesOfBug';
const deleteButton = async (page: Page, name: string) => {
await page.getByRole('button', { name }).hover();
+ await page.getByRole('menuitem', { name: 'Delete' }).waitFor({ state: 'detached' });
await page.getByRole('button', { name }).getByLabel('designer-schema-settings-').hover();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
@@ -31,6 +32,7 @@ test.describe('where multi data details block can be added', () => {
// 1. 打开弹窗,通过 Associated records 添加一个详情区块
await page.getByLabel('action-Action.Link-View').click();
await page.getByLabel('schema-initializer-Grid-popup').hover();
+ await page.getByRole('menuitem', { name: 'Associated records right' }).waitFor({ state: 'detached' });
await page.getByRole('menuitem', { name: 'Details right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records right' }).hover();
await page.getByRole('menuitem', { name: 'Roles' }).click();
@@ -41,6 +43,7 @@ test.describe('where multi data details block can be added', () => {
await expect(page.getByLabel('block-item-CollectionField-').getByText('admin')).toBeVisible();
// 2. 打开弹窗,通过 Other records 添加一个详情区块
+ await page.getByRole('menuitem', { name: 'Details right' }).waitFor({ state: 'detached' });
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Details right' }).hover();
await page.getByRole('menuitem', { name: 'Other records right' }).hover();
@@ -116,6 +119,7 @@ test.describe('configure actions', () => {
await page.getByText('Delete').click();
await page.mouse.move(300, 0);
+ await expect(page.getByRole('button', { name: 'Edit' })).toHaveCount(1);
await expect(page.getByRole('button', { name: 'Edit' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Delete' })).toBeVisible();
diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaSettings.test.ts
index 8dfeba7b33..3557653105 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaSettings.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaSettings.test.ts
@@ -33,7 +33,7 @@ test.describe('multi data details block schema settings', () => {
'Linkage rules',
'Set the data scope',
'Set default sorting rules',
- 'Save as template',
+ // 'Save as template',
'Delete',
],
});
@@ -45,7 +45,7 @@ test.describe('multi data details block schema settings', () => {
// 禁用规则,联动规则失效
await page.getByLabel('block-item-CardItem-users-').hover();
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:detailsWithPagination-users').hover();
- await page.getByText('Linkage rules').click();
+ await page.getByText('Field Linkage rules').click();
await page.getByRole('switch', { name: 'On Off' }).click();
await page.getByRole('button', { name: 'OK' }).click();
await page.reload();
@@ -76,6 +76,7 @@ test.describe('actions schema settings', () => {
await expectSettingsMenu({
page,
showMenu: async () => {
+ await expect(page.getByRole('button', { name: 'Edit' })).toHaveCount(1);
await page.getByRole('button', { name: 'Edit' }).hover();
await page.getByRole('button', { name: 'designer-schema-settings-Action' }).hover();
},
diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/setDataLoadingModeSettingsItem.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/setDataLoadingModeSettingsItem.test.ts
index eaa98da272..3402c78d39 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/setDataLoadingModeSettingsItem.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/setDataLoadingModeSettingsItem.test.ts
@@ -50,10 +50,10 @@ test.describe('setDataLoadingModeSettingsItem', () => {
await page.getByRole('button', { name: 'OK', exact: true }).click();
// 所有区块应该显示 No data
- await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible();
- await expect(page.getByLabel('block-item-CardItem-users-details').getByText('No data')).toBeVisible();
- await expect(page.getByLabel('block-item-CardItem-users-list').getByText('No data')).toBeVisible();
- await expect(page.getByLabel('block-item-BlockItem-users-').getByText('No data')).toBeVisible();
+ await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible();
+ await expect(page.getByLabel('block-item-CardItem-users-details').getByText('No data').last()).toBeVisible();
+ await expect(page.getByLabel('block-item-CardItem-users-list').getByText('No data').last()).toBeVisible();
+ await expect(page.getByLabel('block-item-BlockItem-users-').getByText('No data').last()).toBeVisible();
// 3. 在筛选表单中数据一个筛选条件,点击筛选按钮,区块内应该显示数据
await page.getByLabel('block-item-CollectionField-').getByRole('textbox').click();
@@ -67,10 +67,10 @@ test.describe('setDataLoadingModeSettingsItem', () => {
// 4. 点击筛选表单的 Reset 按钮,区块内应该显示 No data
await page.getByLabel('action-Action-Reset to empty-users-').click();
- await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible();
- await expect(page.getByLabel('block-item-CardItem-users-details').getByText('No data')).toBeVisible();
- await expect(page.getByLabel('block-item-CardItem-users-list').getByText('No data')).toBeVisible();
- await expect(page.getByLabel('block-item-BlockItem-users-').getByText('No data')).toBeVisible();
+ await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible();
+ await expect(page.getByLabel('block-item-CardItem-users-details').getByText('No data').last()).toBeVisible();
+ await expect(page.getByLabel('block-item-CardItem-users-list').getByText('No data').last()).toBeVisible();
+ await expect(page.getByLabel('block-item-BlockItem-users-').getByText('No data').last()).toBeVisible();
});
test('When the data block has data scope settings and dataLoadingMode is manual, data should not be displayed after the first page load', async ({
@@ -78,7 +78,7 @@ test.describe('setDataLoadingModeSettingsItem', () => {
mockPage,
}) => {
await mockPage(TableBlockWithDataScope).goto();
- await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible();
+ await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible();
// 此时点击 filter 按钮,应该还是没数据,因为表单没有值
await page.getByLabel('action-Action-Filter-submit-').click({
@@ -87,7 +87,7 @@ test.describe('setDataLoadingModeSettingsItem', () => {
y: 10,
},
});
- await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible();
+ await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible();
// 点击 Reset 按钮,也是一样
await page.getByLabel('action-Action-Reset-users-').click({
@@ -96,6 +96,6 @@ test.describe('setDataLoadingModeSettingsItem', () => {
y: 10,
},
});
- await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible();
+ await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible();
});
});
diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-multi/detailsWithPaginationSettings.tsx b/packages/core/client/src/modules/blocks/data-blocks/details-multi/detailsWithPaginationSettings.tsx
index 9653bd2d82..35363ab525 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/details-multi/detailsWithPaginationSettings.tsx
+++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/detailsWithPaginationSettings.tsx
@@ -14,7 +14,7 @@ import { SchemaSettings } from '../../../../application/schema-settings/SchemaSe
import { SchemaSettingsItemType } from '../../../../application/schema-settings/types';
import { useDetailsBlockContext } from '../../../../block-provider/DetailsBlockProvider';
import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider';
-import { useCollection_deprecated, useSortFields } from '../../../../collection-manager';
+import { useSortFields } from '../../../../collection-manager';
import { removeNullCondition, useDesignable } from '../../../../schema-component';
import { SchemaSettingsLinkageRules } from '../../../../schema-settings';
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
@@ -24,6 +24,8 @@ import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettin
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { setDataLoadingModeSettingsItem } from './setDataLoadingModeSettingsItem';
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
+import { LinkageRuleCategory } from '../../../../schema-settings/LinkageRules/type';
+import { useCollection } from '../../../../data-source';
const commonItems: SchemaSettingsItemType[] = [
{
@@ -35,13 +37,28 @@ const commonItems: SchemaSettingsItemType[] = [
Component: SchemaSettingsBlockHeightItem,
},
{
- name: 'linkageRules',
+ name: 'fieldLinkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
- const { name } = useCollection_deprecated();
+ const { name } = useCollection();
+ const { t } = useTranslation();
return {
collectionName: name,
readPretty: true,
+ title: t('Field Linkage rules'),
+ };
+ },
+ },
+ {
+ name: 'blockLinkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { name } = useCollection();
+ const { t } = useTranslation();
+ return {
+ collectionName: name,
+ title: t('Block Linkage rules'),
+ category: LinkageRuleCategory.block,
};
},
},
@@ -49,7 +66,7 @@ const commonItems: SchemaSettingsItemType[] = [
name: 'dataScope',
Component: SchemaSettingsDataScope,
useComponentProps() {
- const { name } = useCollection_deprecated();
+ const { name } = useCollection();
const fieldSchema = useFieldSchema();
const { form } = useFormBlockContext();
const field = useField();
@@ -83,7 +100,7 @@ const commonItems: SchemaSettingsItemType[] = [
name: 'sortingRules',
type: 'modal',
useComponentProps() {
- const { name } = useCollection_deprecated();
+ const { name } = useCollection();
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const field = useField();
@@ -201,7 +218,7 @@ const commonItems: SchemaSettingsItemType[] = [
name: 'template',
Component: SchemaSettingsTemplate,
useComponentProps() {
- const { name } = useCollection_deprecated();
+ const { name } = useCollection();
const fieldSchema = useFieldSchema();
const { componentNamePrefix } = useBlockTemplateContext();
const defaultResource =
diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts
index 990305dba6..97c246cfcc 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts
@@ -26,7 +26,7 @@ test.describe('where single data details block can be added', () => {
});
// https://nocobase.height.app/T-3848/description
- test('popup opened by clicking on the button for the relationship field', async ({
+ test.skip('popup opened by clicking on the button for the relationship field', async ({
page,
mockPage,
mockRecord,
@@ -69,7 +69,7 @@ test.describe('where single data details block can be added', () => {
// 3.通过 Associated records 创建一个详情区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Details right' }).hover();
- await page.getByRole('menuitem', { name: 'Associated records' }).hover();
+ await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'manyToOne' }).hover();
await page.getByRole('menuitem', { name: 'Blank block' }).click();
await page.mouse.move(300, 0);
@@ -82,7 +82,7 @@ test.describe('where single data details block can be added', () => {
// 4.通过 Associated records 创建一个详情区块,使用模板
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Details right' }).hover();
- await page.getByRole('menuitem', { name: 'Associated records' }).hover();
+ await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'manyToOne' }).hover();
await page.getByRole('menuitem', { name: 'Duplicate template' }).hover();
await page.getByRole('menuitem', { name: 'example_Details (Fields only)' }).click();
diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaSettings.test.ts
index 4a9cbda2ca..99bed01c67 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaSettings.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaSettings.test.ts
@@ -24,7 +24,7 @@ test.describe('single details block schema settings', () => {
await page.getByLabel('block-item-CardItem-general-form').hover();
await page.getByLabel('designer-schema-settings-CardItem-FormV2.ReadPrettyDesigner-general').hover();
},
- supportedOptions: ['Edit block title', 'Linkage rules', 'Save as block template', 'Delete'],
+ supportedOptions: ['Edit block title', 'Linkage rules', 'Delete'],
});
});
});
diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/detailsBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/detailsBlockSettings.ts
index 6d7f136c3c..e15933e215 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/details-single/detailsBlockSettings.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/detailsBlockSettings.ts
@@ -8,14 +8,17 @@
*/
import { useFieldSchema } from '@formily/react';
+import { useTranslation } from 'react-i18next';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { SchemaSettingsItemType } from '../../../../application/schema-settings/types';
import { useCollection } from '../../../../data-source/collection/CollectionProvider';
+import { useCollection_deprecated } from '../../../../collection-manager';
import { SchemaSettingsFormItemTemplate, SchemaSettingsLinkageRules } from '../../../../schema-settings';
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
+import { LinkageRuleCategory } from '../../../../schema-settings/LinkageRules/type';
const commonItems: SchemaSettingsItemType[] = [
{
@@ -27,13 +30,28 @@ const commonItems: SchemaSettingsItemType[] = [
Component: SchemaSettingsBlockHeightItem,
},
{
- name: 'linkageRules',
+ name: 'fieldLinkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { name } = useCollection();
+ const { t } = useTranslation();
return {
collectionName: name,
readPretty: true,
+ title: t('Field Linkage rules'),
+ };
+ },
+ },
+ {
+ name: 'blockLinkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { name } = useCollection_deprecated();
+ const { t } = useTranslation();
+ return {
+ collectionName: name,
+ title: t('Block Linkage rules'),
+ category: LinkageRuleCategory.block,
};
},
},
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 9eec7020d5..c806156f37 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
@@ -60,10 +60,10 @@ test.describe('configure fields', () => {
await expect(page.getByRole('menuitem', { name: 'ID', exact: true }).getByRole('switch')).toBeChecked();
// add association fields
- await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover();
+ await page.getByRole('menuitem', { name: 'Many to one right' }).hover();
await page.getByRole('menuitem', { name: 'Nickname' }).click();
- await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover();
+ await page.getByRole('menuitem', { name: 'Many to one right' }).hover();
await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).toBeChecked();
await page.mouse.move(300, 0);
@@ -72,13 +72,14 @@ test.describe('configure fields', () => {
// delete fields
await page.getByLabel('schema-initializer-Grid-form:configureFields-general').hover();
+ await expect(page.getByRole('menuitem', { name: 'ID', exact: true })).toHaveCount(1);
await page.getByRole('menuitem', { name: 'ID', exact: true }).click();
await expect(page.getByRole('menuitem', { name: 'ID', exact: true }).getByRole('switch')).not.toBeChecked();
- await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover();
+ await page.getByRole('menuitem', { name: 'Many to one right' }).hover();
await page.getByRole('menuitem', { name: 'Nickname' }).click();
- await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover();
+ await page.getByRole('menuitem', { name: 'Many to one right' }).hover();
await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).not.toBeChecked();
await page.mouse.move(300, 0);
@@ -119,6 +120,7 @@ test.describe('configure actions', () => {
// add button
await page.getByRole('menuitem', { name: 'Submit' }).click();
await page.mouse.move(300, 0);
+ await expect(page.getByRole('button', { name: 'Submit' })).toHaveCount(1);
await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();
// delete button
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 e210a3172e..56459afec6 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
@@ -279,7 +279,7 @@ test.describe('set default value', () => {
await page.getByRole('button', { name: 'OK', exact: true }).click();
// 2. 设置的 ‘abcd’ 应该立即显示在 Nickname 字段的输入框中
- await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('abcd');
+ await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox').last()).toHaveValue('abcd');
});
test('Current popup record', async ({ page, mockPage }) => {
@@ -310,7 +310,7 @@ test.describe('set default value', () => {
// 设置联动规则
await page.getByLabel('block-item-CardItem-users-form').hover();
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:createForm-users').hover();
- await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
+ await page.getByRole('menuitem', { name: 'Field linkage rules' }).click();
await page.mouse.move(300, 0);
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.getByText('Add property').click();
@@ -438,7 +438,7 @@ test.describe('set default value', () => {
// 设置联动规则
await page.getByLabel('block-item-CardItem-users-form').hover();
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:createForm-users').hover();
- await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
+ await page.getByRole('menuitem', { name: 'Field linkage rules' }).click();
await page.mouse.move(300, 0);
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.getByText('Add property').click();
@@ -563,7 +563,7 @@ test.describe('set default value', () => {
// 设置联动规则
await page.getByLabel('block-item-CardItem-users-form').hover();
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:createForm-users').hover();
- await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
+ await page.getByRole('menuitem', { name: 'Field linkage rules' }).click();
await page.mouse.move(300, 0);
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.getByText('Add property').click();
@@ -701,7 +701,7 @@ test.describe('set default value', () => {
// 设置联动规则
await page.getByLabel('block-item-CardItem-users-form').hover();
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:createForm-users').hover();
- await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
+ await page.getByRole('menuitem', { name: 'Field linkage rules' }).click();
await page.mouse.move(300, 0);
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.getByText('Add property').click();
diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings1.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings1.test.ts
index a822b97936..31e53ebdce 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings1.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings1.test.ts
@@ -51,7 +51,8 @@ test.describe('creation form block schema settings', () => {
await runExpect();
});
- test('Save as block template & convert reference to duplicate', async ({ page, mockPage }) => {
+ // deprecated
+ test.skip('Save as block template & convert reference to duplicate', async ({ page, mockPage }) => {
await mockPage(oneTableBlockWithActionsAndFormBlocks).goto();
await page.getByRole('button', { name: 'Add new' }).click();
@@ -115,7 +116,7 @@ test.describe('creation form block schema settings', () => {
await expect(page.getByLabel('block-item-CardItem-general-form')).not.toBeVisible();
});
- test('save as block Template', async ({ page, mockPage }) => {
+ test.skip('save as block Template', async ({ page, mockPage }) => {
await mockPage(oneEmptyForm).goto();
// 先保存为模板 ------------------------------------------------------------------------
@@ -247,7 +248,7 @@ test.describe('creation form block schema settings', () => {
// 重新选择一下数据,字段值才会被填充
// TODO: 保存后,数据应该直接被填充上
- await page.getByLabel('icon-close-select').click();
+ await page.getByLabel('icon-close-select').last().click();
await page.getByTestId('select-object-single').click();
await page.getByRole('option', { name: '2' }).click();
@@ -270,7 +271,7 @@ test.describe('creation form block schema settings', () => {
});
});
- test('save block template & using block template', async ({ page, mockPage, clearBlockTemplates }) => {
+ test.skip('save block template & using block template', async ({ page, mockPage, clearBlockTemplates }) => {
// 确保测试结束后已保存的模板会被清空
await clearBlockTemplates();
const nocoPage = await mockPage({
diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings2.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings2.test.ts
index 32be64963f..bc0708f2b6 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings2.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings2.test.ts
@@ -35,11 +35,16 @@ test.describe('linkage rules', () => {
// 条件:singleLineText 字段的值包含 123 时
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.getByText('Add condition', { exact: true }).click();
- await page.getByTestId('select-filter-field').click();
- await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).click();
- await page.getByLabel('Linkage rules').locator('input[type="text"]').click();
- await page.getByLabel('Linkage rules').locator('input[type="text"]').fill('123');
+ await page.getByLabel('variable-button').first().click();
+ await page.getByText('Current form').last().click();
+ await page.getByText('Current form').last().click();
+ await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).locator('div').click();
+
+ // await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).click();
+ await page.getByTestId('right-filter-field').getByRole('textbox').click();
+ await page.getByTestId('right-filter-field').getByRole('textbox').fill('123');
+ await page.getByRole('tabpanel').getByRole('textbox').last().fill('123');
// action:禁用 longText 字段
await page.getByText('Add property').click();
await page.getByTestId('select-linkage-property-field').click();
@@ -81,7 +86,7 @@ test.describe('linkage rules', () => {
// 修改第一组规则,使其条件中包含一个变量 --------------------------------------------------------------------------
// 当 singleLineText 字段的值包含 longText 字段的值时,禁用 longText 字段
await openLinkageRules();
- await page.getByLabel('variable-button').click();
+ await page.getByLabel('variable-button').last().click();
await expectSupportedVariables(page, [
'Constant',
'Current user',
@@ -128,11 +133,21 @@ test.describe('linkage rules', () => {
// 增加一条规则:当 number 字段的值等于 123 时
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
- await page.locator('.ant-collapse-header').nth(1).getByRole('img', { name: 'right' }).click();
+ await page.locator('.ant-collapse-header .ant-collapse-expand-icon').nth(1).click();
+
+ await page
+ .getByLabel('Linkage rules')
+ .getByRole('tabpanel')
+ .getByText('Add condition', { exact: true })
+ .last()
+ .click();
+ // await page.getByRole('button', { name: 'Select field' }).click();
+
+ await page.getByTestId('left-filter-field').getByLabel('variable-button').last().click();
+ await page.getByText('Current form').last().click();
+ await page.getByText('Current form').last().click();
+ await page.getByRole('menuitemcheckbox', { name: 'number' }).locator('div').click();
- await page.getByLabel('Linkage rules').getByRole('tabpanel').getByText('Add condition', { exact: true }).click();
- await page.getByRole('button', { name: 'Select field' }).click();
- await page.getByRole('menuitemcheckbox', { name: 'number' }).click();
await page.getByLabel('Linkage rules').getByRole('spinbutton').click();
await page.getByLabel('Linkage rules').getByRole('spinbutton').fill('123');
@@ -146,19 +161,19 @@ test.describe('linkage rules', () => {
// action: 为 longText 字段赋上常量值
await page.getByLabel('Linkage rules').getByRole('tabpanel').getByText('Add property').click();
await page.getByRole('button', { name: 'Select field' }).click();
- await page.getByRole('tree').getByText('longText').click();
+ await page.getByRole('tree').getByText('longText').last().click();
await page.getByRole('button', { name: 'action', exact: true }).click();
- await page.getByRole('option', { name: 'Value', exact: true }).click();
+ await page.getByRole('option', { name: 'Value', exact: true }).last().click();
await page.getByLabel('dynamic-component-linkage-rules').getByRole('textbox').fill('456');
// action: 为 integer 字段附上一个表达式,使其值等于 number 字段的值
await page.getByLabel('Linkage rules').getByRole('tabpanel').getByText('Add property').click();
await page.getByRole('button', { name: 'Select field' }).click();
- await page.getByRole('tree').getByText('integer').click();
+ await page.getByRole('tree').getByText('integer').last().click();
await page.getByRole('button', { name: 'action', exact: true }).click();
- await page.getByRole('option', { name: 'Value', exact: true }).click();
- await page.getByTestId('select-linkage-value-type').nth(1).click();
+ await page.getByRole('option', { name: 'Value', exact: true }).last().click();
+ await page.getByTestId('select-linkage-value-type').last().click();
await page.getByText('Expression').click();
await page.getByText('xSelect a variable').click();
@@ -236,7 +251,7 @@ test.describe('linkage rules', () => {
});
// https://nocobase.height.app/T-3806
- test('after save as block template', async ({ page, mockPage }) => {
+ test.skip('after save as block template', async ({ page, mockPage }) => {
await mockPage(T3806).goto();
// 1. 一开始联动规则应该正常
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 d4d5c94351..5dcdb7979d 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
@@ -33,6 +33,7 @@ test.describe('bulk edit form', () => {
await expect(page.getByLabel('block-item-BulkEditField-').getByText('*')).toBeVisible();
// 3. 输入值,点击提交
+ await expect(page.getByLabel('block-item-BulkEditField-').getByRole('textbox')).toHaveCount(1);
await page.getByLabel('block-item-BulkEditField-').getByRole('textbox').fill('123');
await page.getByRole('button', { name: 'Submit' }).click();
@@ -65,6 +66,7 @@ test.describe('bulk edit form', () => {
await expect(page.getByLabel('block-item-BulkEditField-').getByText('*')).toBeVisible();
// 4. 点击提交按钮,应该提示一个错误
+ await expect(page.getByRole('button', { name: 'Submit' })).toHaveCount(1);
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByLabel('block-item-BulkEditField-').getByText('The field value is required')).toBeVisible();
diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/deprecatedVariables.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/deprecatedVariables.test.ts
index 8e27ae035c..4c86cc9422 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/deprecatedVariables.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/deprecatedVariables.test.ts
@@ -18,11 +18,11 @@ test.describe('deprecated variables', () => {
await page.getByLabel('action-Action.Link-Edit').click();
await page.getByLabel('block-item-CardItem-users-form').hover();
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:editForm-users').hover();
- await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
+ await page.getByRole('menuitem', { name: 'Field linkage rules' }).click();
await expect(page.getByLabel('variable-tag').getByText('Current record / Nickname')).toBeVisible();
// 2. 但是变量列表中是禁用状态
- await page.locator('button').filter({ hasText: /^x$/ }).click();
+ await page.locator('button').filter({ hasText: /^x$/ }).last().click();
await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).hover({ position: { x: 40, y: 12 } });
await expect(page.getByRole('tooltip', { name: 'This variable has been deprecated' })).toBeVisible();
await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toHaveClass(
@@ -34,6 +34,7 @@ test.describe('deprecated variables', () => {
// 表达式输入框也是一样
await page.getByText('xSelect a variable').click();
+ await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toHaveCount(1);
await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).hover({ position: { x: 40, y: 12 } });
await expect(page.getByRole('tooltip', { name: 'This variable has been deprecated' })).toBeVisible();
await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toHaveClass(
@@ -44,10 +45,11 @@ test.describe('deprecated variables', () => {
await page.getByLabel('Linkage rules').getByText('Linkage rules').click();
// 3. 当设置为其它变量后,再次打开,变量列表中的弃用变量不再显示
- await page.locator('button').filter({ hasText: /^x$/ }).click();
+ await page.locator('button').filter({ hasText: /^x$/ }).last().click();
+ await expect(page.getByRole('menuitemcheckbox', { name: 'Current form right' })).toHaveCount(1);
await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click();
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
- await expect(page.getByLabel('variable-tag').getByText('Current form / Nickname')).toBeVisible();
+ await expect(page.getByLabel('variable-tag').getByText('Current form / Nickname').last()).toBeVisible();
// 清空表达式
await page.getByLabel('textbox').clear();
await page.getByRole('button', { name: 'OK', exact: true }).click();
@@ -55,8 +57,8 @@ test.describe('deprecated variables', () => {
// 4. 再次打开弹窗,变量列表中的弃用变量不再显示
await page.getByLabel('block-item-CardItem-users-form').hover();
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:editForm-users').hover();
- await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
- await page.locator('button').filter({ hasText: /^x$/ }).click();
+ await page.getByRole('menuitem', { name: 'Field linkage rules' }).click();
+ await page.locator('button').filter({ hasText: /^x$/ }).last().click();
await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toBeHidden();
// 使下拉菜单消失
await page.getByLabel('Linkage rules').getByText('Linkage rules').click();
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 40ea937b17..b137ecea4f 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
@@ -26,7 +26,7 @@ test.describe('where edit form block can be added', () => {
});
// https://nocobase.height.app/T-3848/description
- test('popup opened by clicking on the button for the relationship field', async ({
+ test.skip('popup opened by clicking on the button for the relationship field', async ({
page,
mockPage,
mockRecord,
@@ -85,7 +85,7 @@ test.describe('configure fields', () => {
await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover();
await page.getByRole('menuitem', { name: 'manyToOne3' }).click();
await page.mouse.move(600, 0);
-
+ await page.reload();
await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne1-manyToOne1')).toHaveText(
`manyToOne1:${record.manyToOne1.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 10ea4f6aa6..48a0412023 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
@@ -18,6 +18,7 @@ import {
import { T3825 } from './templatesOfBug';
const clickOption = async (page: Page, optionName: string) => {
await page.getByLabel('block-item-CardItem-general-form').hover();
+ await page.getByRole('menuitem', { name: optionName }).waitFor({ state: 'detached' });
await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover();
await page.getByRole('menuitem', { name: optionName }).click();
};
@@ -84,7 +85,7 @@ test.describe('edit form block schema settings', () => {
await runExpect();
});
- test('Convert reference to duplicate & Save as block template', async ({ page, mockPage, mockRecord }) => {
+ test.skip('Convert reference to duplicate & Save as block template', async ({ page, mockPage, mockRecord }) => {
const nocoPage = await mockPage(oneTableBlockWithActionsAndFormBlocks).waitForInit();
await mockRecord('general');
await nocoPage.goto();
diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/createFormBlockSettings.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/createFormBlockSettings.tsx
index c16c806c77..e29e77446a 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/form/createFormBlockSettings.tsx
+++ b/packages/core/client/src/modules/blocks/data-blocks/form/createFormBlockSettings.tsx
@@ -8,6 +8,7 @@
*/
import { useFieldSchema } from '@formily/react';
+import { useTranslation } from 'react-i18next';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider';
import { useCollection_deprecated } from '../../../../collection-manager';
@@ -21,6 +22,7 @@ import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/Schem
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
+import { LinkageRuleCategory } from '../../../../schema-settings/LinkageRules/type';
export const createFormBlockSettings = new SchemaSettings({
name: 'blockSettings:createForm',
@@ -34,12 +36,27 @@ export const createFormBlockSettings = new SchemaSettings({
Component: SchemaSettingsBlockHeightItem,
},
{
- name: 'linkageRules',
+ name: 'fieldLinkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { name } = useCollection_deprecated();
+ const { t } = useTranslation();
return {
collectionName: name,
+ title: t('Field Linkage rules'),
+ };
+ },
+ },
+ {
+ name: 'blockLinkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { name } = useCollection_deprecated();
+ const { t } = useTranslation();
+ return {
+ collectionName: name,
+ title: t('Block Linkage rules'),
+ category: LinkageRuleCategory.block,
};
},
},
diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/editFormBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/form/editFormBlockSettings.ts
index 6b41165721..8ce416a3e6 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/form/editFormBlockSettings.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/form/editFormBlockSettings.ts
@@ -8,6 +8,7 @@
*/
import { useFieldSchema } from '@formily/react';
+import { useTranslation } from 'react-i18next';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider';
import { useCollection_deprecated } from '../../../../collection-manager';
@@ -21,6 +22,7 @@ import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/Schem
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
+import { LinkageRuleCategory } from '../../../../schema-settings/LinkageRules/type';
export const editFormBlockSettings = new SchemaSettings({
name: 'blockSettings:editForm',
@@ -34,12 +36,27 @@ export const editFormBlockSettings = new SchemaSettings({
Component: SchemaSettingsBlockHeightItem,
},
{
- name: 'linkageRules',
+ name: 'fieldLinkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { name } = useCollection_deprecated();
+ const { t } = useTranslation();
return {
collectionName: name,
+ title: t('Field Linkage rules'),
+ };
+ },
+ },
+ {
+ name: 'blockLinkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { name } = useCollection_deprecated();
+ const { t } = useTranslation();
+ return {
+ collectionName: name,
+ title: t('Block Linkage rules'),
+ category: LinkageRuleCategory.block,
};
},
},
diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockDecoratorProps.ts b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockDecoratorProps.ts
index 5eeb123e45..56e28c10a6 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockDecoratorProps.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockDecoratorProps.ts
@@ -12,9 +12,8 @@ import { useDetailsParentRecord } from '../../details-single/hooks/useDetailsDec
import { useHiddenForInherit } from './useHiddenForInherit';
export function useEditFormBlockDecoratorProps(props) {
- const params = useFormBlockParams();
+ const params = useFormBlockParams(props);
let parentRecord;
-
const { hidden } = useHiddenForInherit(props);
// association 的值是固定不变的,所以这里可以使用 hooks
@@ -31,6 +30,6 @@ export function useEditFormBlockDecoratorProps(props) {
};
}
-function useFormBlockParams() {
- return useParamsFromRecord();
+function useFormBlockParams(props) {
+ return useParamsFromRecord(props);
}
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 5582f3b32e..c8d8eab3b2 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
@@ -13,6 +13,7 @@ import { oneGridCardWithInheritFields } from './templatesOfBug';
const deleteButton = async (page: Page, name: string) => {
await page.getByRole('button', { name }).hover();
+ await page.getByRole('menuitem', { name: 'Delete' }).waitFor({ state: 'detached' });
await page.getByRole('button', { name }).getByLabel('designer-schema-settings-').hover();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
@@ -27,7 +28,7 @@ test.describe('where grid card block can be added', () => {
await expect(page.getByLabel('block-item-BlockItem-users-grid-card')).toBeVisible();
});
- test('popup', async ({ page, mockPage }) => {
+ test.skip('popup', async ({ page, mockPage }) => {
await mockPage(oneEmptyTableWithUsers).goto();
// 1. 打开弹窗,通过 Associated records 创建一个列表区块
@@ -41,12 +42,14 @@ test.describe('where grid card block can be added', () => {
await page.getByLabel('schema-initializer-Grid-').nth(1).hover();
await page.getByRole('menuitem', { name: 'Role name' }).click();
await page.mouse.move(300, 0);
+ await page.reload();
await expect(page.getByText('Root')).toBeVisible();
await expect(page.getByText('Admin')).toBeVisible();
await expect(page.getByText('Member')).toBeVisible();
// 2. 通过 Other records 创建一个列表区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
+ await page.getByRole('menuitem', { name: 'Other records right' }).waitFor({ state: 'detached' });
await page.getByRole('menuitem', { name: 'Grid Card right' }).hover();
await page.getByRole('menuitem', { name: 'Other records right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click();
@@ -151,10 +154,10 @@ test.describe('configure fields', () => {
// add association fields
await page.mouse.wheel(0, 300);
- await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover();
+ await page.getByRole('menuitem', { name: 'Many to one right' }).hover();
await page.getByRole('menuitem', { name: 'Nickname' }).click();
- await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover();
+ await page.getByRole('menuitem', { name: 'Many to one right' }).hover();
await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).toBeChecked();
await page.mouse.move(300, 0);
@@ -165,14 +168,14 @@ test.describe('configure fields', () => {
// delete fields
await formItemInitializer.hover();
- await page.getByRole('menuitem', { name: 'ID', exact: true }).click();
- await expect(page.getByRole('menuitem', { name: 'ID', exact: true }).getByRole('switch')).not.toBeChecked();
+ await page.getByRole('menuitem', { name: 'ID', exact: true }).first().click();
+ await expect(page.getByRole('menuitem', { name: 'ID', exact: true }).getByRole('switch').first()).not.toBeChecked();
await page.mouse.wheel(0, 300);
- await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover();
+ await page.getByRole('menuitem', { name: 'Many to one right' }).hover();
await page.getByRole('menuitem', { name: 'Nickname' }).click();
- await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover();
+ await page.getByRole('menuitem', { name: 'Many to one right' }).hover();
await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).not.toBeChecked();
await page.mouse.move(300, 0);
@@ -185,7 +188,7 @@ test.describe('configure fields', () => {
// add markdown
await formItemInitializer.hover();
- await page.getByRole('menuitem', { name: 'ID', exact: true }).hover();
+ await page.getByRole('menuitem', { name: 'ID', exact: true }).first().hover();
await page.mouse.wheel(0, 300);
await page.getByRole('menuitem', { name: 'Add Markdown' }).click();
diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaSettings.test.ts
index c9547186ba..cf4c79f1cb 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaSettings.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaSettings.test.ts
@@ -18,6 +18,7 @@ test.describe('grid card block schema settings', () => {
page,
showMenu: async () => {
await page.getByLabel('block-item-BlockItem-general-grid-card').hover();
+ await page.waitForTimeout(1000);
await page.getByLabel('designer-schema-settings-BlockItem-GridCard.Designer-general').hover();
},
supportedOptions: [
@@ -25,7 +26,7 @@ test.describe('grid card block schema settings', () => {
'Set the data scope',
'Set default sorting rules',
'Records per page',
- 'Save as template',
+ // 'Save as template',
'Delete',
],
});
diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardBlockSettings.ts
index 074ec0f60d..5310ce5187 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardBlockSettings.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardBlockSettings.ts
@@ -24,6 +24,8 @@ import { useBlockTemplateContext } from '../../../../schema-templates/BlockTempl
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
import { SetTheCountOfColumnsDisplayedInARow } from './SetTheCountOfColumnsDisplayedInARow';
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
+import { LinkageRuleCategory } from '../../../../schema-settings/LinkageRules/type';
+import { SchemaSettingsLinkageRules } from '../../../../schema-settings';
export const gridCardBlockSettings = new SchemaSettings({
name: 'blockSettings:gridCard',
@@ -32,6 +34,19 @@ export const gridCardBlockSettings = new SchemaSettings({
name: 'setTheBlockHeight',
Component: SchemaSettingsBlockHeightItem,
},
+ {
+ name: 'blockLinkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { name } = useCollection_deprecated();
+ const { t } = useTranslation();
+ return {
+ collectionName: name,
+ title: t('Block Linkage rules'),
+ category: LinkageRuleCategory.block,
+ };
+ },
+ },
{
name: 'SetTheCountOfColumnsDisplayedInARow',
Component: SetTheCountOfColumnsDisplayedInARow,
diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/hooks/useGridCardBlockDecoratorProps.ts b/packages/core/client/src/modules/blocks/data-blocks/grid-card/hooks/useGridCardBlockDecoratorProps.ts
index 6c70969a56..c4f4605cef 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/grid-card/hooks/useGridCardBlockDecoratorProps.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/hooks/useGridCardBlockDecoratorProps.ts
@@ -27,3 +27,11 @@ export function useGridCardBlockDecoratorProps(props) {
parseVariableLoading,
};
}
+
+export function useGridCardBlockItemProps() {
+ return {};
+}
+
+export function useGridCardBlockProps() {
+ return {};
+}
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 3c7db0668b..272e2f3007 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
@@ -12,6 +12,7 @@ import { oneEmptyTableWithUsers } from '../../details-multi/__e2e__/templatesOfB
const deleteButton = async (page: Page, name: string) => {
await page.getByRole('button', { name }).hover();
+ await page.getByRole('menuitem', { name: 'Delete' }).waitFor({ state: 'detached' });
await page.getByRole('button', { name }).getByLabel('designer-schema-settings-').hover();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
@@ -40,6 +41,7 @@ test.describe('where list block can be added', () => {
await page.getByLabel('schema-initializer-Grid-').nth(1).hover();
await page.getByRole('menuitem', { name: 'Role name' }).click();
await page.mouse.move(300, 0);
+ await page.reload();
await expect(page.getByLabel('block-item-CollectionField-').getByText('Root')).toBeVisible();
await expect(page.getByLabel('block-item-CollectionField-').getByText('Admin')).toBeVisible();
await expect(page.getByLabel('block-item-CollectionField-').getByText('Member')).toBeVisible();
@@ -71,6 +73,9 @@ test.describe('configure global actions', () => {
await page.getByRole('menuitem', { name: 'Refresh' }).click();
await page.mouse.move(300, 0);
+ await expect(page.getByRole('button', { name: 'Filter' })).toHaveCount(1);
+ await expect(page.getByRole('button', { name: 'Add new' })).toHaveCount(1);
+ await expect(page.getByRole('button', { name: 'Refresh' })).toHaveCount(1);
await expect(page.getByRole('button', { name: 'Filter' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Add new' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Refresh' })).toBeVisible();
diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaSettings.test.ts
index 1bb977926e..1561b015c7 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaSettings.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaSettings.test.ts
@@ -24,7 +24,7 @@ test.describe('list block schema settings', () => {
'Set the data scope',
'Set default sorting rules',
'Records per page',
- 'Save as template',
+ // 'Save as template',
'Delete',
],
});
diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/hooks/useListBlockDecoratorProps.ts b/packages/core/client/src/modules/blocks/data-blocks/list/hooks/useListBlockDecoratorProps.ts
index 4944874fb5..833b859966 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/list/hooks/useListBlockDecoratorProps.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/list/hooks/useListBlockDecoratorProps.ts
@@ -22,3 +22,7 @@ export function useListBlockDecoratorProps(props) {
parentRecord,
};
}
+
+export function useListBlockProps() {
+ return {};
+}
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 f664cf58e2..98e784fa77 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
@@ -22,6 +22,8 @@ import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettin
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
+import { LinkageRuleCategory } from '../../../../schema-settings/LinkageRules/type';
+import { SchemaSettingsLinkageRules } from '../../../../schema-settings';
export const listBlockSettings = new SchemaSettings({
name: 'blockSettings:list',
@@ -34,6 +36,19 @@ export const listBlockSettings = new SchemaSettings({
name: 'setTheBlockHeight',
Component: SchemaSettingsBlockHeightItem,
},
+ {
+ name: 'blockLinkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { name } = useCollection_deprecated();
+ const { t } = useTranslation();
+ return {
+ collectionName: name,
+ title: t('Block Linkage rules'),
+ category: LinkageRuleCategory.block,
+ };
+ },
+ },
{
name: 'SetTheDataScope',
Component: SchemaSettingsDataScope,
diff --git a/packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/schemaInitializer.test.ts
index 4da6804303..6cccd9cfde 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/schemaInitializer.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/schemaInitializer.test.ts
@@ -90,7 +90,7 @@ test.describe('configure actions column', () => {
// 列宽度默认为 100
await expectActionsColumnWidth(100);
- await page.getByText('Actions', { exact: true }).hover();
+ await page.getByText('Actions', { exact: true }).hover({ force: true });
await page.getByLabel('designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-users').hover();
await page.getByRole('menuitem', { name: 'Column width' }).click();
diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/TableBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/TableBlockInitializer.tsx
index bc3695f6df..73258a75e5 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/table/TableBlockInitializer.tsx
+++ b/packages/core/client/src/modules/blocks/data-blocks/table/TableBlockInitializer.tsx
@@ -61,14 +61,11 @@ export const TableBlockInitializer = ({
export const useCreateTableBlock = () => {
const { insert } = useSchemaInitializer();
- const { getCollection } = useCollectionManager_deprecated();
const createTableBlock = ({ item }) => {
- const collection = getCollection(item.name, item.dataSource);
const schema = createTableBlockUISchema({
collectionName: item.name,
dataSource: item.dataSource,
- rowKey: collection.filterTargetKey || 'id',
});
insert(schema);
};
diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/actions/linkage.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/actions/linkage.test.ts
index 78a3680d6b..0faf73f886 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/actions/linkage.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/actions/linkage.test.ts
@@ -13,22 +13,23 @@ import { T4334 } from '../templatesOfBug';
// fix https://nocobase.height.app/T-2187
test('action linkage by row data', async ({ page, mockPage }) => {
await mockPage(T4334).goto();
- const adminEditAction = page.getByLabel('action-Action.Link-Edit-update-roles-table-admin');
+ const adminEditAction = page
+ .getByLabel('action-Action.Link-Edit-update-roles-table-admin')
+ .locator('.nb-action-title');
const adminEditActionStyle = await adminEditAction.evaluate((element) => {
- const computedStyle = window.getComputedStyle(element);
+ const computedStyle = window.getComputedStyle(element.querySelector('.nb-action-title'));
return {
opacity: computedStyle.opacity,
};
});
- const rootEditAction = page.getByLabel('action-Action.Link-Edit-update-roles-table-root');
+ const rootEditAction = page.getByLabel('action-Action.Link-Edit-update-roles-table-root').locator('.nb-action-title');
const rootEditActionStyle = await rootEditAction.evaluate((element) => {
- const computedStyle = window.getComputedStyle(element);
+ const computedStyle = window.getComputedStyle(element.querySelector('.nb-action-title'));
return {
opacity: computedStyle.opacity,
// 添加其他你需要的样式属性
};
});
-
expect(adminEditActionStyle.opacity).not.toBe('0.1');
expect(rootEditActionStyle.opacity).not.toBe('1');
});
diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/blockTemplate.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/blockTemplate.test.ts
index 57837da786..bfdacbaac0 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/blockTemplate.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/blockTemplate.test.ts
@@ -10,7 +10,7 @@
import { expect, test } from '@nocobase/test/e2e';
import { ordinaryBlockTemplatesCannotBeUsedToCreateAssociationBlocksAndViceVersa } from './templatesOfBug';
-test.describe('block template', () => {
+test.skip('block template', () => {
test('Ordinary block templates cannot be used to create association blocks, and vice versa', async ({
page,
mockPage,
diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/hideColumn.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/hideColumn.test.ts
index 8a14b3db12..2dded11a76 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/hideColumn.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/hideColumn.test.ts
@@ -25,6 +25,7 @@ test.describe('hide column', () => {
// 2. Sub table: hide column
await page.getByRole('button', { name: 'Role name' }).hover();
+ await page.getByRole('menuitem', { name: 'Hide column question-circle' }).waitFor({ state: 'detached' });
await page
.getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-roles' })
.hover();
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 352afc0fdb..90b836587d 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
@@ -156,6 +156,7 @@ test.describe('configure columns', () => {
await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover();
await page.getByRole('menuitem', { name: 'manyToOne3' }).click();
await page.mouse.move(600, 0);
+ await page.reload();
// 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
@@ -194,6 +195,7 @@ test.describe('configure columns', () => {
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}`,
);
@@ -212,6 +214,7 @@ test.describe('configure columns', () => {
await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover();
await page.getByRole('menuitem', { name: 'manyToOne3' }).click();
await page.mouse.move(600, 0);
+ await page.reload();
// 2. 点击每一个关系字段,创建一个详情区块,显示 ID 字段,断言 ID 是否正确
await page
@@ -295,20 +298,21 @@ test.describe('configure actions column', () => {
await nocoPage.goto();
// add view & Edit & Delete & Duplicate ------------------------------------------------------------
- await page.getByText('Actions', { exact: true }).hover();
+ await page.getByText('Actions', { exact: true }).hover({ force: true });
await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
await page.getByRole('menuitem', { name: 'View' }).click();
- await page.getByText('Actions', { exact: true }).hover();
+ await page.getByText('Actions', { exact: true }).hover({ force: true });
await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
await page.getByRole('menuitem', { name: 'Edit' }).click();
- await page.getByText('Actions', { exact: true }).hover();
+ await page.getByText('Actions', { exact: true }).hover({ force: true });
await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
await page.getByRole('menuitem', { name: 'Delete' }).click();
+ await page.mouse.move(500, 0);
- await page.getByText('Actions', { exact: true }).hover();
- await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
+ // await page.getByText('Actions', { exact: true }).hover({ force: true });
+ // await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
await page.mouse.move(300, 0);
@@ -330,11 +334,11 @@ test.describe('configure actions column', () => {
await expect(page.getByLabel('action-Action.Link-Duplicate-duplicate-t_unp4scqamw9-table-0')).not.toBeVisible();
// add custom action ------------------------------------------------------------
- await page.getByText('Actions', { exact: true }).hover();
+ await page.getByText('Actions', { exact: true }).hover({ force: true });
await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
await page.getByRole('menuitem', { name: 'Popup' }).click();
- await page.getByText('Actions', { exact: true }).hover();
+ await page.getByText('Actions', { exact: true }).hover({ force: true });
await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
await page.getByRole('menuitem', { name: 'Update record' }).click();
@@ -351,7 +355,7 @@ test.describe('configure actions column', () => {
// 列宽度默认为 100
await expect(page.getByRole('columnheader', { name: 'Actions', exact: true })).toHaveJSProperty('offsetWidth', 100);
- await page.getByText('Actions', { exact: true }).hover();
+ await page.getByText('Actions', { exact: true }).hover({ force: true });
await page.getByLabel('designer-schema-settings-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
await page.getByRole('menuitem', { name: 'Column width' }).click();
diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer1.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer1.test.ts
index 05e7877d27..69b79e9bc6 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer1.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer1.test.ts
@@ -12,6 +12,7 @@ import { T3686, T4005 } from './templatesOfBug';
const deleteButton = async (page: Page, name: string) => {
await page.getByRole('button', { name }).hover();
+ await page.getByRole('menuitem', { name: 'Delete' }).waitFor({ state: 'detached' });
await page.getByRole('button', { name }).getByLabel('designer-schema-settings-').hover();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
@@ -37,7 +38,8 @@ test.describe('where table block can be added', () => {
// 添加当前表关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Table right' }).hover();
- await page.getByRole('menuitem', { name: 'Associated records' }).hover();
+ await page.getByRole('menuitem', { name: 'childAssociationField' }).waitFor({ state: 'detached' });
+ await page.getByRole('menuitem', { name: 'Associated records right' }).last().hover();
await page.getByRole('menuitem', { name: 'childAssociationField' }).click();
await page
.getByTestId('drawer-Action.Container-childCollection-View record')
@@ -46,9 +48,11 @@ test.describe('where table block can be added', () => {
await page.getByRole('menuitem', { name: 'childTargetText' }).click();
// 添加父表关系区块
+ await page.getByRole('menuitem', { name: 'Table right' }).waitFor({ state: 'detached' });
await page.getByLabel('schema-initializer-Grid-popup').hover();
+ await page.getByRole('menuitem', { name: 'Associated records right' }).waitFor({ state: 'detached' });
await page.getByRole('menuitem', { name: 'Table right' }).hover();
- await page.getByRole('menuitem', { name: 'Associated records' }).hover();
+ await page.getByRole('menuitem', { name: 'Associated records right' }).hover();
await page.getByRole('menuitem', { name: 'parentAssociationField' }).click();
await page.getByLabel('schema-initializer-TableV2-table:configureColumns-parentTargetCollection').hover();
await page.getByRole('menuitem', { name: 'parentTargetText' }).click();
@@ -69,6 +73,7 @@ test.describe('where table block can be added', () => {
// 通过 Other records 创建一个表格区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
+ await page.getByRole('menuitem', { name: 'Other records right' }).waitFor({ state: 'detached' });
await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.getByRole('menuitem', { name: 'Other records right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click();
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 226d3a271b..ed1dda956b 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
@@ -87,7 +87,7 @@ test.describe('actions schema settings', () => {
// 切换为 dialog
await page.getByRole('menuitem', { name: 'Open mode' }).click();
- await page.getByRole('option', { name: 'Dialog' }).click();
+ await page.getByRole('option', { name: 'Dialog' }).last().click();
await page.getByRole('button', { name: 'Add new' }).click();
await expect(page.getByTestId('modal-Action.Container-general-Add record')).toBeVisible();
@@ -97,7 +97,7 @@ test.describe('actions schema settings', () => {
await page.getByLabel('action-Action-Add new-create-').hover();
await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-general' }).hover();
await page.getByRole('menuitem', { name: 'Open mode Dialog' }).click();
- await page.getByRole('option', { name: 'Page' }).click();
+ await page.getByRole('option', { name: 'Page' }).last().click();
// 点击按钮后会跳转到一个页面
await page.getByLabel('action-Action-Add new-create-').click();
@@ -116,7 +116,7 @@ test.describe('actions schema settings', () => {
// 创建一条数据后返回,列表中应该有这条数据
await page.getByTestId('select-single').click();
- await page.getByRole('option', { name: 'option3' }).click();
+ await page.getByRole('option', { name: 'option3' }).last().click();
// 提交后会自动返回
await page.getByLabel('action-Action-Submit-submit-').click();
@@ -136,7 +136,7 @@ test.describe('actions schema settings', () => {
// 切换为 small
await page.getByRole('menuitem', { name: 'Popup size' }).click();
- await page.getByRole('option', { name: 'Small' }).click();
+ await page.getByRole('option', { name: 'Small' }).last().click();
await page.getByRole('button', { name: 'Add new' }).click();
const drawerWidth =
@@ -148,7 +148,7 @@ test.describe('actions schema settings', () => {
// 切换为 large
await showMenu(page);
await page.getByRole('menuitem', { name: 'Popup size' }).click();
- await page.getByRole('option', { name: 'Large' }).click();
+ await page.getByRole('option', { name: 'Large' }).last().click();
await page.getByRole('button', { name: 'Add new' }).click();
const drawerWidth2 =
@@ -316,7 +316,8 @@ test.describe('actions schema settings', () => {
// 添加一个条件:ID 等于 1
await page.getByText('Add condition', { exact: true }).click();
- await page.getByTestId('select-filter-field').click();
+ await page.getByTestId('left-filter-field').getByLabel('variable-button').click();
+ await page.getByText('Current record').last().click();
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
await page.getByRole('spinbutton').click();
await page.getByRole('spinbutton').fill('1');
@@ -325,7 +326,7 @@ test.describe('actions schema settings', () => {
await page.getByText('Add property').click();
await page.getByLabel('block-item-ArrayCollapse-general').click();
await page.getByTestId('select-linkage-properties').click();
- await page.getByRole('option', { name: 'Disabled' }).click();
+ await page.getByRole('option', { name: 'Disabled' }).last().click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await expect(page.getByLabel('action-Action.Link-View record-view-general-table-0')).toHaveAttribute(
@@ -336,11 +337,12 @@ test.describe('actions schema settings', () => {
// 设置第二组规则 --------------------------------------------------------------------------
await openLinkageRules();
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
- await page.locator('.ant-collapse-header').nth(1).getByRole('img', { name: 'right' }).click();
+ await page.locator('.ant-collapse-header .ant-collapse-expand-icon').nth(1).click();
// 添加一个条件:ID 等于 1
- await page.getByRole('tabpanel').getByText('Add condition', { exact: true }).click();
- await page.getByRole('button', { name: 'Select field' }).click();
+ await page.getByRole('tabpanel').getByText('Add condition', { exact: true }).last().click();
+ await page.getByTestId('left-filter-field').getByLabel('variable-button').last().click();
+ await page.getByText('Current record').last().click();
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
await page.getByRole('spinbutton').click();
await page.getByRole('spinbutton').fill('1');
@@ -348,7 +350,7 @@ test.describe('actions schema settings', () => {
// action: 使按钮可用
await page.getByRole('tabpanel').getByText('Add property').click();
await page.locator('.ant-select', { hasText: 'action' }).click();
- await page.getByRole('option', { name: 'Enabled' }).click();
+ await page.getByRole('option', { name: 'Enabled' }).last().click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
// 后面的 action 会覆盖前面的
@@ -533,7 +535,7 @@ test.describe('actions schema settings', () => {
await page.getByLabel('action-Action.Link-View').hover();
await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:view-users').hover();
await page.getByRole('menuitem', { name: 'Open mode Drawer' }).click();
- await page.getByRole('option', { name: 'Page' }).click();
+ await page.getByRole('option', { name: 'Page' }).last().click();
// 跳转到子页面后,其内容应该和弹窗中的内容一致
await page.getByLabel('action-Action.Link-View').click();
@@ -706,7 +708,7 @@ test.describe('actions schema settings', () => {
.getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:view-roles' })
.hover();
await page.getByRole('menuitem', { name: 'Open mode Drawer' }).click();
- await page.getByRole('option', { name: 'Page' }).click();
+ await page.getByRole('option', { name: 'Page' }).last().click();
// 点击按钮跳转到子页面
await page.getByLabel('action-Action.Link-View role-view-roles-table-admin').click();
@@ -902,7 +904,6 @@ test.describe('actions schema settings', () => {
await page.getByRole('menuitem', { name: 'Submit' }).click();
await page.mouse.move(300, 0);
await page.getByRole('button', { name: 'Submit' }).click();
-
await page.getByLabel('designer-schema-settings-CardItem-TableBlockDesigner-treeCollection').hover();
await page.getByRole('menuitem', { name: 'Tree table' }).click();
@@ -928,6 +929,7 @@ test.describe('actions schema settings', () => {
await page.getByLabel('schema-initializer-Grid-form:').hover();
await page.getByRole('menuitem', { name: 'Parent', exact: true }).click();
await page.mouse.move(300, 0);
+ await page.reload();
await expect(
page
.getByLabel('block-item-CollectionField-')
@@ -955,7 +957,7 @@ test.describe('actions schema settings', () => {
await page.getByLabel('action-Action.Link-Add child-').hover();
await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:addChild-treeCollection').hover();
await page.getByRole('menuitem', { name: 'Open mode Drawer' }).click();
- await page.getByRole('option', { name: 'Page' }).click();
+ await page.getByRole('option', { name: 'Page' }).last().click();
// open popup with page mode
await page.getByLabel('action-Action.Link-Add child-').click();
@@ -994,7 +996,7 @@ test.describe('table column schema settings', () => {
// 1. 关系字段下拉框中应该有数据
await page.locator('.nb-sub-table-addNew').click();
await page.getByTestId('select-object-multiple').click();
- await expect(page.getByRole('option', { name: record1.singleLineText, exact: true })).toBeVisible();
+ await expect(page.getByRole('option', { name: record1.singleLineText, exact: true }).last()).toBeVisible();
// 2. 为该关系字段设置一个数据范围后,下拉框中应该有一个匹配项
await page.getByRole('button', { name: 'manyToMany1', exact: true }).hover();
@@ -1009,7 +1011,7 @@ test.describe('table column schema settings', () => {
await page.reload();
await page.locator('.nb-sub-table-addNew').click();
await page.getByTestId('select-object-multiple').click();
- await expect(page.getByRole('option', { name: record1.singleLineText, exact: true })).toBeVisible();
+ await expect(page.getByRole('option', { name: record1.singleLineText, exact: true }).last()).toBeVisible();
});
test('fixed column', async ({ page, mockPage }) => {
diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings1.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings1.test.ts
index 39ae0df88d..f0272c2a6c 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings1.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings1.test.ts
@@ -34,7 +34,7 @@ test.describe('table block schema settings', () => {
'Set the data scope',
'Records per page',
'Connect data blocks',
- 'Save as template',
+ // 'Save as template',
'Delete',
],
});
diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings2.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings2.test.ts
index 7e3ae3d6a4..a74a136d5b 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings2.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings2.test.ts
@@ -9,8 +9,8 @@
import { expect, test } from '@nocobase/test/e2e';
-test.describe('save as template', () => {
- test('save as template, then delete it', async ({ page, mockPage, clearBlockTemplates }) => {
+test.skip('save as template', () => {
+ test.skip('save as template, then delete it', async ({ page, mockPage, clearBlockTemplates }) => {
// 1. 创建一个区块,然后保存为模板
await mockPage().goto();
await page.getByLabel('schema-initializer-Grid-page:').hover();
diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/tree/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/tree/schemaSettings.test.ts
index 7092c2c685..e2c17c112b 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/tree/schemaSettings.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/tree/schemaSettings.test.ts
@@ -25,7 +25,7 @@ test.describe('tree table block schema settings', () => {
'Set default sorting rules',
'Records per page',
'Connect data blocks',
- 'Save as template',
+ // 'Save as template',
'Delete',
],
});
diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__tests__/createTableBLockSchema.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__tests__/createTableBLockSchema.test.ts
index 07e5de14db..2c4ac7473e 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/table/__tests__/createTableBLockSchema.test.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/table/__tests__/createTableBLockSchema.test.ts
@@ -17,7 +17,7 @@ vi.mock('@formily/shared', () => {
describe('createTableBLockSchemaV2', () => {
it('should create a default table block schema with minimum options', () => {
- const options = { dataSource: 'abc', collectionName: 'users', association: 'users.roles', rowKey: 'rowKey' };
+ const options = { dataSource: 'abc', collectionName: 'users', association: 'users.roles' };
const schema = createTableBlockUISchema(options);
expect(schema).toMatchInlineSnapshot(`
@@ -85,7 +85,6 @@ describe('createTableBLockSchemaV2', () => {
"params": {
"pageSize": 20,
},
- "rowKey": "rowKey",
"showIndex": true,
},
"x-filter-targets": [],
diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/createTableBlockUISchema.ts b/packages/core/client/src/modules/blocks/data-blocks/table/createTableBlockUISchema.ts
index 2628161034..3fb14295a4 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/table/createTableBlockUISchema.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/table/createTableBlockUISchema.ts
@@ -13,10 +13,9 @@ import { uid } from '@formily/shared';
export const createTableBlockUISchema = (options: {
dataSource: string;
collectionName?: string;
- rowKey?: string;
association?: string;
}): ISchema => {
- const { collectionName, dataSource, rowKey, association } = options;
+ const { collectionName, dataSource, association } = options;
if (!dataSource) {
throw new Error('dataSource is required');
@@ -35,7 +34,6 @@ export const createTableBlockUISchema = (options: {
params: {
pageSize: 20,
},
- rowKey,
showIndex: true,
dragSort: false,
},
diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps.ts b/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps.ts
index bc91c97de5..2082be83fd 100644
--- a/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps.ts
+++ b/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps.ts
@@ -11,15 +11,19 @@ import { useFieldSchema } from '@formily/react';
import { useMemo } from 'react';
import { useParsedFilter } from '../../../../../block-provider/hooks/useParsedFilter';
import { useParentRecordCommon } from '../../../useParentRecordCommon';
+import { useDataSourceManager } from '../../../../../data-source';
export const useTableBlockDecoratorProps = (props) => {
const { params, parseVariableLoading } = useTableBlockParams(props);
const parentRecord = useParentRecordCommon(props.association);
+ const dm = useDataSourceManager();
+ const collection = dm.getDataSource(props.dataSource)?.collectionManager.getCollection(props.collection);
return {
params,
parentRecord,
parseVariableLoading,
+ rowKey: collection?.filterTargetKey || 'id',
};
};
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 32c931e8f3..ed391e9210 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
@@ -141,6 +141,7 @@ export const useTableBlockProps = () => {
const storedFilter = block.service.params?.[1]?.filters || {};
if (selectedRow.includes(record[tableBlockContextBasicValue.rowKey])) {
+ block.clearSelection?.();
if (block.dataLoadingMode === 'manual') {
return block.clearData();
}
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 da441e8f62..e99f8a4a11 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
@@ -25,6 +25,35 @@ import { setDefaultSortingRulesSchemaSettingsItem } from '../../../../schema-set
import { setTheDataScopeSchemaSettingsItem } from '../../../../schema-settings/setTheDataScopeSchemaSettingsItem';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
+import { SchemaSettingsItemType } from '../../../../application';
+import { SchemaSettingsLinkageRules } from '../../../../schema-settings';
+import { LinkageRuleCategory } from '../../../../schema-settings/LinkageRules/type';
+
+const enabledIndexColumn: SchemaSettingsItemType = {
+ name: 'enableIndexColumn',
+ type: 'switch',
+ useComponentProps: () => {
+ const field = useField();
+ const fieldSchema = useFieldSchema();
+ const { t } = useTranslation();
+ const { dn } = useDesignable();
+ return {
+ title: t('Enable index column'),
+ checked: field.decoratorProps.enableIndexÏColumn !== false,
+ onChange: async (enableIndexÏColumn) => {
+ field.decoratorProps = field.decoratorProps || {};
+ field.decoratorProps.enableIndexÏColumn = enableIndexÏColumn;
+ fieldSchema['x-decorator-props'].enableIndexÏColumn = enableIndexÏColumn;
+ dn.emit('patch', {
+ schema: {
+ ['x-uid']: fieldSchema['x-uid'],
+ 'x-decorator-props': fieldSchema['x-decorator-props'],
+ },
+ });
+ },
+ };
+ },
+};
export const tableBlockSettings = new SchemaSettings({
name: 'blockSettings:table',
@@ -37,6 +66,19 @@ export const tableBlockSettings = new SchemaSettings({
name: 'setTheBlockHeight',
Component: SchemaSettingsBlockHeightItem,
},
+ {
+ name: 'linkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { name } = useCollection_deprecated();
+ const { t } = useTranslation();
+ return {
+ collectionName: name,
+ title: t('Block Linkage rules'),
+ category: LinkageRuleCategory.block,
+ };
+ },
+ },
{
name: 'treeTable',
type: 'switch',
@@ -111,7 +153,6 @@ export const tableBlockSettings = new SchemaSettings({
const { resource } = field.decoratorProps;
const collectionField = resource && getCollectionField(resource);
const api = useAPIClient();
-
return {
title: t('Enable drag and drop sorting'),
checked: field.decoratorProps.dragSort,
@@ -147,6 +188,7 @@ export const tableBlockSettings = new SchemaSettings({
return field.decoratorProps.dragSort;
},
},
+ enabledIndexColumn,
setTheDataScopeSchemaSettingsItem,
setDefaultSortingRulesSchemaSettingsItem,
setDataLoadingModeSettingsItem,
diff --git a/packages/core/client/src/modules/blocks/filter-blocks/FilterCollectionField.tsx b/packages/core/client/src/modules/blocks/filter-blocks/FilterCollectionField.tsx
index 2f075ecaf3..3a8d26d7ef 100644
--- a/packages/core/client/src/modules/blocks/filter-blocks/FilterCollectionField.tsx
+++ b/packages/core/client/src/modules/blocks/filter-blocks/FilterCollectionField.tsx
@@ -94,9 +94,13 @@ export const FilterCollectionFieldInternalField: React.FC = (props: Props) => {
// @ts-ignore
field.dataSource = uiSchema.enum;
const originalProps =
- compile({ ...(operator?.schema?.['x-component-props'] || {}), ...(uiSchema['x-component-props'] || {}) }) || {};
+ compile({
+ ...(operator?.schema?.['x-component-props'] || {}),
+ ...(uiSchema['x-component-props'] || {}),
+ ...(fieldSchema?.['x-component-props'] || {}),
+ }) || {};
- field.componentProps = merge(originalProps, field.componentProps || {}, dynamicProps || {});
+ field.componentProps = merge(field.componentProps || {}, originalProps, dynamicProps || {});
}, [uiSchemaOrigin]);
if (!uiSchemaOrigin) return null;
diff --git a/packages/core/client/src/modules/blocks/filter-blocks/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/filter-blocks/__e2e__/schemaInitializer.test.ts
index ebdc65a7c9..3d76b13799 100644
--- a/packages/core/client/src/modules/blocks/filter-blocks/__e2e__/schemaInitializer.test.ts
+++ b/packages/core/client/src/modules/blocks/filter-blocks/__e2e__/schemaInitializer.test.ts
@@ -35,7 +35,11 @@ test.describe('where filter block can be added', () => {
// 3. 与 Table、Details、List、GridCard 等区块建立连接
const connectByForm = async (name: string) => {
+ await page
+ .getByLabel('designer-schema-settings-CardItem-blockSettings:filterForm-users')
+ .waitFor({ state: 'hidden' });
await page.getByLabel('block-item-CardItem-users-filter-form').hover();
+ await page.getByRole('menuitem', { name: 'Connect data blocks right' }).waitFor({ state: 'detached' });
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:filterForm-users').hover();
await page.getByRole('menuitem', { name: 'Connect data blocks right' }).hover();
await page.getByRole('menuitem', { name }).click();
@@ -43,6 +47,7 @@ test.describe('where filter block can be added', () => {
const connectByCollapse = async (name: string) => {
await page.mouse.move(-500, 0);
await page.getByLabel('block-item-CardItem-users-filter-collapse').hover();
+ await page.getByRole('menuitem', { name: 'Connect data blocks right' }).waitFor({ state: 'detached' });
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:filterCollapse-users').hover();
await page.getByRole('menuitem', { name: 'Connect data blocks right' }).hover();
await page.getByRole('menuitem', { name }).click();
@@ -150,7 +155,9 @@ test.describe('where filter block can be added', () => {
}
// 2. 测试用表单筛选其它区块
+ await page.getByRole('menuitem', { name: 'Form right' }).waitFor({ state: 'detached' });
await page.getByLabel('schema-initializer-Grid-popup').hover();
+ await page.getByRole('menuitem', { name: 'Users' }).waitFor({ state: 'detached' });
await page.getByRole('menuitem', { name: 'Form right' }).hover();
await page.getByRole('menuitem', { name: 'Users' }).click();
await page.getByLabel('schema-initializer-Grid-filterForm:configureFields-users').hover();
diff --git a/packages/core/client/src/modules/blocks/filter-blocks/collapse/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/filter-blocks/collapse/__e2e__/schemaSettings.test.ts
index 7092bd15cc..04b3bf5f91 100644
--- a/packages/core/client/src/modules/blocks/filter-blocks/collapse/__e2e__/schemaSettings.test.ts
+++ b/packages/core/client/src/modules/blocks/filter-blocks/collapse/__e2e__/schemaSettings.test.ts
@@ -25,7 +25,7 @@ test.describe('collapse schema settings', () => {
await page.getByLabel('block-item-CardItem-general-filter-collapse').hover();
await page.getByLabel('designer-schema-settings-CardItem-AssociationFilter.BlockDesigner-general').hover();
},
- supportedOptions: ['Edit block title', 'Save as template', 'Connect data blocks', 'Delete'],
+ supportedOptions: ['Edit block title', 'Connect data blocks', 'Delete'],
});
});
@@ -46,7 +46,7 @@ test.describe('collapse schema settings', () => {
await page.getByRole('menuitem', { name: 'General' }).click();
// 点击一个选项,进行筛选
- await page.getByRole('button', { name: 'right singleSelect search' }).click();
+ await page.getByRole('button', { name: 'collapsed singleSelect search' }).click();
await page.getByLabel('block-item-CardItem-general-filter-collapse').getByText('Option1').click();
// 注意:在本地运行时,由于运行结束后不会清空之前创建的数据,所以在第一次运行之后,下面会报错。如果遇到这种情况,可以先不管
diff --git a/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseBlockSettings.ts b/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseBlockSettings.ts
index 503c1c6ada..6e6ff7ad44 100644
--- a/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseBlockSettings.ts
+++ b/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseBlockSettings.ts
@@ -12,11 +12,14 @@ import { useTranslation } from 'react-i18next';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { useCollection_deprecated } from '../../../../collection-manager';
import { FilterBlockType } from '../../../../filter-provider';
+import { SchemaSettingsLinkageRules } from '../../../../schema-settings';
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
import { SchemaSettingsConnectDataBlocks } from '../../../../schema-settings/SchemaSettingsConnectDataBlocks';
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
+import { LinkageRuleCategory } from '../../../../schema-settings/LinkageRules/type';
+import { useCollection } from '../../../../data-source/collection/CollectionProvider';
export const filterCollapseBlockSettings = new SchemaSettings({
name: 'blockSettings:filterCollapse',
@@ -29,6 +32,19 @@ export const filterCollapseBlockSettings = new SchemaSettings({
name: 'setTheBlockHeight',
Component: SchemaSettingsBlockHeightItem,
},
+ {
+ name: 'blockLinkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { name } = useCollection();
+ const { t } = useTranslation();
+ return {
+ collectionName: name,
+ title: t('Block Linkage rules'),
+ category: LinkageRuleCategory.block,
+ };
+ },
+ },
{
name: 'ConvertReferenceToDuplicate',
Component: SchemaSettingsTemplate,
diff --git a/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/autoFilterWhenSettingDefaultValue.test.ts b/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/autoFilterWhenSettingDefaultValue.test.ts
index 605c7e64a9..0a420f3da1 100644
--- a/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/autoFilterWhenSettingDefaultValue.test.ts
+++ b/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/autoFilterWhenSettingDefaultValue.test.ts
@@ -126,7 +126,7 @@ test.describe('filter form', () => {
y: 10,
},
});
- await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible();
+ await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible();
// 4. 此时点击 Reset 按钮,应该只显示一条数据,因为会把 nickname 的值重置为 {{$user.nickname}}
await page.getByLabel('action-Action-Reset-users-').click({
@@ -152,6 +152,6 @@ test.describe('filter form', () => {
y: 10,
},
});
- await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible();
+ await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data').last()).toBeVisible();
});
});
diff --git a/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/schemaSettings.test.ts
index 1b0fc57a40..1e1d82959b 100644
--- a/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/schemaSettings.test.ts
+++ b/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/schemaSettings.test.ts
@@ -28,7 +28,7 @@ test.describe('filter block schema settings', () => {
},
supportedOptions: [
'Edit block title',
- 'Save as block template',
+ // 'Save as block template',
'Linkage rules',
'Connect data blocks',
'Delete',
@@ -37,7 +37,7 @@ test.describe('filter block schema settings', () => {
});
test.describe('connect data blocks', () => {
- test('connecting two blocks of the same collection', async ({
+ test.skip('connecting two blocks of the same collection', async ({
page,
mockPage,
mockRecords,
diff --git a/packages/core/client/src/modules/blocks/filter-blocks/form/filterFormBlockSettings.ts b/packages/core/client/src/modules/blocks/filter-blocks/form/filterFormBlockSettings.ts
index 64573a1b0f..05ea675a63 100644
--- a/packages/core/client/src/modules/blocks/filter-blocks/form/filterFormBlockSettings.ts
+++ b/packages/core/client/src/modules/blocks/filter-blocks/form/filterFormBlockSettings.ts
@@ -19,6 +19,7 @@ import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/Schema
import { SchemaSettingsConnectDataBlocks } from '../../../../schema-settings/SchemaSettingsConnectDataBlocks';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
+import { LinkageRuleCategory } from '../../../../schema-settings/LinkageRules/type';
export const filterFormBlockSettings = new SchemaSettings({
name: 'blockSettings:filterForm',
@@ -48,12 +49,27 @@ export const filterFormBlockSettings = new SchemaSettings({
},
},
{
- name: 'linkageRules',
+ name: 'fieldLinkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { name } = useCollection_deprecated();
+ const { t } = useTranslation();
return {
collectionName: name,
+ title: t('Field Linkage rules'),
+ };
+ },
+ },
+ {
+ name: 'blockLinkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { name } = useCollection();
+ const { t } = useTranslation();
+ return {
+ collectionName: name,
+ title: t('Block Linkage rules'),
+ category: LinkageRuleCategory.block,
};
},
},
diff --git a/packages/core/client/src/modules/blocks/other-blocks/markdown/markdownBlockSettings.ts b/packages/core/client/src/modules/blocks/other-blocks/markdown/markdownBlockSettings.ts
index aecb5cd040..f779581ea2 100644
--- a/packages/core/client/src/modules/blocks/other-blocks/markdown/markdownBlockSettings.ts
+++ b/packages/core/client/src/modules/blocks/other-blocks/markdown/markdownBlockSettings.ts
@@ -7,11 +7,14 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import { useField } from '@formily/react';
+import { useField, useFieldSchema } from '@formily/react';
import { useTranslation } from 'react-i18next';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
import { SchemaSettingsRenderEngine } from '../../../../schema-settings/SchemaSettingsRenderEngine';
+import { SchemaSettingsLinkageRules } from '../../../../schema-settings';
+import { LinkageRuleCategory } from '../../../../schema-settings/LinkageRules/type';
+
export const markdownBlockSettings = new SchemaSettings({
name: 'blockSettings:markdown',
items: [
@@ -21,7 +24,6 @@ export const markdownBlockSettings = new SchemaSettings({
useComponentProps() {
const field = useField();
const { t } = useTranslation();
-
return {
title: t('Edit markdown'),
onClick: () => {
@@ -34,6 +36,27 @@ export const markdownBlockSettings = new SchemaSettings({
name: 'setTheBlockHeight',
Component: SchemaSettingsBlockHeightItem,
},
+ {
+ name: 'blockLinkageRules',
+ Component: SchemaSettingsLinkageRules,
+ useComponentProps() {
+ const { t } = useTranslation();
+ const fieldSchema = useFieldSchema();
+ const underForm = fieldSchema['x-decorator'] === 'FormItem';
+ return {
+ title: underForm ? t('Linkage rules') : t('Block Linkage rules'),
+ category: LinkageRuleCategory.block,
+ returnScope: (options) => {
+ return options.filter((v) => {
+ if (!underForm) {
+ return !['$nForm', '$nRecord'].includes(v.value);
+ }
+ return true;
+ });
+ },
+ };
+ },
+ },
{
name: 'setBlockTemplate',
Component: SchemaSettingsRenderEngine,
diff --git a/packages/core/client/src/modules/blocks/utils.ts b/packages/core/client/src/modules/blocks/utils.ts
new file mode 100644
index 0000000000..f2fb6599d9
--- /dev/null
+++ b/packages/core/client/src/modules/blocks/utils.ts
@@ -0,0 +1,91 @@
+/**
+ * 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 { useEffect, useState } from 'react';
+import { VariableOption, VariablesContextType } from '../../variables/types';
+import { conditionAnalyses } from '../../schema-component/common/utils/uitls';
+import { useApp } from '../../application';
+import { useCollectionRecord } from '../../data-source';
+
+enum ActionType {
+ Visible = 'visible',
+ Hidden = 'hidden',
+}
+
+const linkageAction = async (
+ {
+ operator,
+ condition,
+ variables,
+ localVariables,
+ conditionType,
+ displayResult,
+ }: {
+ operator;
+ condition;
+ variables: VariablesContextType;
+ localVariables: VariableOption[];
+ conditionType: 'advanced';
+ displayResult: any[];
+ },
+ jsonLogic: any,
+) => {
+ switch (operator) {
+ case ActionType.Visible:
+ if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
+ displayResult.push(ActionType.Visible);
+ }
+ return displayResult;
+ case ActionType.Hidden:
+ if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
+ displayResult.push(ActionType.Hidden);
+ }
+ return displayResult;
+ default:
+ return null;
+ }
+};
+
+export const useReactiveLinkageEffect = (
+ linkageRules: any[],
+ variables: VariablesContextType,
+ localVariables: VariableOption[],
+ triggerLinkageUpdate,
+) => {
+ const app = useApp();
+ const jsonLogic = app.jsonLogic;
+ const [displayResult, setDisplayResult] = useState(null);
+ const record = useCollectionRecord();
+ useEffect(() => {
+ const runLinkages = async () => {
+ const result: string[] = [];
+
+ for (const rule of linkageRules.filter((r) => !r.disabled)) {
+ for (const action of rule.actions || []) {
+ await linkageAction(
+ {
+ operator: action.operator,
+ condition: rule.condition,
+ variables,
+ localVariables,
+ conditionType: rule.conditionType,
+ displayResult: result,
+ },
+ jsonLogic,
+ );
+ }
+ }
+ setDisplayResult(result);
+ };
+
+ runLinkages();
+ }, [linkageRules, triggerLinkageUpdate, record]);
+
+ return displayResult;
+};
diff --git a/packages/core/client/src/modules/fields/__e2e__/component/AssociationSelect/dataScope.test.ts b/packages/core/client/src/modules/fields/__e2e__/component/AssociationSelect/dataScope.test.ts
index eda935801a..cf9444da71 100644
--- a/packages/core/client/src/modules/fields/__e2e__/component/AssociationSelect/dataScope.test.ts
+++ b/packages/core/client/src/modules/fields/__e2e__/component/AssociationSelect/dataScope.test.ts
@@ -53,7 +53,7 @@ test.describe('AssociationSelect ', () => {
.getByLabel('block-item-CollectionField-test-form-test.b-b')
.getByTestId('select-object-multiple')
.click();
- await expect(page.getByText('No data')).toBeVisible();
+ await expect(page.getByText('No data').last()).toBeVisible();
// 2. 当给字段 a 选择一个值后,字段 b 的下拉列表中会显示符合条件的值
await page
diff --git a/packages/core/client/src/modules/fields/__e2e__/styleLinkageRule.test.ts b/packages/core/client/src/modules/fields/__e2e__/styleLinkageRule.test.ts
new file mode 100644
index 0000000000..5912e3b9d0
--- /dev/null
+++ b/packages/core/client/src/modules/fields/__e2e__/styleLinkageRule.test.ts
@@ -0,0 +1,54 @@
+/**
+ * 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 { oneDetailBlockFieldWidthFieldStyle } from './templates';
+
+test.describe('field style linkage rule', () => {
+ test('field style support Color、Background Color、Text Align、Font Size、Font Weight、Font Style', async ({
+ page,
+ mockPage,
+ }) => {
+ await mockPage(oneDetailBlockFieldWidthFieldStyle).goto();
+ await page.getByLabel('block-item-CardItem-users-').hover();
+ await page.getByLabel('block-item-CollectionField-users-details-users.nickname-Nickname').hover();
+ await page
+ .getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-users-users.nickname', {
+ exact: true,
+ })
+ .hover();
+ await page.getByText('Style').click();
+ await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
+ await page.getByText('Add property').click();
+ await page.locator('div').filter({ hasText: 'Style Linkage' }).nth(2).click();
+ await page.getByTestId('select-linkage-properties').click();
+ await expect(page.getByText('Color', { exact: true })).toBeVisible();
+ await expect(page.getByText('Background Color')).toBeVisible();
+ await expect(page.getByText('Text Align')).toBeVisible();
+ await expect(page.getByText('Font Size(px)')).toBeVisible();
+ await expect(page.getByText('Font Weight')).toBeVisible();
+ await expect(page.getByText('Font Style')).toBeVisible();
+ // 配置字段样式
+ await page.getByText('Font Size(px)').click();
+
+ await page.getByPlaceholder('Valid range: 10-').click();
+ await page.getByPlaceholder('Valid range: 10-').fill('40');
+ await page.getByRole('button', { name: 'OK' }).click();
+ // 获取元素并断言其 font-size 为 40px
+ const fontSize = await page
+ .locator('.ant-formily-item-control-content-component')
+ .first()
+ .evaluate((element) => {
+ return window.getComputedStyle(element).fontSize;
+ });
+
+ // 断言 font-size 是 40px
+ expect(fontSize).toBe('40px');
+ });
+});
diff --git a/packages/core/client/src/modules/fields/__e2e__/templates.ts b/packages/core/client/src/modules/fields/__e2e__/templates.ts
index ce2b24d33b..a0fbd9545d 100644
--- a/packages/core/client/src/modules/fields/__e2e__/templates.ts
+++ b/packages/core/client/src/modules/fields/__e2e__/templates.ts
@@ -2476,3 +2476,227 @@ export const shouldImmediatelyShowDrawerWhenClickingEnableLinkForTheFirstTime =
'x-index': 1,
},
};
+
+export const oneDetailBlockFieldWidthFieldStyle = {
+ pageSchema: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Page',
+ 'x-app-version': 'v1.7.0-beta.5',
+ properties: {
+ j5rf9qdpszc: {
+ type: 'void',
+ 'x-component': 'Grid',
+ 'x-initializer': 'page:addBlock',
+ properties: {
+ x72noji3ol7: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.7.0-beta.5',
+ properties: {
+ '6klgaiwxs8q': {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Col',
+ 'x-app-version': '1.7.0-beta.5',
+ properties: {
+ dpj7gevoiqw: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-acl-action': 'users:view',
+ 'x-decorator': 'DetailsBlockProvider',
+ 'x-use-decorator-props': 'useDetailsWithPaginationDecoratorProps',
+ 'x-decorator-props': {
+ dataSource: 'main',
+ collection: 'users',
+ readPretty: true,
+ action: 'list',
+ params: {
+ pageSize: 1,
+ },
+ },
+ 'x-toolbar': 'BlockSchemaToolbar',
+ 'x-settings': 'blockSettings:detailsWithPagination',
+ 'x-component': 'CardItem',
+ 'x-app-version': '1.7.0-beta.5',
+ properties: {
+ '5zo8ay6xu74': {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Details',
+ 'x-read-pretty': true,
+ 'x-use-component-props': 'useDetailsWithPaginationProps',
+ 'x-app-version': '1.7.0-beta.5',
+ properties: {
+ '2u7t8s8zffc': {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-initializer': 'details:configureActions',
+ 'x-component': 'ActionBar',
+ 'x-component-props': {
+ style: {
+ marginBottom: 24,
+ },
+ },
+ 'x-app-version': '1.7.0-beta.5',
+ 'x-uid': 'ajfbilj1eas',
+ '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.7.0-beta.5',
+ properties: {
+ g1jwsjkiks0: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.7.0-beta.5',
+ properties: {
+ gi5z3ss61t2: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Col',
+ 'x-app-version': '1.7.0-beta.5',
+ properties: {
+ nickname: {
+ 'x-uid': '2a4om1os4i8',
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'string',
+ 'x-toolbar': 'FormItemSchemaToolbar',
+ 'x-settings': 'fieldSettings:FormItem',
+ 'x-component': 'CollectionField',
+ 'x-decorator': 'FormItem',
+ 'x-collection-field': 'users.nickname',
+ 'x-component-props': {},
+ 'x-app-version': '1.7.0-beta.5',
+ // 'x-linkage-style-rules': [
+ // {
+ // condition: {
+ // $and: [],
+ // },
+ // actions: [
+ // {
+ // operator: 'fontSize',
+ // value: {
+ // mode: 'constant',
+ // value: 40,
+ // },
+ // },
+ // ],
+ // },
+ // ],
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'ki6424bbnoa',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': '387m3183ea3',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ '1rhnbs2gcca': {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.7.0-beta.5',
+ properties: {
+ d5ma1xl6j2i: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Col',
+ 'x-app-version': '1.7.0-beta.5',
+ properties: {
+ username: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'string',
+ 'x-toolbar': 'FormItemSchemaToolbar',
+ 'x-settings': 'fieldSettings:FormItem',
+ 'x-component': 'CollectionField',
+ 'x-decorator': 'FormItem',
+ 'x-collection-field': 'users.username',
+ 'x-component-props': {},
+ 'x-app-version': '1.7.0-beta.5',
+ 'x-uid': '1ayny4b2ylm',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'cdkqplw5ec7',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'k35rh0ef681',
+ 'x-async': false,
+ 'x-index': 2,
+ },
+ },
+ 'x-uid': 'erulf9efwvv',
+ '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.7.0-beta.5',
+ 'x-uid': 'rcmvkgxvob9',
+ 'x-async': false,
+ 'x-index': 3,
+ },
+ },
+ 'x-uid': 'jra1fx1jp0k',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': '3u4qbg33yhg',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'g126n7qmm1k',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'ftxf3gxjwxs',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ name: 'j8dv3zpp2l7',
+ 'x-uid': 'n9tpyozmuz9',
+ 'x-async': true,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'u3njequ14zz',
+ 'x-async': true,
+ 'x-index': 1,
+ },
+};
diff --git a/packages/core/client/src/modules/fields/component/CascadeSelect/cascadeSelectComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/CascadeSelect/cascadeSelectComponentFieldSettings.tsx
index 8730b493fa..6c6f2af2d8 100644
--- a/packages/core/client/src/modules/fields/component/CascadeSelect/cascadeSelectComponentFieldSettings.tsx
+++ b/packages/core/client/src/modules/fields/component/CascadeSelect/cascadeSelectComponentFieldSettings.tsx
@@ -17,6 +17,7 @@ import { useDesignable, useFieldModeOptions, useIsAddNewForm } from '../../../..
import { isSubMode } from '../../../../schema-component/antd/association-field/util';
import { useTitleFieldOptions } from '../../../../schema-component/antd/form-item/FormItem.Settings';
import { ellipsisSettingsItem } from '../Input/inputComponentSettings';
+import { setTheDataScope } from '../Select/selectComponentFieldSettings';
const fieldComponent: any = {
name: 'fieldComponent',
@@ -100,5 +101,5 @@ const titleField: any = {
export const cascadeSelectComponentFieldSettings = new SchemaSettings({
name: 'fieldSettings:component:CascadeSelect',
- items: [fieldComponent, titleField, ellipsisSettingsItem],
+ items: [fieldComponent, titleField, ellipsisSettingsItem, setTheDataScope],
});
diff --git a/packages/core/client/src/modules/fields/component/Input/inputComponentSettings.tsx b/packages/core/client/src/modules/fields/component/Input/inputComponentSettings.tsx
index 8f53455a71..d86283a3f2 100644
--- a/packages/core/client/src/modules/fields/component/Input/inputComponentSettings.tsx
+++ b/packages/core/client/src/modules/fields/component/Input/inputComponentSettings.tsx
@@ -8,6 +8,7 @@
*/
import { useField, useFieldSchema } from '@formily/react';
+import _ from 'lodash';
import { useTranslation } from 'react-i18next';
import { useBlockContext, useOpenModeContext } from '../../../../';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
@@ -45,11 +46,12 @@ export const ellipsisSettingsItem: SchemaSettingsItemType = {
tableFieldInstanceList.forEach((fieldInstance) => {
fieldInstance.componentProps.ellipsis = checked;
});
- schema['x-component-props']['ellipsis'] = checked;
} else {
formField.componentProps.ellipsis = checked;
}
+ _.set(schema, 'x-component-props.ellipsis', checked);
+
await dn.emit('patch', {
schema: {
'x-uid': schema['x-uid'],
diff --git a/packages/core/client/src/modules/fields/component/Select/__e2e__/selectDataScope.test.ts b/packages/core/client/src/modules/fields/component/Select/__e2e__/selectDataScope.test.ts
index 368503878c..341e3be388 100644
--- a/packages/core/client/src/modules/fields/component/Select/__e2e__/selectDataScope.test.ts
+++ b/packages/core/client/src/modules/fields/component/Select/__e2e__/selectDataScope.test.ts
@@ -34,6 +34,6 @@ test.describe('data scope of component Select', () => {
await page.getByTestId('select-object-multiple').click();
await expect(page.getByRole('option', { name: 'admin' })).toBeHidden();
await expect(page.getByRole('option', { name: 'member' })).toBeHidden();
- await expect(page.getByText('No data')).toBeVisible();
+ await expect(page.getByText('No data').last()).toBeVisible();
});
});
diff --git a/packages/core/client/src/modules/fields/component/Select/__e2e__/selectOptionsInLinkageRule.test.ts b/packages/core/client/src/modules/fields/component/Select/__e2e__/selectOptionsInLinkageRule.test.ts
new file mode 100644
index 0000000000..5de86a8bd6
--- /dev/null
+++ b/packages/core/client/src/modules/fields/component/Select/__e2e__/selectOptionsInLinkageRule.test.ts
@@ -0,0 +1,34 @@
+/**
+ * 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 { oneFormWithSelectField } from './templatesOfBug';
+
+test.describe('options of Select field in linkage rule', () => {
+ test('options change with linkage rule ', async ({ page, mockPage }) => {
+ await mockPage(oneFormWithSelectField).goto();
+ // 联动规则控制选项
+ await page.getByLabel('block-item-CardItem-general-').hover();
+ await page.getByLabel('block-item-CollectionField-').click();
+ await expect(page.getByRole('option', { name: 'option2' })).toBeVisible();
+ await expect(page.getByRole('option', { name: 'option3' })).not.toBeVisible();
+ await page.getByRole('option', { name: 'option2' }).click();
+
+ // 去掉联动规则恢复选项
+ await page.getByLabel('block-item-CardItem-general-').hover();
+ await page.getByLabel('designer-schema-settings-CardItem-blockSettings:createForm-general').hover();
+ await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
+ await page.getByRole('switch', { name: 'On Off' }).click();
+ await page.getByRole('button', { name: 'OK' }).click();
+ await page.reload();
+ await page.getByLabel('block-item-CollectionField-').click();
+ await expect(page.getByRole('option', { name: 'option2' }).last()).toBeVisible();
+ await expect(page.getByRole('option', { name: 'option3' })).toBeVisible();
+ });
+});
diff --git a/packages/core/client/src/modules/fields/component/Select/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/fields/component/Select/__e2e__/templatesOfBug.ts
index 1d90fe8ebf..dc2e7f36cf 100644
--- a/packages/core/client/src/modules/fields/component/Select/__e2e__/templatesOfBug.ts
+++ b/packages/core/client/src/modules/fields/component/Select/__e2e__/templatesOfBug.ts
@@ -7,6 +7,8 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
+import { generalWithM2oSingleSelect } from '@nocobase/test/e2e';
+
export const T3867 = {
pageSchema: {
_isJSONSchemaObject: true,
@@ -581,3 +583,176 @@ export const oneFormWithSubTableSelectField = {
},
],
};
+
+export const oneFormWithSelectField = {
+ pageSchema: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Page',
+ properties: {
+ iza2br2wzq4: {
+ type: 'void',
+ 'x-component': 'Grid',
+ 'x-initializer': 'page:addBlock',
+ properties: {
+ dc4lkvej93w: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.7.0-beta.1',
+ properties: {
+ jrfai5z5a4e: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Col',
+ 'x-app-version': '1.7.0-beta.1',
+ properties: {
+ bph3a0yrmvp: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-acl-action-props': {
+ skipScopeCheck: true,
+ },
+ 'x-acl-action': 'general:create',
+ 'x-decorator': 'FormBlockProvider',
+ 'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
+ 'x-decorator-props': {
+ dataSource: 'main',
+ collection: 'general',
+ },
+ 'x-toolbar': 'BlockSchemaToolbar',
+ 'x-settings': 'blockSettings:createForm',
+ 'x-component': 'CardItem',
+ 'x-app-version': '1.7.0-beta.1',
+ properties: {
+ '2ef5ea23d4b': {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'FormV2',
+ 'x-use-component-props': 'useCreateFormBlockProps',
+ 'x-app-version': '1.7.0-beta.1',
+ properties: {
+ grid: {
+ 'x-uid': 'oqk574y4mx2',
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid',
+ 'x-initializer': 'form:configureFields',
+ 'x-app-version': '1.7.0-beta.1',
+ 'x-linkage-rules': [
+ {
+ condition: {
+ $and: [],
+ },
+ actions: [
+ {
+ targetFields: ['singleSelect'],
+ operator: 'options',
+ value: {
+ value: ['option2'],
+ },
+ },
+ ],
+ },
+ ],
+ properties: {
+ '1ijfrrxqs4c': {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Row',
+ 'x-app-version': '1.7.0-beta.1',
+ properties: {
+ '8j09c65cp2x': {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-component': 'Grid.Col',
+ 'x-app-version': '1.7.0-beta.1',
+ properties: {
+ singleSelect: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'string',
+ 'x-toolbar': 'FormItemSchemaToolbar',
+ 'x-settings': 'fieldSettings:FormItem',
+ 'x-component': 'CollectionField',
+ 'x-decorator': 'FormItem',
+ 'x-collection-field': 'general.singleSelect',
+ 'x-component-props': {
+ style: {
+ width: '100%',
+ },
+ },
+ 'x-app-version': '1.7.0-beta.1',
+ 'x-uid': 'dxsbc80ewso',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'la6qedc9qwx',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'ijsbxa4yys7',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ kpp1azsxbzy: {
+ _isJSONSchemaObject: true,
+ version: '2.0',
+ type: 'void',
+ 'x-initializer': 'createForm:configureActions',
+ 'x-component': 'ActionBar',
+ 'x-component-props': {
+ layout: 'one-column',
+ },
+ 'x-app-version': '1.7.0-beta.1',
+ 'x-uid': '1n56lepnve8',
+ 'x-async': false,
+ 'x-index': 2,
+ },
+ },
+ 'x-uid': 'lf74cpc3tub',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'mhxlfdk74jy',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'obyvsr0s0nx',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 's6jd8p9h28q',
+ 'x-async': false,
+ 'x-index': 1,
+ },
+ },
+ name: '8btun7jzkrk',
+ 'x-uid': 'eyjf89l83a1',
+ 'x-async': true,
+ 'x-index': 1,
+ },
+ },
+ 'x-uid': 'hizrr7jzogr',
+ 'x-async': true,
+ 'x-index': 1,
+ },
+ collections: generalWithM2oSingleSelect,
+};
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 2823e9d62f..8daaabe25d 100644
--- a/packages/core/client/src/modules/fields/component/Select/selectComponentFieldSettings.tsx
+++ b/packages/core/client/src/modules/fields/component/Select/selectComponentFieldSettings.tsx
@@ -126,6 +126,10 @@ export const getAllowMultiple = (params?: { title: string }) => {
return {
name: 'allowMultiple',
type: 'switch',
+ useVisible() {
+ const isAssociationField = useIsAssociationField();
+ return isAssociationField;
+ },
useComponentProps() {
const { t } = useTranslation();
const field = useField();
@@ -228,7 +232,7 @@ const setDefaultSortingRules = {
Component: SchemaSettingsSortingRule,
};
-const setTheDataScope: any = {
+export const setTheDataScope: any = {
name: 'setTheDataScope',
Component: SchemaSettingsDataScope,
useComponentProps() {
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 d5b5ae25d3..93f2cab0a7 100644
--- a/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx
+++ b/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx
@@ -28,7 +28,34 @@ import { isSubMode } from '../../../../schema-component/antd/association-field/u
import { useIsAssociationField } from '../../../../schema-component/antd/form-item';
import { FormLinkageRules } from '../../../../schema-settings/LinkageRules';
import { SchemaSettingsLinkageRules } from '../../../../schema-settings/SchemaSettings';
+import { SchemaSettingsItemType } from '../../../../application';
+import { useColumnSchema } from '../../../../schema-component';
+const enabledIndexColumn: SchemaSettingsItemType = {
+ name: 'enableIndexColumn',
+ type: 'switch',
+ useComponentProps: () => {
+ const field = useField();
+ const fieldSchema = useFieldSchema();
+ const { t } = useTranslation();
+ const { dn } = useDesignable();
+ return {
+ title: t('Enable index column'),
+ checked: field.componentProps.enableIndexÏColumn !== false,
+ onChange: async (enableIndexÏColumn) => {
+ field.componentProps = field.componentProps || {};
+ field.componentProps.enableIndexÏColumn = enableIndexÏColumn;
+ fieldSchema['x-component-props'].enableIndexÏColumn = enableIndexÏColumn;
+ dn.emit('patch', {
+ schema: {
+ ['x-uid']: fieldSchema['x-uid'],
+ 'x-component-props': fieldSchema['x-component-props'],
+ },
+ });
+ },
+ };
+ },
+};
const fieldComponent: any = {
name: 'fieldComponent',
type: 'select',
@@ -312,11 +339,12 @@ export const linkageRules = {
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const field = useField();
- const fieldSchema = useFieldSchema();
+ const schema = useFieldSchema();
+ const { fieldSchema: columnSchema } = useColumnSchema();
+ const fieldSchema = columnSchema || schema;
const cm = useCollectionManager();
const collectionField = cm.getCollectionField(fieldSchema['x-collection-field']);
const { rerenderDataBlock } = useRerenderDataBlock();
-
return {
collectionName: collectionField?.target,
Component: LinkageRulesComponent,
@@ -365,6 +393,7 @@ export const subTablePopoverComponentFieldSettings = new SchemaSettings({
allowSelectExistingRecord,
allowDisassociation,
setDefaultSortingRules,
+ enabledIndexColumn,
linkageRules,
recordPerPage,
],
diff --git a/packages/core/client/src/modules/menu/LinkMenuItem.tsx b/packages/core/client/src/modules/menu/LinkMenuItem.tsx
index 8217f7de14..aace433ec4 100644
--- a/packages/core/client/src/modules/menu/LinkMenuItem.tsx
+++ b/packages/core/client/src/modules/menu/LinkMenuItem.tsx
@@ -14,6 +14,14 @@ import React, { useCallback, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { Router } from 'react-router-dom';
import { SchemaInitializerItem } from '../../application';
+import {
+ CollectionManagerProvider,
+ useCollectionManager,
+} from '../../data-source/collection/CollectionManagerProvider';
+import {
+ DataSourceManagerProvider,
+ useDataSourceManager,
+} from '../../data-source/data-source/DataSourceManagerProvider';
import { useGlobalTheme } from '../../global-theme';
import { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema';
import {
@@ -34,6 +42,8 @@ export const LinkMenuItem = () => {
const { urlSchema, paramsSchema } = useURLAndHTMLSchema();
const parentRoute = useParentRoute();
const { createRoute } = useNocoBaseRoutes();
+ const dm = useDataSourceManager();
+ const cm = useCollectionManager();
const handleClick = useCallback(async () => {
const values = await FormDialog(
@@ -41,31 +51,35 @@ export const LinkMenuItem = () => {
() => {
const history = createMemoryHistory();
return (
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
);
},
theme,
diff --git a/packages/core/client/src/modules/page/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/page/__e2e__/schemaSettings.test.ts
index ed8c0a7879..8c43d6fc56 100644
--- a/packages/core/client/src/modules/page/__e2e__/schemaSettings.test.ts
+++ b/packages/core/client/src/modules/page/__e2e__/schemaSettings.test.ts
@@ -63,9 +63,11 @@ test.describe('page schema settings', () => {
await page.getByLabel('block-item-Input-Tab name').getByRole('textbox').fill('new tab');
// 选择一个图标
await page.getByRole('button', { name: 'Select icon' }).click();
+ await expect(page.getByLabel('account-book').locator('svg')).toHaveCount(1);
await page.getByLabel('account-book').locator('svg').click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await expect(page.getByText('new tab')).toBeVisible();
+ await expect(page.getByLabel('account-book').locator('svg')).toHaveCount(1);
await expect(page.getByLabel('account-book').locator('svg')).toBeVisible();
});
});
@@ -92,10 +94,12 @@ test.describe('tabs schema settings', () => {
await page.getByLabel('block-item-Input-Tab name').getByRole('textbox').click();
await page.getByLabel('block-item-Input-Tab name').getByRole('textbox').fill('new name of page tab');
await page.getByRole('button', { name: 'Select icon' }).click();
+ await expect(page.getByLabel('account-book').locator('svg')).toHaveCount(1);
await page.getByLabel('account-book').locator('svg').click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await expect(page.getByText('new name of page tab')).toBeVisible();
+ await expect(page.getByLabel('account-book').locator('svg')).toHaveCount(1);
await expect(page.getByLabel('account-book').locator('svg')).toBeVisible();
});
diff --git a/packages/core/client/src/modules/popup/PopupContextProvider.tsx b/packages/core/client/src/modules/popup/PopupContextProvider.tsx
index 2b034a4477..400e1d005d 100644
--- a/packages/core/client/src/modules/popup/PopupContextProvider.tsx
+++ b/packages/core/client/src/modules/popup/PopupContextProvider.tsx
@@ -20,6 +20,8 @@ import { PopupVisibleProvider, PopupVisibleProviderContext } from '../../schema-
export const PopupContextProvider: React.FC<{
visible?: boolean;
setVisible?: (visible: boolean) => void;
+ openMode?: string;
+ openSize?: string;
}> = (props) => {
const { visible: visibleFromProps, setVisible: setVisibleFromProps } = props;
const [visible, setVisible] = useState(false);
@@ -37,8 +39,8 @@ export const PopupContextProvider: React.FC<{
},
[setVisibleFromProps, setVisibleWithURL],
);
- const openMode = fieldSchema['x-component-props']?.['openMode'] || 'drawer';
- const openSize = fieldSchema['x-component-props']?.['openSize'];
+ const openMode = props.openMode || fieldSchema['x-component-props']?.['openMode'] || 'drawer';
+ const openSize = props.openSize || fieldSchema['x-component-props']?.['openSize'];
return (
diff --git a/packages/core/client/src/modules/popup/__e2e__/router.test.ts b/packages/core/client/src/modules/popup/__e2e__/router.test.ts
index 1d534bc912..753d28d632 100644
--- a/packages/core/client/src/modules/popup/__e2e__/router.test.ts
+++ b/packages/core/client/src/modules/popup/__e2e__/router.test.ts
@@ -36,7 +36,8 @@ test.describe('popup router', () => {
await page.locator('.ant-drawer-mask').click();
// expect to be back to the first page
- await page.getByText('Users单层子页面Configure').hover();
+ await page.getByLabel('block-item-CardItem-users-').getByText('Users 单层子页面Configure').hover();
+
await expect(
page.getByRole('button', { name: 'designer-schema-settings-CardItem-blockSettings:table-users' }),
).toBeVisible();
@@ -60,7 +61,7 @@ test.describe('popup router', () => {
await page.locator('.ant-drawer-mask').click();
// expect to be back to the first page
- await page.getByText('Users单层子页面Configure').hover();
+ await page.getByLabel('block-item-CardItem-users-').getByText('Users 单层子页面Configure').hover();
await expect(
page.getByRole('button', { name: 'designer-schema-settings-CardItem-blockSettings:table-users' }),
).toBeVisible();
diff --git a/packages/core/client/src/modules/popup/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/popup/__e2e__/schemaInitializer.test.ts
index 6ec7d36c03..ecbb9205a0 100644
--- a/packages/core/client/src/modules/popup/__e2e__/schemaInitializer.test.ts
+++ b/packages/core/client/src/modules/popup/__e2e__/schemaInitializer.test.ts
@@ -55,8 +55,9 @@ test.describe('add blocks to the popup', () => {
// 通过 Association records 创建一个关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Details right' }).hover();
- await page.getByRole('menuitem', { name: 'Associated records' }).hover();
+ await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'Roles' }).click();
+ await page.mouse.move(300, 0);
await page.getByLabel('schema-initializer-Grid-details:configureFields-roles').hover();
await page.getByRole('menuitem', { name: 'Role UID' }).click();
await page.mouse.move(300, 0);
@@ -87,7 +88,7 @@ test.describe('add blocks to the popup', () => {
// 通过 Association records 创建关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Table right' }).hover();
- await page.getByRole('menuitem', { name: 'Associated records' }).hover();
+ await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'manyToMany' }).click();
await page.mouse.move(-300, 0);
await page
@@ -135,7 +136,7 @@ test.describe('add blocks to the popup', () => {
// 通过 Association records 创建一个关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Table right' }).hover();
- await page.getByRole('menuitem', { name: 'Associated records' }).hover();
+ await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'Roles' }).click();
await page
.getByTestId('drawer-Action.Container-users-View record')
diff --git a/packages/core/client/src/modules/popup/__e2e__/schemaInitializer1.test.ts b/packages/core/client/src/modules/popup/__e2e__/schemaInitializer1.test.ts
index 7d9b481842..01be31da31 100644
--- a/packages/core/client/src/modules/popup/__e2e__/schemaInitializer1.test.ts
+++ b/packages/core/client/src/modules/popup/__e2e__/schemaInitializer1.test.ts
@@ -124,10 +124,13 @@ test.describe('where to open a popup and what can be added to it', () => {
async function addBlock(names: string[]) {
await page.getByLabel('schema-initializer-Grid-popup').hover();
+ await page.waitForTimeout(500);
for (let i = 0; i < names.length - 1; i++) {
const name = names[i];
await page.getByRole('menuitem', { name }).hover();
+ await page.waitForTimeout(500);
}
+ await expect(page.getByRole('menuitem', { name: names[names.length - 1] })).toHaveCount(1);
await page.getByRole('menuitem', { name: names[names.length - 1] }).click();
await page.mouse.move(300, 0);
}
@@ -206,13 +209,14 @@ test.describe('where to open a popup and what can be added to it', () => {
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
await page.getByRole('menuitem', { name: 'Details' }).hover();
- await page.getByRole('menuitem', { name: 'Associated records' }).hover();
+ await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'Many to one' }).click();
await page.mouse.move(300, 0);
await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible();
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
- await page.getByRole('menuitem', { name: 'Table right' }).hover();
+ await page.getByRole('menuitem', { name: 'Table right' }).click();
+ await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1);
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'One to many' }).click();
await page.mouse.move(300, 0);
@@ -272,13 +276,14 @@ test.describe('where to open a popup and what can be added to it', () => {
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
await page.getByRole('menuitem', { name: 'Details' }).hover();
- await page.getByRole('menuitem', { name: 'Associated records' }).hover();
+ await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'Many to one' }).click();
await page.mouse.move(300, 0);
await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible();
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
- await page.getByRole('menuitem', { name: 'Table right' }).hover();
+ await page.getByRole('menuitem', { name: 'Table right' }).click();
+ await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1);
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'One to many' }).click();
await page.mouse.move(300, 0);
diff --git a/packages/core/client/src/modules/popup/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/popup/__e2e__/schemaSettings.test.ts
index 7da9ed5c43..23c5bbfa7c 100644
--- a/packages/core/client/src/modules/popup/__e2e__/schemaSettings.test.ts
+++ b/packages/core/client/src/modules/popup/__e2e__/schemaSettings.test.ts
@@ -44,6 +44,7 @@ test.describe('tabs schema settings', () => {
await page.getByRole('button', { name: 'OK', exact: true }).click();
await expect(page.getByText('Add new with new name')).toBeVisible();
+ await expect(page.getByLabel('account-book').locator('svg')).toHaveCount(1);
await expect(page.getByLabel('account-book').locator('svg')).toBeVisible();
});
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 4d48df81f3..e0bfad2db1 100644
--- a/packages/core/client/src/modules/popup/__e2e__/subPage.test.ts
+++ b/packages/core/client/src/modules/popup/__e2e__/subPage.test.ts
@@ -71,7 +71,7 @@ test.describe('sub page', () => {
expect(page.url()).not.toContain('/popups/');
// 确认是否回到了主页面
- await page.getByText('Users单层子页面Configure').hover();
+ // await page.getByText('Users单层子页面Configure').hover();
await expect(
page.getByRole('button', { name: 'designer-schema-settings-CardItem-blockSettings:table-users' }),
).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 55d755cb8d..7768b268c0 100644
--- a/packages/core/client/src/modules/variable/__e2e__/basic.test.ts
+++ b/packages/core/client/src/modules/variable/__e2e__/basic.test.ts
@@ -18,7 +18,7 @@ test.describe('variables', () => {
await page.getByLabel('action-Action.Link-View-view-').hover();
await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:view-users').hover();
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
- await page.getByLabel('variable-button').click();
+ await page.getByTestId('left-filter-field').getByLabel('variable-button').click();
// 2. 断言应该显示的变量
['Constant', 'Current user', 'Current role', 'API token', 'Date variables', 'Current record'].forEach(
diff --git a/packages/core/client/src/modules/variable/__e2e__/currentRecord.test.ts b/packages/core/client/src/modules/variable/__e2e__/currentRecord.test.ts
index 01a1256d2c..59f2f00f24 100644
--- a/packages/core/client/src/modules/variable/__e2e__/currentRecord.test.ts
+++ b/packages/core/client/src/modules/variable/__e2e__/currentRecord.test.ts
@@ -22,14 +22,14 @@ test.describe('variable: Current Record', () => {
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.getByText('Add condition', { exact: true }).click();
- await page.getByLabel('variable-button').click();
+ await page.getByLabel('variable-button').first().click();
// 当前表单中应该包含 “Nickname” 字段
await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click();
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
// 当前对象中应该包含 “Role UID” 字段
- await page.getByLabel('variable-button').click();
+ await page.getByLabel('variable-button').first().click();
await page.getByText('Current object').click();
await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click();
await page.getByRole('menuitemcheckbox', { name: 'Role UID' }).click();
@@ -43,12 +43,12 @@ test.describe('variable: Current Record', () => {
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.getByText('Add condition', { exact: true }).click();
- await page.getByLabel('variable-button').click();
+ await page.getByLabel('variable-button').first().click();
// 当前记录中应该包含 “Nickname” 字段
await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).click();
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
- await page.getByLabel('variable-button').click();
+ await page.getByLabel('variable-button').first().click();
// 当前对象中应该包含 “Role UID” 字段
await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click();
diff --git a/packages/core/client/src/modules/variable/__e2e__/parentObject.test.ts b/packages/core/client/src/modules/variable/__e2e__/parentObject.test.ts
index 9e040459b4..d8c825b7ca 100644
--- a/packages/core/client/src/modules/variable/__e2e__/parentObject.test.ts
+++ b/packages/core/client/src/modules/variable/__e2e__/parentObject.test.ts
@@ -55,6 +55,7 @@ test.describe('variable: parent object', () => {
// 1. Use "Current form" and "Parent object" variables in nested subforms and subtables
await page.getByLabel('block-item-CollectionField-collection1-form-collection1.m2m1-m2m1').hover();
+ await page.getByRole('menuitem', { name: 'Linkage rules' }).waitFor({ state: 'detached' });
await page
.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-collection1-collection1.m2m1', {
exact: true,
diff --git a/packages/core/client/src/nocobase-buildin-plugin/index.tsx b/packages/core/client/src/nocobase-buildin-plugin/index.tsx
index 7524e0b89f..4cf96356cd 100644
--- a/packages/core/client/src/nocobase-buildin-plugin/index.tsx
+++ b/packages/core/client/src/nocobase-buildin-plugin/index.tsx
@@ -15,12 +15,13 @@ import { tval } from '@nocobase/utils/client';
import { AuthErrorCode } from '@nocobase/auth/client';
import { Button, Modal, Result, Spin } from 'antd';
import React, { FC } from 'react';
-import { Navigate, useNavigate } from 'react-router-dom';
+import { Navigate } from 'react-router-dom';
import { ACLPlugin } from '../acl';
import { Application } from '../application';
import { Plugin } from '../application/Plugin';
import { BlockSchemaComponentPlugin } from '../block-provider';
import { CollectionPlugin } from '../collection-manager';
+import { AppNotFound } from '../common/AppNotFound';
import { RemoteDocumentTitlePlugin } from '../document-title';
import { PinnedListPlugin } from '../plugin-manager';
import { PMPlugin } from '../pm';
@@ -75,9 +76,9 @@ const useErrorProps = (app: Application, error: any) => {
}
};
-const AppError: FC<{ error: Error; app: Application }> = observer(
+const AppError: FC<{ error: Error & { title?: string }; app: Application }> = observer(
({ app, error }) => {
- const props = useErrorProps(app, error);
+ const props = getProps(app);
return (
= observer(
transform: translate(0, -50%);
`}
status="error"
- title={app.i18n.t('App error')}
+ title={error?.title || app.i18n.t('App error', { ns: 'client' })}
subTitle={app.i18n.t(error?.message)}
+ {...props}
extra={[
window.location.reload()}>
{app.i18n.t('Try again')}
@@ -125,6 +127,14 @@ const getProps = (app: Application) => {
};
}
+ if (app.error.code === 'APP_WARNING') {
+ return {
+ status: 'warning',
+ title: 'App warning',
+ subTitle: app.error?.message,
+ };
+ }
+
if (app.error.code === 'APP_INITIALIZING') {
return {
status: 'info',
@@ -252,22 +262,6 @@ const AppMaintainingDialog: FC<{ app: Application; error: Error }> = observer(
{ displayName: 'AppMaintainingDialog' },
);
-export const AppNotFound = () => {
- const navigate = useNavigate();
- return (
- navigate('/', { replace: true })} type="primary">
- Back Home
-
- }
- />
- );
-};
-
export class NocoBaseBuildInPlugin extends Plugin {
async afterAdd() {
this.app.addComponents({
diff --git a/packages/core/client/src/plugin-manager/PinnedPluginListProvider.tsx b/packages/core/client/src/plugin-manager/PinnedPluginListProvider.tsx
index 15fccb4c2c..095a5804c3 100644
--- a/packages/core/client/src/plugin-manager/PinnedPluginListProvider.tsx
+++ b/packages/core/client/src/plugin-manager/PinnedPluginListProvider.tsx
@@ -30,14 +30,48 @@ export const PinnedPluginListProvider: React.FC<{ items: any }> = (props) => {
const pinnedPluginListClassName = css`
display: inline-flex;
align-items: center;
+ color: var(--colorTextHeaderMenu);
+
+ .anticon {
+ color: var(--colorTextHeaderMenu);
+ }
.ant-btn {
- border: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
height: 46px;
width: 46px;
+ padding: 0;
+ border: 0;
border-radius: 0;
background: none;
color: rgba(255, 255, 255, 0.65);
+ vertical-align: middle;
+
+ a {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ }
+
+ .ant-badge {
+ color: rgba(255, 255, 255, 0.65);
+ .anticon {
+ display: inline-block;
+ vertical-align: middle;
+ line-height: 1em;
+ font-size: initial;
+ }
+ > sup {
+ height: 10px;
+ line-height: 10px;
+ font-size: 8px;
+ }
+ }
+
&:hover {
background: rgba(255, 255, 255, 0.1) !important;
}
diff --git a/packages/core/client/src/pm/PluginManager.tsx b/packages/core/client/src/pm/PluginManager.tsx
index 76365d1537..f2f3b95f2c 100644
--- a/packages/core/client/src/pm/PluginManager.tsx
+++ b/packages/core/client/src/pm/PluginManager.tsx
@@ -10,7 +10,7 @@
export * from './PluginManagerLink';
import { PageHeader } from '@ant-design/pro-layout';
import { useDebounce } from 'ahooks';
-import { Button, Col, Divider, Input, List, Modal, Result, Row, Space, Spin, Table, Tabs } from 'antd';
+import { Button, Col, Divider, Input, List, Modal, Row, Space, Spin, Table, TableProps, Tabs } from 'antd';
import _ from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -19,6 +19,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { css } from '@emotion/css';
import { useACLRoleContext } from '../acl/ACLProvider';
import { useAPIClient, useRequest } from '../api-client';
+import { AppNotFound } from '../common/AppNotFound';
import { useDocumentTitle } from '../document-title';
import { useToken } from '../style';
import { PluginCard } from './PluginCard';
@@ -127,25 +128,27 @@ function BulkEnableButton({ plugins = [] }) {
}}
size={'small'}
pagination={false}
- columns={[
- {
- title: t('Plugin'),
- dataIndex: 'displayName',
- ellipsis: true,
- },
- {
- title: t('Description'),
- dataIndex: 'description',
- ellipsis: true,
- width: 300,
- },
- {
- title: t('Package name'),
- dataIndex: 'packageName',
- width: 300,
- ellipsis: true,
- },
- ]}
+ columns={
+ [
+ {
+ title: t('Plugin'),
+ dataIndex: 'displayName',
+ ellipsis: true,
+ },
+ {
+ title: t('Description'),
+ dataIndex: 'description',
+ ellipsis: true,
+ width: 300,
+ },
+ {
+ title: t('Package name'),
+ dataIndex: 'packageName',
+ width: 300,
+ ellipsis: true,
+ },
+ ] as TableProps['columns']
+ }
dataSource={items}
/>
@@ -407,6 +410,6 @@ export const PluginManager = () => {
) : (
-
+
);
};
diff --git a/packages/core/client/src/pm/PluginManagerLink.tsx b/packages/core/client/src/pm/PluginManagerLink.tsx
index 07867ce568..2fce194c38 100644
--- a/packages/core/client/src/pm/PluginManagerLink.tsx
+++ b/packages/core/client/src/pm/PluginManagerLink.tsx
@@ -13,6 +13,7 @@ import React, { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { useApp, useNavigateNoUpdate } from '../application';
+import { useMobileLayout } from '../route-switch/antd/admin-layout';
import { useCompile } from '../schema-component';
import { useToken } from '../style';
@@ -20,6 +21,12 @@ export const PluginManagerLink = () => {
const { t } = useTranslation();
const navigate = useNavigateNoUpdate();
const { token } = useToken();
+ const { isMobileLayout } = useMobileLayout();
+
+ if (isMobileLayout) {
+ return null;
+ }
+
return (
{
};
}, [app.pluginSettingsManager]);
+ const { isMobileLayout } = useMobileLayout();
+
+ if (isMobileLayout) {
+ return null;
+ }
+
return (
{
}
/>
)}
-
- {currentSetting ? (
-
- ) : (
-
- )}
-
+
diff --git a/packages/core/client/src/pm/index.tsx b/packages/core/client/src/pm/index.tsx
index 0ee4017f49..6ea78f24cf 100644
--- a/packages/core/client/src/pm/index.tsx
+++ b/packages/core/client/src/pm/index.tsx
@@ -29,12 +29,13 @@ export class PMPlugin extends Plugin {
}
addSettings() {
- this.app.pluginSettingsManager.add('ui-schema-storage', {
- title: '{{t("Block templates")}}',
- icon: 'LayoutOutlined',
- Component: BlockTemplatesPane,
- aclSnippet: 'pm.ui-schema-storage.block-templates',
- });
+ // hide old block template settings page
+ // this.app.pluginSettingsManager.add('ui-schema-storage', {
+ // title: '{{t("Block templates")}}',
+ // icon: 'LayoutOutlined',
+ // Component: BlockTemplatesPane,
+ // aclSnippet: 'pm.ui-schema-storage.block-templates',
+ // });
this.app.pluginSettingsManager.add('system-settings', {
icon: 'SettingOutlined',
title: '{{t("System settings")}}',
diff --git a/packages/core/client/src/route-switch/antd/admin-layout/__tests__/findFirstPageRoute.test.tsx b/packages/core/client/src/route-switch/antd/admin-layout/__tests__/findFirstPageRoute.test.tsx
new file mode 100644
index 0000000000..5bfd4554d1
--- /dev/null
+++ b/packages/core/client/src/route-switch/antd/admin-layout/__tests__/findFirstPageRoute.test.tsx
@@ -0,0 +1,212 @@
+/**
+ * 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 { findFirstPageRoute, NocoBaseDesktopRouteType } from '..';
+import { NocoBaseDesktopRoute } from '../convertRoutesToSchema';
+
+describe('findFirstPageRoute', () => {
+ // 基本测试:空路由数组
+ it('should return undefined for empty routes array', () => {
+ const result = findFirstPageRoute([]);
+ expect(result).toBeUndefined();
+ });
+
+ // 基本测试:undefined 路由数组
+ it('should return undefined for undefined routes', () => {
+ const result = findFirstPageRoute(undefined);
+ expect(result).toBeUndefined();
+ });
+
+ // 测试:只有一个页面路由
+ it('should find the first page route when there is only one page', () => {
+ const routes: NocoBaseDesktopRoute[] = [
+ {
+ id: 1,
+ schemaUid: 'page1',
+ type: NocoBaseDesktopRouteType.page,
+ title: 'Page 1',
+ },
+ ];
+
+ const result = findFirstPageRoute(routes);
+ expect(result).toEqual(routes[0]);
+ });
+
+ // 测试:多个页面路由
+ it('should find the first page route when there are multiple pages', () => {
+ const routes: NocoBaseDesktopRoute[] = [
+ {
+ id: 1,
+ schemaUid: 'page1',
+ type: NocoBaseDesktopRouteType.page,
+ title: 'Page 1',
+ },
+ {
+ id: 2,
+ schemaUid: 'page2',
+ type: NocoBaseDesktopRouteType.page,
+ title: 'Page 2',
+ },
+ ];
+
+ const result = findFirstPageRoute(routes);
+ expect(result).toEqual(routes[0]);
+ });
+
+ // 测试:不同类型的路由混合
+ it('should find the first page route among mixed route types', () => {
+ const routes: NocoBaseDesktopRoute[] = [
+ {
+ id: 1,
+ schemaUid: 'link1',
+ type: NocoBaseDesktopRouteType.link,
+ title: 'Link 1',
+ },
+ {
+ id: 2,
+ schemaUid: 'page1',
+ type: NocoBaseDesktopRouteType.page,
+ title: 'Page 1',
+ },
+ ];
+
+ const result = findFirstPageRoute(routes);
+ expect(result).toEqual(routes[1]);
+ });
+
+ // 测试:隐藏的菜单项
+ it('should ignore hidden menu items', () => {
+ const routes: NocoBaseDesktopRoute[] = [
+ {
+ id: 1,
+ schemaUid: 'page1',
+ type: NocoBaseDesktopRouteType.page,
+ title: 'Page 1',
+ hideInMenu: true,
+ },
+ {
+ id: 2,
+ schemaUid: 'page2',
+ type: NocoBaseDesktopRouteType.page,
+ title: 'Page 2',
+ },
+ ];
+
+ const result = findFirstPageRoute(routes);
+ expect(result).toEqual(routes[1]);
+ });
+
+ // 测试:嵌套路由
+ it('should find page route in nested group', () => {
+ const routes: NocoBaseDesktopRoute[] = [
+ {
+ id: 1,
+ type: NocoBaseDesktopRouteType.group,
+ title: 'Group 1',
+ children: [
+ {
+ id: 11,
+ schemaUid: 'page1',
+ type: NocoBaseDesktopRouteType.page,
+ title: 'Page 1',
+ },
+ ],
+ },
+ ];
+
+ const result = findFirstPageRoute(routes);
+ expect(result).toEqual(routes[0].children[0]);
+ });
+
+ // 测试:多层嵌套路由
+ it('should find page route in deeply nested groups', () => {
+ const routes: NocoBaseDesktopRoute[] = [
+ {
+ id: 1,
+ type: NocoBaseDesktopRouteType.group,
+ title: 'Group 1',
+ children: [
+ {
+ id: 11,
+ type: NocoBaseDesktopRouteType.group,
+ title: 'Group 1-1',
+ children: [
+ {
+ id: 111,
+ schemaUid: 'page1',
+ type: NocoBaseDesktopRouteType.page,
+ title: 'Page 1',
+ },
+ ],
+ },
+ ],
+ },
+ ];
+
+ const result = findFirstPageRoute(routes);
+ expect(result).toEqual(routes[0].children[0].children[0]);
+ });
+
+ // 测试:复杂路由结构
+ it('should find the first visible page in a complex route structure', () => {
+ const routes: NocoBaseDesktopRoute[] = [
+ {
+ id: 1,
+ type: NocoBaseDesktopRouteType.group,
+ title: 'Group 1',
+ hideInMenu: true,
+ children: [
+ {
+ id: 11,
+ schemaUid: 'page1',
+ type: NocoBaseDesktopRouteType.page,
+ title: 'Page 1',
+ },
+ ],
+ },
+ {
+ id: 2,
+ type: NocoBaseDesktopRouteType.group,
+ title: 'Group 2',
+ children: [
+ {
+ id: 21,
+ schemaUid: 'page2',
+ type: NocoBaseDesktopRouteType.page,
+ title: 'Page 2',
+ },
+ ],
+ },
+ ];
+
+ const result = findFirstPageRoute(routes);
+ expect(result).toEqual(routes[1].children[0]);
+ });
+
+ // 测试:空组
+ it('should skip empty groups and find page in next group', () => {
+ const routes: NocoBaseDesktopRoute[] = [
+ {
+ id: 1,
+ type: NocoBaseDesktopRouteType.group,
+ title: 'Empty Group',
+ children: [],
+ },
+ {
+ id: 2,
+ schemaUid: 'page1',
+ type: NocoBaseDesktopRouteType.page,
+ title: 'Page 1',
+ },
+ ];
+
+ const result = findFirstPageRoute(routes);
+ expect(result).toEqual(routes[1]);
+ });
+});
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 ddbf8a266f..b3a6a1c610 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
@@ -7,17 +7,17 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import { EllipsisOutlined } from '@ant-design/icons';
+import { EllipsisOutlined, HighlightOutlined } from '@ant-design/icons';
import ProLayout, { RouteContext, RouteContextType } from '@ant-design/pro-layout';
import { HeaderViewProps } from '@ant-design/pro-layout/es/components/Header';
import { css } from '@emotion/css';
-import { Popover, Tooltip } from 'antd';
+import { theme as antdTheme, ConfigProvider, Popover, Result, Tooltip } from 'antd';
import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
+import { useTranslation } from 'react-i18next';
import { Link, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
import {
ACLRolesCheckProvider,
- AppNotFound,
CurrentAppInfoProvider,
DndContext,
Icon,
@@ -29,6 +29,7 @@ import {
RemoteSchemaTemplateManagerProvider,
SortableItem,
useDesignable,
+ useGlobalTheme,
useMenuDragEnd,
useParseURLAndParams,
useRequest,
@@ -44,6 +45,7 @@ import {
useLocationNoUpdate,
} from '../../../application/CustomRouterContextProvider';
import { Plugin } from '../../../application/Plugin';
+import { AppNotFound } from '../../../common/AppNotFound';
import { withTooltipComponent } from '../../../hoc/withTooltipComponent';
import { menuItemInitializer } from '../../../modules/menu/menuItemInitializer';
import { useMenuTranslation } from '../../../schema-component/antd/menu/locale';
@@ -51,6 +53,7 @@ import { KeepAlive } from './KeepAlive';
import { NocoBaseDesktopRoute, NocoBaseDesktopRouteType } from './convertRoutesToSchema';
import { MenuSchemaToolbar, ResetThemeTokenAndKeepAlgorithm } from './menuItemSettings';
import { userCenterSettings } from './userCenterSettings';
+import { createStyles } from 'antd-style';
export { KeepAlive, NocoBaseDesktopRouteType };
@@ -169,6 +172,7 @@ const layoutContentClass = css`
const className1 = css`
width: 168px;
+ height: var(--nb-header-height);
margin-right: 4px;
display: inline-flex;
flex-shrink: 0;
@@ -196,12 +200,53 @@ const pageContentStyle: React.CSSProperties = {
overflowY: 'auto',
};
+const ShowTipWhenNoPages = () => {
+ const { allAccessRoutes } = useAllAccessDesktopRoutes();
+ const { designable } = useDesignable();
+ const { token } = useToken();
+ const { t } = useTranslation();
+ const location = useLocation();
+
+ // Check if there are any pages
+ if (allAccessRoutes.length === 0 && !designable && ['/admin', '/admin/'].includes(location.pathname)) {
+ return (
+ }
+ title={t('No pages yet, please configure first')}
+ subTitle={t(`Click the "UI Editor" icon in the upper right corner to enter the UI Editor mode`)}
+ />
+ );
+ }
+
+ return null;
+};
+
+// 移动端中需要使用 dvh 单位来计算高度,否则会出现滚动不到最底部的问题
+const mobileHeight = {
+ height: `calc(100dvh - var(--nb-header-height))`,
+};
+
+function isDvhSupported() {
+ // 创建一个测试元素
+ const testEl = document.createElement('div');
+
+ // 尝试设置 dvh 单位
+ testEl.style.height = '1dvh';
+
+ // 如果浏览器支持 dvh,则会解析这个值
+ // 如果不支持,height 将保持为空字符串或被设置为无效值
+ return testEl.style.height === '1dvh';
+}
+
export const LayoutContent = () => {
+ const style = useMemo(() => (isDvhSupported() ? mobileHeight : undefined), []);
+
/* Use the "nb-subpages-slot-without-header-and-side" class name to locate the position of the subpages */
return (
-
+
);
@@ -263,7 +308,7 @@ const GroupItem: FC<{ item: any }> = (props) => {
};
const WithTooltip: FC<{ title: string; hidden: boolean }> = (props) => {
- const { inHeader } = useContext(headerContext);
+ const { inHeader } = useContext(HeaderContext);
return (
@@ -401,7 +446,7 @@ const contentStyle = {
paddingInline: 0,
};
-const headerContext = React.createContext<{ inHeader: boolean }>({ inHeader: false });
+const HeaderContext = React.createContext<{ inHeader: boolean }>({ inHeader: false });
const popoverStyle = css`
.ant-popover-inner {
@@ -458,6 +503,8 @@ const subMenuItemRender = (item, dom) => {
};
const CollapsedButton: FC<{ collapsed: boolean }> = (props) => {
+ const { token } = useToken();
+
return (
{(context) =>
@@ -470,7 +517,7 @@ const CollapsedButton: FC<{ collapsed: boolean }> = (props) => {
// Fix the issue where the collapse/expand button is covered by subpages
.ant-pro-sider-collapsed-button {
top: 64px;
- left: ${props.collapsed ? 52 : 188}px;
+ left: ${props.collapsed ? 52 : (token.siderWidth || 200) - 12}px;
z-index: 200;
transition: left 0.2s;
}
@@ -490,34 +537,86 @@ const collapsedButtonRender = (collapsed, dom) => {
return {dom} ;
};
+/**
+ * 这个问题源自 antd 的一个 bug,等 antd 修复了这个问题后,可以删除这个样式。
+ * - issue: https://github.com/ant-design/pro-components/issues/8593
+ * - issue: https://github.com/ant-design/pro-components/issues/8522
+ * - issue: https://github.com/ant-design/pro-components/issues/8432
+ */
+const useHeaderStyle = createStyles(({ token }: any) => {
+ return {
+ headerPopup: {
+ '&.ant-menu-submenu-popup>.ant-menu': {
+ backgroundColor: `${token.colorBgHeader}`,
+ },
+ },
+ headerMenuActive: {
+ '& .ant-menu-submenu-selected>.ant-menu-submenu-title': {
+ color: token.colorTextHeaderMenuActive,
+ },
+ },
+ };
+});
const headerContextValue = { inHeader: true };
+const HeaderWrapper: FC = (props) => {
+ const { styles } = useHeaderStyle();
+
+ return (
+
+ {props.children}
+
+ );
+};
const headerRender = (props: HeaderViewProps, defaultDom: React.ReactNode) => {
- return {defaultDom} ;
+ return {defaultDom} ;
+};
+
+const IsMobileLayoutContext = React.createContext<{
+ isMobileLayout: boolean;
+ setIsMobileLayout: React.Dispatch>;
+}>({
+ isMobileLayout: false,
+ setIsMobileLayout: () => {},
+});
+
+const MobileLayoutProvider: FC = (props) => {
+ const [isMobileLayout, setIsMobileLayout] = useState(false);
+ const value = useMemo(() => ({ isMobileLayout, setIsMobileLayout }), [isMobileLayout]);
+
+ return {props.children} ;
+};
+
+export const useMobileLayout = () => {
+ const { isMobileLayout, setIsMobileLayout } = useContext(IsMobileLayoutContext);
+ return { isMobileLayout, setIsMobileLayout };
};
export const InternalAdminLayout = () => {
const { allAccessRoutes } = useAllAccessDesktopRoutes();
- const { designable } = useDesignable();
+ const { designable: _designable } = useDesignable();
const location = useLocation();
const { onDragEnd } = useMenuDragEnd();
const { token } = useToken();
- const [isMobile, setIsMobile] = useState(false);
- const [collapsed, setCollapsed] = useState(false);
+ const { isMobileLayout, setIsMobileLayout } = useMobileLayout();
+ const [collapsed, setCollapsed] = useState(isMobileLayout);
const doNotChangeCollapsedRef = useRef(false);
const { t } = useMenuTranslation();
+ const designable = isMobileLayout ? false : _designable;
+ const { styles } = useHeaderStyle();
+
const route = useMemo(() => {
return {
path: '/',
- children: convertRoutesToLayout(allAccessRoutes, { designable, isMobile, t }),
+ children: convertRoutesToLayout(allAccessRoutes, { designable, isMobile: isMobileLayout, t }),
};
- }, [allAccessRoutes, designable, isMobile, t]);
+ }, [allAccessRoutes, designable, isMobileLayout, t]);
const layoutToken = useMemo(() => {
return {
header: {
colorBgHeader: token.colorBgHeader,
colorTextMenu: token.colorTextHeaderMenu,
colorTextMenuSelected: token.colorTextHeaderMenuActive,
- colorTextMenuActive: token.colorTextHeaderMenuActive,
+ colorTextMenuActive: token.colorTextHeaderMenuHover,
colorBgMenuItemHover: token.colorBgHeaderMenuHover,
colorBgMenuItemSelected: token.colorBgHeaderMenuActive,
heightLayoutHeader: 46,
@@ -534,6 +633,21 @@ export const InternalAdminLayout = () => {
bgLayout: token.colorBgLayout,
};
}, [token]);
+ const { theme, isDarkTheme } = useGlobalTheme();
+ const mobileTheme = useMemo(() => {
+ return {
+ ...theme,
+ token: {
+ ...theme.token,
+ paddingPageHorizontal: 8, // Horizontal page padding
+ paddingPageVertical: 8, // Vertical page padding
+ marginBlock: 12, // Spacing between blocks
+ borderRadiusBlock: 8, // Block border radius
+ fontSize: 16, // Font size
+ },
+ algorithm: isDarkTheme ? [antdTheme.compactAlgorithm, antdTheme.darkAlgorithm] : antdTheme.compactAlgorithm, // Set mobile mode to always use compact algorithm
+ };
+ }, [theme, isDarkTheme]);
const onCollapse = useCallback((collapsed: boolean) => {
if (doNotChangeCollapsedRef.current) {
@@ -549,11 +663,17 @@ export const InternalAdminLayout = () => {
});
}, []);
+ const menuProps = useMemo(() => {
+ return {
+ overflowedIndicatorPopupClassName: styles.headerPopup,
+ };
+ }, [styles.headerPopup]);
+
return (
{
onCollapse={onCollapse}
collapsed={collapsed}
onPageChange={onPageChange}
+ menuProps={menuProps}
>
{(value: RouteContextType) => {
const { isMobile: _isMobile } = value;
- if (_isMobile !== isMobile) {
- setIsMobile(_isMobile);
+ if (_isMobile !== isMobileLayout) {
+ setIsMobileLayout(_isMobile);
}
- return ;
+ return (
+
+
+
+ );
}}
@@ -587,27 +712,11 @@ export const InternalAdminLayout = () => {
);
};
-function getDefaultPageUid(routes: NocoBaseDesktopRoute[]) {
- // Find the first route of type "page"
- for (const route of routes) {
- if (route.type === NocoBaseDesktopRouteType.page) {
- return route.schemaUid;
- }
-
- if (route.children?.length) {
- const result = getDefaultPageUid(route.children);
- if (result) {
- return result;
- }
- }
- }
-}
-
const NavigateToDefaultPage: FC = (props) => {
const { allAccessRoutes } = useAllAccessDesktopRoutes();
const location = useLocationNoUpdate();
- const defaultPageUid = getDefaultPageUid(allAccessRoutes);
+ const defaultPageUid = findFirstPageRoute(allAccessRoutes)?.schemaUid;
return (
<>
@@ -696,6 +805,7 @@ export class AdminLayoutPlugin extends Plugin {
async load() {
this.app.schemaSettingsManager.add(userCenterSettings);
this.app.addComponents({ AdminLayout, AdminDynamicPage });
+ this.app.use(MobileLayoutProvider);
}
}
@@ -717,36 +827,6 @@ export function findRouteBySchemaUid(schemaUid: string, treeArray: any[]) {
return null;
}
-const MenuItemIcon: FC<{ icon: string; title: string }> = (props) => {
- const { inHeader } = useContext(headerContext);
-
- return (
-
- {(value: RouteContextType) => {
- const { collapsed } = value;
-
- if (collapsed && !inHeader) {
- return props.icon ? (
-
- ) : (
-
- {props.title.charAt(0)}
-
- );
- }
-
- return props.icon ? : null;
- }}
-
- );
-};
-
const MenuDesignerButton: FC<{ testId: string }> = (props) => {
const { render: renderInitializer } = useSchemaInitializerRender(menuItemInitializer);
@@ -756,6 +836,19 @@ const MenuDesignerButton: FC<{ testId: string }> = (props) => {
});
};
+const MenuTitleWithIcon: FC<{ icon: any; title: string }> = (props) => {
+ if (props.icon) {
+ return (
+
+
+ {props.title}
+
+ );
+ }
+
+ return <>{props.title}>;
+};
+
function convertRoutesToLayout(
routes: NocoBaseDesktopRoute[],
{ designable, parentRoute, isMobile, t, depth = 0 }: any,
@@ -775,9 +868,11 @@ function convertRoutesToLayout(
};
const result: any[] = routes.map((item) => {
+ const name = depth > 1 ? : t(item.title); // ProLayout 组件不显示第二级菜单的 icon,所以这里自己实现
+
if (item.type === NocoBaseDesktopRouteType.link) {
return {
- name: t(item.title),
+ name,
icon: item.icon ? : null,
path: '/',
hideInMenu: item.hideInMenu,
@@ -788,7 +883,7 @@ function convertRoutesToLayout(
if (item.type === NocoBaseDesktopRouteType.page) {
return {
- name: t(item.title),
+ name,
icon: item.icon ? : null,
path: `/admin/${item.schemaUid}`,
redirect: `/admin/${item.schemaUid}`,
@@ -808,7 +903,7 @@ function convertRoutesToLayout(
}
return {
- name: t(item.title),
+ name,
icon: item.icon ? : null,
path: `/admin/${item.id}`,
redirect:
@@ -853,16 +948,17 @@ function findRouteById(id: string, treeArray: any[]) {
return null;
}
-function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) {
+export function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) {
if (!routes) return;
- for (const route of routes) {
+ for (const route of routes.filter((item) => !item.hideInMenu)) {
if (route.type === NocoBaseDesktopRouteType.page) {
return route;
}
- if (route.children?.length) {
- return findFirstPageRoute(route.children);
+ if (route.type === NocoBaseDesktopRouteType.group && route.children?.length) {
+ const result = findFirstPageRoute(route.children);
+ if (result) return result;
}
}
}
diff --git a/packages/core/client/src/route-switch/antd/admin-layout/menuItemSettings.tsx b/packages/core/client/src/route-switch/antd/admin-layout/menuItemSettings.tsx
index 3b91f08ef0..176aea409a 100644
--- a/packages/core/client/src/route-switch/antd/admin-layout/menuItemSettings.tsx
+++ b/packages/core/client/src/route-switch/antd/admin-layout/menuItemSettings.tsx
@@ -28,7 +28,6 @@ import {
useGlobalTheme,
useNavigateNoUpdate,
useNocoBaseRoutes,
- useToken,
useURLAndHTMLSchema,
} from '../../..';
import { getPageMenuSchema } from '../../../';
@@ -48,6 +47,11 @@ const components = { TreeSelect };
const toItems = (routes: NocoBaseDesktopRoute[], { t, compile }) => {
const items = [];
for (const route of routes) {
+ // filter out the tabs
+ if (route.type === NocoBaseDesktopRouteType.tabs) {
+ continue;
+ }
+
const item = {
label: isVariable(route.title) ? compile(route.title) : t(route.title),
value: `${route.id}||${route.type}`,
@@ -61,7 +65,7 @@ const toItems = (routes: NocoBaseDesktopRoute[], { t, compile }) => {
};
const insertPositionToMethod = {
- beforeBegin: 'prepend',
+ beforeBegin: 'insertBefore',
afterEnd: 'insertAfter',
};
@@ -490,28 +494,43 @@ const MoveToMenuItem = () => {
const { moveRoute } = useNocoBaseRoutes();
const currentRoute = useCurrentRoute();
- const onMoveToSubmit: (values: any) => void = useCallback(async ({ target, position }) => {
- const [targetId] = target?.split?.('||') || [];
- if (!targetId) {
- return;
- }
+ const onMoveToSubmit: (values: any) => void = useCallback(
+ async ({ target, position }) => {
+ const [targetId] = target?.split?.('||') || [];
+ if (!targetId) {
+ return;
+ }
- if (targetId === undefined || !currentRoute) {
- return;
- }
+ if (targetId === undefined || !currentRoute) {
+ return;
+ }
- const positionToMethod = {
- beforeBegin: 'prepend',
- afterEnd: 'insertAfter',
- };
+ const positionToMethod = {
+ beforeBegin: 'insertBefore',
+ afterEnd: 'insertAfter',
+ };
- await moveRoute({
- sourceId: currentRoute.id as any,
- targetId: targetId,
- sortField: 'sort',
- method: positionToMethod[position],
- });
- }, []);
+ // 'beforeEnd' 表示的是插入到一个分组的里面
+ const options =
+ position === 'beforeEnd'
+ ? {
+ targetScope: {
+ parentId: targetId,
+ },
+ }
+ : {
+ targetId: targetId,
+ };
+
+ await moveRoute({
+ sourceId: currentRoute.id as any,
+ sortField: 'sort',
+ method: positionToMethod[position],
+ ...options,
+ });
+ },
+ [currentRoute, moveRoute],
+ );
return (
{
const { getComponentByOpenMode, defaultOpenMode } = useOpenModeContext() || {};
- const { openMode = defaultOpenMode } = useActionContext();
+ const { openMode = props.openMode || defaultOpenMode } = useActionContext();
const popupLevel = React.useContext(PopupLevelContext);
const currentLevel = popupLevel + 1;
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 63e758f4fb..0f24cf6b72 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
@@ -9,14 +9,16 @@
import { ISchema, useField, useFieldSchema } from '@formily/react';
import { isValid, uid } from '@formily/shared';
-import { ModalProps } from 'antd';
-import React, { useCallback } from 'react';
+import { ModalProps, Select } from 'antd';
+import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useCompile, useDesignable } from '../..';
-import { isInitializersSame, useApp } from '../../../application';
+import { isInitializersSame, useApp, usePlugin } from '../../../application';
+import { useGlobalVariable } from '../../../application/hooks/useGlobalVariable';
import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings';
import { useSchemaToolbar } from '../../../application/schema-toolbar';
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager';
+import { highlightBlock, startScrollEndTracking, stopScrollEndTracking, unhighlightBlock } from '../../../filter-provider/highlightBlock';
import { FlagProvider } from '../../../flag-provider';
import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings';
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
@@ -32,7 +34,9 @@ import {
SchemaSettingsSwitchItem,
} from '../../../schema-settings/SchemaSettings';
import { DefaultValueProvider } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
+import { useAllDataBlocks } from '../page/AllDataBlocksProvider';
import { useLinkageAction } from './hooks';
+import { useAfterSuccessOptions } from './hooks/useGetAfterSuccessVariablesOptions';
import { requestSettingsSchema } from './utils';
const MenuGroup = (props) => {
@@ -67,7 +71,24 @@ export function ButtonEditor(props) {
title: t('Button icon'),
default: fieldSchema?.['x-component-props']?.icon,
'x-component-props': {},
- 'x-visible': !isLink,
+ },
+ onlyIcon: {
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Checkbox',
+ title: t('Icon only'),
+ default: fieldSchema?.['x-component-props']?.onlyIcon,
+ 'x-component-props': {},
+ 'x-visible': isLink,
+ 'x-reactions': [
+ {
+ dependencies: ['icon'],
+ fulfill: {
+ state: {
+ hidden: '{{!$deps[0]}}',
+ },
+ },
+ },
+ ],
},
iconColor: {
title: t('Color'),
@@ -96,13 +117,14 @@ export function ButtonEditor(props) {
},
} as ISchema
}
- onSubmit={({ title, icon, type, iconColor }) => {
+ onSubmit={({ title, icon, type, iconColor, onlyIcon }) => {
if (field.address.toString() === fieldSchema.name) {
field.title = title;
field.componentProps.iconColor = iconColor;
field.componentProps.icon = icon;
field.componentProps.danger = type === 'danger';
field.componentProps.type = type || field.componentProps.type;
+ field.componentProps.onlyIcon = onlyIcon;
} else {
field.form.query(new RegExp(`.${fieldSchema.name}$`)).forEach((fieldItem) => {
fieldItem.title = title;
@@ -110,6 +132,7 @@ export function ButtonEditor(props) {
fieldItem.componentProps.icon = icon;
fieldItem.componentProps.danger = type === 'danger';
fieldItem.componentProps.type = type || fieldItem.componentProps.type;
+ fieldItem.componentProps.onlyIcon = onlyIcon;
});
}
@@ -119,6 +142,7 @@ export function ButtonEditor(props) {
fieldSchema['x-component-props'].icon = icon;
fieldSchema['x-component-props'].danger = type === 'danger';
fieldSchema['x-component-props'].type = type || field.componentProps.type;
+ fieldSchema['x-component-props'].onlyIcon = onlyIcon;
dn.emit('patch', {
schema: {
@@ -167,6 +191,13 @@ export function AssignedFieldValues() {
'x-component': 'Grid',
'x-initializer': 'assignFieldValuesForm:configureFields',
};
+ if (fieldSchema['x-template-uid']) {
+ initialSchema['x-template-root-ref'] = {
+ 'x-template-uid': fieldSchema['x-template-uid'],
+ 'x-path': 'x-action-settings.schemaUid',
+ };
+ }
+
const tips = {
'customize:update': t(
'After clicking the custom button, the following fields of the current record will be saved according to the following form.',
@@ -253,26 +284,117 @@ export function SkipValidation() {
);
}
+const fieldNames = {
+ value: 'value',
+ label: 'label',
+};
+const useVariableProps = (environmentVariables) => {
+ const scope = useAfterSuccessOptions();
+ return {
+ scope: [environmentVariables, ...scope].filter(Boolean),
+ fieldNames,
+ };
+};
+
+const hideDialog = (dialogClassName: string) => {
+ const dialogMask = document.querySelector(`.${dialogClassName} > .ant-modal-mask`);
+ const dialogWrap = document.querySelector(`.${dialogClassName} > .ant-modal-wrap`);
+ if (dialogMask) {
+ dialogMask.style.opacity = '0';
+ dialogMask.style.transition = 'opacity 0.5s ease';
+ }
+ if (dialogWrap) {
+ dialogWrap.style.opacity = '0';
+ dialogWrap.style.transition = 'opacity 0.5s ease';
+ }
+}
+
+const showDialog = (dialogClassName: string) => {
+ const dialogMask = document.querySelector(`.${dialogClassName} > .ant-modal-mask`);
+ const dialogWrap = document.querySelector(`.${dialogClassName} > .ant-modal-wrap`);
+ if (dialogMask) {
+ dialogMask.style.opacity = '1';
+ dialogMask.style.transition = 'opacity 0.5s ease';
+ }
+ if (dialogWrap) {
+ dialogWrap.style.opacity = '1';
+ dialogWrap.style.transition = 'opacity 0.5s ease';
+ }
+}
+
+export const BlocksSelector = (props) => {
+ const { getAllDataBlocks } = useAllDataBlocks();
+ const allDataBlocks = getAllDataBlocks();
+ const compile = useCompile();
+ const { t } = useTranslation();
+
+ // 转换 allDataBlocks 为 Select 选项
+ const options = useMemo(() => {
+ return allDataBlocks.map(block => {
+ // 防止列表中出现已关闭的弹窗中的区块
+ if (!block.dom?.isConnected) {
+ return null;
+ }
+
+ const title = `${compile(block.collection.title)} #${block.uid.slice(0, 4)}`;
+ return {
+ label: title,
+ value: block.uid,
+ onMouseEnter() {
+ block.highlightBlock();
+ hideDialog('dialog-after-successful-submission');
+ startScrollEndTracking(block.dom, () => {
+ highlightBlock(block.dom.cloneNode(true) as HTMLElement, block.dom.getBoundingClientRect());
+ });
+ },
+ onMouseLeave() {
+ block.unhighlightBlock();
+ showDialog('dialog-after-successful-submission');
+ stopScrollEndTracking(block.dom);
+ unhighlightBlock();
+ }
+ }
+ }).filter(Boolean);
+ }, [allDataBlocks, t]);
+
+ return (
+
+ );
+}
+
export function AfterSuccess() {
const { dn } = useDesignable();
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const { onSuccess } = fieldSchema?.['x-action-settings'] || {};
+ const environmentVariables = useGlobalVariable('$env');
+ const templatePlugin: any = usePlugin('@nocobase/plugin-block-template');
+ const isInBlockTemplateConfigPage = templatePlugin?.isInBlockTemplateConfigPage?.();
+
return (
useVariableProps(environmentVariables),
+ },
+ blocksToRefresh: {
+ type: 'array',
+ title: t('Refresh data blocks'),
+ 'x-decorator': 'FormItem',
+ 'x-use-decorator-props': () => {
+ return {
+ tooltip: t('After successful submission, the selected data blocks will be automatically refreshed.'),
+ };
+ },
+ 'x-component': BlocksSelector,
+ 'x-hidden': isInBlockTemplateConfigPage, // 模板配置页面暂不支持该配置
},
},
} as ISchema
diff --git a/packages/core/client/src/schema-component/antd/action/Action.Drawer.style.ts b/packages/core/client/src/schema-component/antd/action/Action.Drawer.style.ts
index 1974d0dd94..532b942c2e 100644
--- a/packages/core/client/src/schema-component/antd/action/Action.Drawer.style.ts
+++ b/packages/core/client/src/schema-component/antd/action/Action.Drawer.style.ts
@@ -25,7 +25,7 @@ export const useStyles = genStyleHook('nb-action-drawer', (token) => {
},
},
'&.nb-record-picker-selector': {
- '.ant-drawer-wrapper-body': {
+ '.ant-drawer-content': {
backgroundColor: token.colorBgLayout,
},
'.nb-block-item': {
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 49d5eaaf60..fa8e7442a9 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
@@ -10,6 +10,7 @@
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
import { Drawer } from 'antd';
import classNames from 'classnames';
+import { useTranslation } from 'react-i18next';
import React, { FC, startTransition, useCallback, useEffect, useMemo, useState } from 'react';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
@@ -22,6 +23,7 @@ import { useActionContext } from './hooks';
import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer';
import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types';
import { getZIndex, useZIndexContext, zIndexContext } from './zIndexContext';
+import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant';
const MemoizeRecursionField = React.memo(RecursionField);
MemoizeRecursionField.displayName = 'MemoizeRecursionField';
@@ -81,6 +83,7 @@ export const InternalActionDrawer: React.FC = observer(
const { visible, setVisible, openSize = 'middle', drawerProps } = useActionContext();
const schema = useFieldSchema();
const field = useField();
+ const { t } = useTranslation();
const { componentCls, hashId } = useStyles();
const tabContext = useTabsContext();
const parentZIndex = useZIndexContext();
@@ -118,7 +121,6 @@ export const InternalActionDrawer: React.FC = observer(
},
[footerNodeName],
);
-
return (
@@ -126,7 +128,7 @@ export const InternalActionDrawer: React.FC = observer(
{
return (
-
+
);
}),
{ displayName: 'ActionLink' },
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 09562cb54d..9ccb215771 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
@@ -9,7 +9,7 @@
import { css } from '@emotion/css';
import { observer, useField, useFieldSchema } from '@formily/react';
-import { Modal, ModalProps } from 'antd';
+import { Modal, ModalProps, Skeleton } from 'antd';
import classNames from 'classnames';
import React, { FC, startTransition, useEffect, useState } from 'react';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
@@ -53,7 +53,6 @@ const ActionModalContent: FC<{ footerNodeName: string; field: any; schema: any }
if (!deferredVisible) {
return null;
}
-
return (
{
+ if (ready) {
+ return;
+ }
+ if (visible) {
+ const timer = setTimeout(() => setReady(true), delay);
+ return () => clearTimeout(timer);
+ } else {
+ setReady(false);
+ }
+ }, [delay, ready, visible]);
+ return ready;
+}
+
export const InternalActionModal: React.FC> = observer(
(props) => {
- const { footerNodeName = 'Action.Modal.Footer', width, zIndex: _zIndex, ...others } = props;
+ const { footerNodeName = 'Action.Modal.Footer', width, zIndex: _zIndex, delay = 200, ...others } = props;
const { visible, setVisible, openSize = 'middle', modalProps } = useActionContext();
const actualWidth = width ?? openSizeWidthMap.get(openSize);
const schema = useFieldSchema();
@@ -90,6 +105,7 @@ export const InternalActionModal: React.FC> = obse
}
const zIndex = getZIndex('modal', _zIndex || parentZIndex, props.level || 0);
+ const ready = useDelayedVisible(visible, delay); // 200ms 与 Modal 动画时间一致
return (
@@ -154,7 +170,11 @@ export const InternalActionModal: React.FC> = obse
)
}
>
-
+ {ready ? (
+
+ ) : (
+
+ )}
@@ -179,4 +199,17 @@ ActionModal.Footer = observer(
{ displayName: 'ActionModal.Footer' },
);
+ActionModal.FootBar = observer(
+ () => {
+ const field = useField();
+ const schema = useFieldSchema();
+ return (
+
+
+
+ );
+ },
+ { displayName: 'ActionModal.FootBar' },
+);
+
export default ActionModal;
diff --git a/packages/core/client/src/schema-component/antd/action/Action.Page.style.ts b/packages/core/client/src/schema-component/antd/action/Action.Page.style.ts
index 198c7c3df7..0d98c7215a 100644
--- a/packages/core/client/src/schema-component/antd/action/Action.Page.style.ts
+++ b/packages/core/client/src/schema-component/antd/action/Action.Page.style.ts
@@ -20,7 +20,8 @@ export const useActionPageStyle = genStyleHook('nb-action-page', (token) => {
right: 0,
bottom: 0,
backgroundColor: token.colorBgLayout,
- overflow: 'auto',
+ overflowX: 'hidden',
+ overflowY: 'auto',
'.ant-tabs-nav': {
background: token.colorBgContainer,
diff --git a/packages/core/client/src/schema-component/antd/action/Action.style.ts b/packages/core/client/src/schema-component/antd/action/Action.style.ts
index ccf047cf03..14f991d228 100644
--- a/packages/core/client/src/schema-component/antd/action/Action.style.ts
+++ b/packages/core/client/src/schema-component/antd/action/Action.style.ts
@@ -34,6 +34,9 @@ const useStyles = genStyleHook('nb-action', (token) => {
background: 'var(--colorBgSettingsHover)',
border: '0',
pointerEvents: 'none',
+ '&.nb-in-template': {
+ background: 'var(--colorTemplateBgSettingsHover)',
+ },
'> .general-schema-designer-icons': {
position: 'absolute',
right: '2px',
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 cb7657877a..b41b765ed1 100644
--- a/packages/core/client/src/schema-component/antd/action/Action.tsx
+++ b/packages/core/client/src/schema-component/antd/action/Action.tsx
@@ -10,7 +10,7 @@
import { Field } from '@formily/core';
import { observer, Schema, useField, useFieldSchema, useForm } from '@formily/react';
import { isPortalInBody } from '@nocobase/utils/client';
-import { App, Button } from 'antd';
+import { App, Button, Tooltip } from 'antd';
import classnames from 'classnames';
import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -48,9 +48,12 @@ import { ActionContextProvider } from './context';
import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction';
import { ActionContextProps, ActionProps, ComposedAction } from './types';
import { linkageAction, setInitialActionState } from './utils';
+import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant';
+import { BlockContext } from '../../../block-provider/BlockProvider';
// 这个要放到最下面,否则会导致前端单测失败
import { useApp } from '../../../application';
+import { useAllDataBlocks } from '../page/AllDataBlocksProvider';
const useA = () => {
return {
@@ -94,12 +97,16 @@ export const Action: ComposedAction = withDynamicSchemaProps(
const { designable } = useDesignable();
const tarComponent = useComponent(component) || component;
const variables = useVariables();
- const localVariables = useLocalVariables({ currentForm: { values: recordData, readPretty: false } as any });
+ const localVariables = useLocalVariables({
+ currentForm: { values: recordData, readPretty: false } as any,
+ });
const { visibleWithURL, setVisibleWithURL } = usePopupUtils();
const { setSubmitted } = useActionContext();
const { getAriaLabel } = useGetAriaLabelOfAction(title);
const parentRecordData = useCollectionParentRecordData();
const app = useApp();
+ const { getAllDataBlocks } = useAllDataBlocks();
+
useEffect(() => {
if (field.stateOfLinkageRules) {
setInitialActionState(field);
@@ -116,6 +123,7 @@ export const Action: ComposedAction = withDynamicSchemaProps(
condition: v.condition,
variables,
localVariables,
+ conditionType: v.conditionType,
},
app.jsonLogic,
);
@@ -130,37 +138,62 @@ export const Action: ComposedAction = withDynamicSchemaProps(
[onMouseEnter],
);
+ const handleClick = useMemo(() => {
+ return (
+ onClick &&
+ (async (e, callback) => {
+ await onClick?.(e, callback);
+
+ // 执行完 onClick 之后,刷新数据区块
+ const blocksToRefresh = fieldSchema['x-action-settings']?.onSuccess?.blocksToRefresh || [];
+ if (blocksToRefresh.length > 0) {
+ getAllDataBlocks().forEach((block) => {
+ if (blocksToRefresh.includes(block.uid)) {
+ try {
+ block.service?.refresh();
+ } catch (error) {
+ console.error('Failed to refresh block:', block.uid, error);
+ }
+ }
+ });
+ }
+ })
+ );
+ }, [onClick, fieldSchema, getAllDataBlocks]);
+
return (
-
+
+
+
);
}),
{ displayName: 'Action' },
@@ -247,7 +280,6 @@ const InternalAction: React.FC = observer(function Com(prop
const aclCtx = useACLActionParamsContext();
const { run, element, disabled: disableAction } = useAction?.(actionCallback) || ({} as any);
const disabled = form.disabled || field.disabled || field.data?.disabled || propsDisabled || disableAction;
-
const buttonStyle = useMemo(() => {
return {
...style,
@@ -322,11 +354,7 @@ const InternalAction: React.FC = observer(function Com(prop
}
if (addChild) {
- return wrapSSR(
-
- {result}
- ,
- ) as React.ReactElement;
+ return wrapSSR({result} ) as React.ReactElement;
}
return wrapSSR(result) as React.ReactElement;
@@ -341,6 +369,7 @@ Action.Popover = function ActionPopover(props) {
{props.children}
);
+
return (
{
const {
designable,
@@ -558,8 +589,11 @@ const RenderButtonInner = observer(
Designer,
designerProps,
title,
+ isLink,
+ onlyIcon,
...others
} = props;
+ const { t } = useTranslation();
const debouncedClick = useCallback(
debounce(
(e: React.MouseEvent, checkPortal = true) => {
@@ -581,8 +615,26 @@ const RenderButtonInner = observer(
return null;
}
- const actionTitle = title || field?.title;
-
+ const rawTitle = title ?? field?.title;
+ const actionTitle = typeof rawTitle === 'string' ? t(rawTitle, { ns: NAMESPACE_UI_SCHEMA }) : rawTitle;
+ const { opacity, ...restButtonStyle } = buttonStyle;
+ const linkStyle = isLink && opacity ? { opacity } : undefined;
+ const WrapperComponent = React.forwardRef(
+ ({ component: Component = tarComponent || Button, icon, onlyIcon, children, ...restProps }: any, ref) => {
+ return (
+
+ {onlyIcon ? (
+
+ {icon && typeof icon === 'string' ? : icon}
+
+ ) : (
+ {icon && typeof icon === 'string' ? : icon}
+ )}
+ {onlyIcon ? children[1] : children}
+
+ );
+ },
+ );
return (
: icon}
+ icon={typeof icon === 'string' ? : icon}
disabled={disabled}
- style={buttonStyle}
+ style={isLink ? restButtonStyle : buttonStyle}
onClick={process.env.__E2E__ ? handleButtonClick : debouncedClick} // E2E 中的点击操作都是很快的,如果加上 debounce 会导致 E2E 测试失败
- component={tarComponent || Button}
+ component={onlyIcon || tarComponent ? WrapperComponent : tarComponent || Button}
className={classnames(componentCls, hashId, className, 'nb-action')}
type={type === 'danger' ? undefined : type}
+ title={actionTitle}
+ onlyIcon={onlyIcon}
>
- {actionTitle && {actionTitle} }
+ {!onlyIcon && actionTitle && (
+
+ {actionTitle}
+
+ )}
);
diff --git a/packages/core/client/src/schema-component/antd/action/ActionBar.tsx b/packages/core/client/src/schema-component/antd/action/ActionBar.tsx
index 09adfde89d..a1dfc5ecea 100644
--- a/packages/core/client/src/schema-component/antd/action/ActionBar.tsx
+++ b/packages/core/client/src/schema-component/antd/action/ActionBar.tsx
@@ -72,7 +72,7 @@ const InternalActionBar: FC = (props: any) => {
diff --git a/packages/core/client/src/schema-component/antd/action/__tests__/action.test.tsx b/packages/core/client/src/schema-component/antd/action/__tests__/action.test.tsx
index 1c9e0fd932..47b75ef3d6 100644
--- a/packages/core/client/src/schema-component/antd/action/__tests__/action.test.tsx
+++ b/packages/core/client/src/schema-component/antd/action/__tests__/action.test.tsx
@@ -118,8 +118,5 @@ describe('Action.Popover', () => {
});
fireEvent.mouseLeave(btn);
- await waitFor(() => {
- expect(document.querySelector('.ant-popover')).not.toBeInTheDocument();
- });
});
});
diff --git a/packages/core/client/src/schema-component/antd/action/hooks/useGetAfterSuccessVariablesOptions.ts b/packages/core/client/src/schema-component/antd/action/hooks/useGetAfterSuccessVariablesOptions.ts
new file mode 100644
index 0000000000..311c1b7be9
--- /dev/null
+++ b/packages/core/client/src/schema-component/antd/action/hooks/useGetAfterSuccessVariablesOptions.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 { useMemo } from 'react';
+import { useCollection_deprecated, useCollectionFilterOptions } from '../../../../collection-manager';
+import { useCollectionRecordData } from '../../../../data-source';
+import { useTranslation } from 'react-i18next';
+import { useCompile } from '../../../';
+import { usePopupVariable } from '../../../../schema-settings/VariableInput/hooks';
+import { useCurrentRoleVariable } from '../../../../schema-settings/VariableInput/hooks';
+import { useFormBlockContext } from '../../../../block-provider';
+
+export const useAfterSuccessOptions = () => {
+ const collection = useCollection_deprecated();
+ const { t } = useTranslation();
+ const fieldsOptions = useCollectionFilterOptions(collection);
+ const userFieldOptions = useCollectionFilterOptions('users', 'main');
+ const compile = useCompile();
+ const recordData = useCollectionRecordData();
+ const { form } = useFormBlockContext();
+ const [fields, userFields] = useMemo(() => {
+ return [compile(fieldsOptions), compile(userFieldOptions)];
+ }, [fieldsOptions, userFieldOptions]);
+ const { settings: popupRecordSettings, shouldDisplayPopupRecord } = usePopupVariable();
+ const { currentRoleSettings } = useCurrentRoleVariable();
+ return useMemo(() => {
+ return [
+ form && {
+ value: '$record',
+ label: t('Response record', { ns: 'client' }),
+ children: [...fields],
+ },
+ recordData && {
+ value: 'currentRecord',
+ label: t('Current record', { ns: 'client' }),
+ children: [...fields],
+ },
+ shouldDisplayPopupRecord && {
+ ...popupRecordSettings,
+ },
+ {
+ value: 'currentUser',
+ label: t('Current user', { ns: 'client' }),
+ children: userFields,
+ },
+ currentRoleSettings,
+ {
+ value: 'currentTime',
+ label: t('Current time', { ns: 'client' }),
+ children: null,
+ },
+ {
+ value: '$nToken',
+ label: 'API token',
+ children: null,
+ },
+ ].filter(Boolean);
+ }, [recordData, t, fields, form, userFields]);
+};
diff --git a/packages/core/client/src/schema-component/antd/action/hooks/useGetAriaLabelOfAction.ts b/packages/core/client/src/schema-component/antd/action/hooks/useGetAriaLabelOfAction.ts
index 9ce1c8416c..5012fc8d64 100644
--- a/packages/core/client/src/schema-component/antd/action/hooks/useGetAriaLabelOfAction.ts
+++ b/packages/core/client/src/schema-component/antd/action/hooks/useGetAriaLabelOfAction.ts
@@ -32,7 +32,7 @@ export const useGetAriaLabelOfAction = (title: string) => {
let { name: blockName } = useBlockContext() || {};
const actionTitle = title || compile(fieldSchema.title);
collectionName = collectionName ? `-${collectionName}` : '';
- blockName = blockName ? `-${blockName}` : '';
+ blockName = blockName && blockName !== 'action' ? `-${blockName}` : '';
action = action ? `-${action}` : '';
recordName = recordName ? `-${recordName}` : '';
diff --git a/packages/core/client/src/schema-component/antd/action/hooks/useSetAriaLabelForDrawer.ts b/packages/core/client/src/schema-component/antd/action/hooks/useSetAriaLabelForDrawer.ts
index 0846c17817..4b28f6509d 100644
--- a/packages/core/client/src/schema-component/antd/action/hooks/useSetAriaLabelForDrawer.ts
+++ b/packages/core/client/src/schema-component/antd/action/hooks/useSetAriaLabelForDrawer.ts
@@ -18,7 +18,7 @@ export function useSetAriaLabelForDrawer(visible: boolean) {
if (visible) {
// 因为 Action 是点击后渲染内容,所以需要延迟一下
setTimeout(() => {
- const wrappers = [...document.querySelectorAll('.ant-drawer-wrapper-body')];
+ const wrappers = [...document.querySelectorAll('.ant-drawer-body')];
const masks = [...document.querySelectorAll('.ant-drawer-mask')];
// 如果存在多个 mask,最后一个 mask 为当前打开的 mask;wrapper 也是同理
const currentMask = masks[masks.length - 1];
diff --git a/packages/core/client/src/schema-component/antd/action/index.tsx b/packages/core/client/src/schema-component/antd/action/index.tsx
index 80766dbb69..1048a88186 100644
--- a/packages/core/client/src/schema-component/antd/action/index.tsx
+++ b/packages/core/client/src/schema-component/antd/action/index.tsx
@@ -16,5 +16,6 @@ export * from './hooks/useGetAriaLabelOfAction';
export * from './hooks/useGetAriaLabelOfDrawer';
export * from './hooks/useGetAriaLabelOfModal';
export * from './hooks/useGetAriaLabelOfPopover';
+export * from './hooks/useGetAfterSuccessVariablesOptions';
export * from './types';
export * from './zIndexContext';
diff --git a/packages/core/client/src/schema-component/antd/action/types.ts b/packages/core/client/src/schema-component/antd/action/types.ts
index 3df496e1b4..eb95e1ce45 100644
--- a/packages/core/client/src/schema-component/antd/action/types.ts
+++ b/packages/core/client/src/schema-component/antd/action/types.ts
@@ -81,6 +81,7 @@ export interface ActionProps extends ButtonProps {
* @internal
*/
addChild?: boolean;
+ onlyIcon?: boolean;
}
export type ComposedAction = React.FC
& {
@@ -92,6 +93,7 @@ export type ActionDrawerProps = T & {
footerNodeName?: string;
/** 当前弹窗嵌套的层级 */
level?: number;
+ delay?: number;
};
export type ComposedActionDrawer = React.FC> & {
diff --git a/packages/core/client/src/schema-component/antd/action/utils.ts b/packages/core/client/src/schema-component/antd/action/utils.ts
index 5254021916..216035160a 100644
--- a/packages/core/client/src/schema-component/antd/action/utils.ts
+++ b/packages/core/client/src/schema-component/antd/action/utils.ts
@@ -87,12 +87,14 @@ export const linkageAction = async (
condition,
variables,
localVariables,
+ conditionType,
}: {
operator;
field;
condition;
variables: VariablesContextType;
localVariables: VariableOption[];
+ conditionType: 'advanced' | 'basic';
},
jsonLogic: any,
) => {
@@ -101,7 +103,7 @@ export const linkageAction = async (
switch (operator) {
case ActionType.Visible:
- if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
+ if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
displayResult.push(operator);
field.data = field.data || {};
field.data.hidden = false;
@@ -113,7 +115,7 @@ export const linkageAction = async (
field.display = last(displayResult);
break;
case ActionType.Hidden:
- if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
+ if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
field.data = field.data || {};
field.data.hidden = true;
} else {
@@ -122,7 +124,7 @@ export const linkageAction = async (
}
break;
case ActionType.Disabled:
- if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
+ if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
disableResult.push(true);
}
field.stateOfLinkageRules = {
@@ -133,7 +135,7 @@ export const linkageAction = async (
field.componentProps['disabled'] = last(disableResult);
break;
case ActionType.Active:
- if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
+ if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
disableResult.push(false);
} else {
disableResult.push(!!field.componentProps?.['disabled']);
diff --git a/packages/core/client/src/schema-component/antd/appends-tree-select/AppendsTreeSelect.tsx b/packages/core/client/src/schema-component/antd/appends-tree-select/AppendsTreeSelect.tsx
index e62aea2aac..e372f451f9 100644
--- a/packages/core/client/src/schema-component/antd/appends-tree-select/AppendsTreeSelect.tsx
+++ b/packages/core/client/src/schema-component/antd/appends-tree-select/AppendsTreeSelect.tsx
@@ -7,12 +7,13 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import { CloseCircleFilled } from '@ant-design/icons';
+import { CloseCircleFilled, DownOutlined } from '@ant-design/icons';
import { Tag, TreeSelect } from 'antd';
-import type { DefaultOptionType, TreeSelectProps } from 'rc-tree-select/es/TreeSelect';
+import type { TreeSelectProps } from 'rc-tree-select/es/TreeSelect';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { CollectionFieldOptions_deprecated, parseCollectionName, useApp, useCompile } from '../../..';
+import { DefaultOptionType } from 'antd/es/select';
export type AppendsTreeSelectProps = {
value: string[] | string;
@@ -261,6 +262,7 @@ export const AppendsTreeSelect: React.FC }
/>
);
};
diff --git a/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx b/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx
index 190a5e6f23..740d66fd5f 100644
--- a/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx
+++ b/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx
@@ -14,6 +14,7 @@ import React, { useEffect, useMemo, useState } from 'react';
import { useAPIClient, useRequest } from '../../../api-client';
import { useCollectionManager } from '../../../data-source/collection';
import { markRecordAsNew } from '../../../data-source/collection-record/isNewRecord';
+import { getDataSourceHeaders } from '../../../data-source/utils';
import { useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive';
import { useSchemaComponentContext } from '../../hooks';
import { AssociationFieldContext } from './context';
@@ -67,9 +68,11 @@ export const AssociationFieldProvider = observer(
if (_.isUndefined(ids) || _.isNil(ids) || _.isNaN(ids)) {
return Promise.reject(null);
}
+
return api.request({
resource: collectionField.target,
action: Array.isArray(ids) ? 'list' : 'get',
+ headers: getDataSourceHeaders(cm?.dataSource?.key),
params: {
filter: {
[targetKey]: ids,
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 57debf2402..03c67938c8 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
@@ -14,22 +14,28 @@ import { uid } from '@formily/shared';
import { Space, message } from 'antd';
import { isEqual } from 'lodash';
import { isFunction } from 'mathjs';
-import React, { useEffect, useState, useContext } from 'react';
+import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ClearCollectionFieldContext,
NocoBaseRecursionField,
RecordProvider,
+ SchemaComponentContext,
useAPIClient,
useCollectionRecordData,
- SchemaComponentContext,
+ useCollectionManager_deprecated,
} from '../../../';
-import { Action } from '../action';
+import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
import { isVariable } from '../../../variables/utils/isVariable';
import { getInnermostKeyAndValue } from '../../common/utils/uitls';
+import { Action } from '../action';
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
import useServiceOptions, { useAssociationFieldContext } from './hooks';
-import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
+
+const removeIfKeyEmpty = (obj, filterTargetKey) => {
+ if (!obj || typeof obj !== 'object' || !filterTargetKey || Array.isArray(obj)) return obj;
+ return !obj[filterTargetKey] ? undefined : obj;
+};
export const AssociationFieldAddNewer = (props) => {
const schemaComponentCtxValue = useContext(SchemaComponentContext);
@@ -69,6 +75,11 @@ export const filterAnalyses = (filters): any[] => {
return results;
};
+function getFieldPath(str) {
+ const lastIndex = str.lastIndexOf('.');
+ return lastIndex === -1 ? str : str.slice(0, lastIndex);
+}
+
const InternalAssociationSelect = observer(
(props: AssociationSelectProps) => {
const { objectValue = true, addMode: propsAddMode, ...rest } = props;
@@ -88,23 +99,34 @@ const InternalAssociationSelect = observer(
const resource = api.resource(collectionField.target);
const recordData = useCollectionRecordData();
const schemaComponentCtxValue = useContext(SchemaComponentContext);
+ const { getCollection } = useCollectionManager_deprecated();
+ const associationCollection = getCollection(collectionField.target);
+ const { filterTargetKey } = associationCollection;
useEffect(() => {
const initValue = isVariable(field.value) ? undefined : field.value;
const value = Array.isArray(initValue) ? initValue.filter(Boolean) : initValue;
- setInnerValue(value);
- }, [field.value]);
+ const result = removeIfKeyEmpty(value, filterTargetKey);
+ setInnerValue(result);
+ if (!isEqual(field.value, result)) {
+ field.value = result;
+ }
+ }, [field.value, filterTargetKey]);
+
useEffect(() => {
const id = uid();
form.addEffects(id, () => {
//支持深层次子表单
onFieldInputValueChange('*', (fieldPath: any) => {
const linkageFields = filterAnalyses(field.componentProps?.service?.params?.filter) || [];
+ const linageFieldEntire = getFieldPath(fieldPath.address.entire);
+ const targetFieldEntire = getFieldPath(field.address.entire);
if (
linkageFields.includes(fieldPath?.props?.name) &&
field.value &&
isEqual(fieldPath?.indexes, field?.indexes) &&
- fieldPath?.props?.name !== field.props.name
+ fieldPath?.props?.name !== field.props.name &&
+ (!field?.indexes?.length || isEqual(linageFieldEntire, targetFieldEntire))
) {
field.setValue(null);
setInnerValue(null);
@@ -151,7 +173,6 @@ const InternalAssociationSelect = observer(
);
};
- console.log(fieldSchema);
return (
@@ -160,7 +181,7 @@ const InternalAssociationSelect = observer(
{...rest}
size={'middle'}
objectValue={objectValue}
- value={value || innerValue}
+ value={removeIfKeyEmpty(value || innerValue, filterTargetKey)}
service={service}
onChange={(value) => {
const val = value?.length !== 0 ? value : null;
diff --git a/packages/core/client/src/schema-component/antd/association-field/InternalCascadeSelect.tsx b/packages/core/client/src/schema-component/antd/association-field/InternalCascadeSelect.tsx
index a03c545b2d..b59dec00ad 100644
--- a/packages/core/client/src/schema-component/antd/association-field/InternalCascadeSelect.tsx
+++ b/packages/core/client/src/schema-component/antd/association-field/InternalCascadeSelect.tsx
@@ -13,6 +13,8 @@ import { FormProvider, connect, createSchemaField, observer, useField, useFieldS
import { uid } from '@formily/shared';
import { Select as AntdSelect, Input, Space, Spin, Tag } from 'antd';
import dayjs from 'dayjs';
+import { css } from '@emotion/css';
+import { debounce } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAPIClient, useCollectionManager_deprecated } from '../../../';
@@ -130,7 +132,7 @@ const CascadeSelect = connect((props) => {
const response = await resource.list({
pageSize: 200,
params: service?.params,
- filter: mergeFilter([filter]),
+ filter: mergeFilter([filter, service?.params?.filter]),
tree: !filter.parentId ? true : undefined,
});
return response?.data?.data;
@@ -152,7 +154,11 @@ const CascadeSelect = connect((props) => {
} else {
associationField.value = option;
}
- onChange?.(options);
+ if (options.length === 1 && !options[0].value) {
+ onChange?.(null);
+ } else {
+ onChange?.(options);
+ }
};
const onDropdownVisibleChange = async (visible, selectedValue, index) => {
@@ -238,28 +244,38 @@ export const InternalCascadeSelect = observer(
const fieldSchema = useFieldSchema();
const { loading, data: formData } = useDataBlockRequest() || {};
const initialValue = formData?.data?.[fieldSchema.name];
+
+ const handleFormValuesChange = debounce((form) => {
+ if (collectionField.interface === 'm2o') {
+ // 对 m2o 类型字段,提取最后一个非 null 值
+ const value = extractLastNonNullValueObjects(form.values?.[fieldSchema.name]);
+ setTimeout(() => {
+ form.setValuesIn(fieldSchema.name, value);
+ field.value = value;
+ });
+ } else {
+ // 对 select_array 类型字段,过滤掉空对象
+ const value = extractLastNonNullValueObjects(form.values?.select_array).filter(
+ (v) => v && Object.keys(v).length > 0,
+ );
+ setTimeout(() => {
+ field.value = value;
+ });
+ }
+ }, 300);
+
useEffect(() => {
const id = uid();
selectForm.addEffects(id, () => {
onFormValuesChange((form) => {
- if (collectionField.interface === 'm2o') {
- const value = extractLastNonNullValueObjects(form.values?.[fieldSchema.name]);
- setTimeout(() => {
- form.setValuesIn(fieldSchema.name, value);
- field.value = value;
- });
- } else {
- const value = extractLastNonNullValueObjects(form.values?.select_array).filter(
- (v) => v && Object.keys(v).length > 0,
- );
- setTimeout(() => {
- field.value = value;
- });
- }
+ handleFormValuesChange(form);
});
});
+
return () => {
selectForm.removeEffects(id);
+ // 清除防抖定时器
+ handleFormValuesChange.cancel();
};
}, []);
@@ -282,6 +298,24 @@ export const InternalCascadeSelect = observer(
items: {
type: 'void',
'x-component': 'Space',
+ 'x-component-props': {
+ style: {
+ width: '100%',
+ display: 'flex',
+ },
+ className: css`
+ .ant-formily-item-control {
+ max-width: 100% !important;
+ }
+ .ant-space-item:nth-child(1) {
+ flex: 0.1;
+ }
+
+ .ant-space-item:nth-child(2) {
+ flex: 3;
+ }
+ `,
+ },
properties: {
sort: {
type: 'void',
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 afe85386bc..43f32ea728 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
@@ -8,11 +8,13 @@
*/
import { observer, useField, useFieldSchema } from '@formily/react';
+import { transformMultiColumnToSingleColumn } from '@nocobase/utils/client';
import { Select, Space } from 'antd';
import { differenceBy, unionBy } from 'lodash';
import React, { useContext, useMemo, useState } from 'react';
import {
FormProvider,
+ PopupSettingsProvider,
RecordPickerContext,
RecordPickerProvider,
SchemaComponentOptions,
@@ -24,6 +26,7 @@ import {
NocoBaseRecursionField,
RecordProvider,
useCollectionRecordData,
+ useMobileLayout,
} from '../../..';
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
import {
@@ -117,6 +120,7 @@ export const InternalPicker = observer(
collectionField,
currentFormCollection: collectionName,
};
+ const { isMobileLayout } = useMobileLayout();
const getValue = () => {
if (multiple == null) return null;
@@ -147,8 +151,20 @@ export const InternalPicker = observer(
},
};
};
+ const scope = useMemo(
+ () => ({
+ usePickActionProps,
+ useTableSelectorProps,
+ }),
+ [],
+ );
+ const newSchema = useMemo(
+ () => (isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema) : fieldSchema),
+ [isMobileLayout, fieldSchema],
+ );
+
return (
- <>
+
-
+
{
return s['x-component'] === 'AssociationField.Selector';
}}
@@ -233,7 +244,7 @@ export const InternalPicker = observer(
- >
+
);
},
{ displayName: 'InternalPicker' },
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 e09ae7aca2..b908d06fbc 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
@@ -13,7 +13,14 @@ import _ from 'lodash';
import React, { FC, Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDesignable, usePopupSettings } from '../../';
import { WithoutTableFieldResource } from '../../../block-provider';
-import { CollectionRecordProvider, useCollectionManager, useCollectionRecordData } from '../../../data-source';
+import {
+ CollectionRecordProvider,
+ DataBlockProvider,
+ useAssociationName,
+ useCollection,
+ useCollectionManager,
+ useCollectionRecordData,
+} from '../../../data-source';
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
@@ -277,12 +284,14 @@ export const ReadPrettyInternalViewer: React.FC =
const field = useField();
const [visible, setVisible] = useState(false);
const { options: collectionField } = useAssociationFieldContext();
+ const associationName = useAssociationName();
const { visibleWithURL, setVisibleWithURL } = usePopupUtils();
const [btnHover, setBtnHover] = useState(!!visibleWithURL);
const { defaultOpenMode } = useOpenModeContext();
const parentRecordData = useCollectionRecordData();
const [recordData, setRecordData] = useState(null);
const { isPopupVisibleControlledByURL } = usePopupSettings();
+ const collection = useCollection();
const onClickItem = useCallback((props: { recordData: any }) => {
setRecordData(props.recordData);
@@ -335,14 +344,24 @@ export const ReadPrettyInternalViewer: React.FC =
}
return (
-
- {/* The recordData here is only provided when the popup is opened, not the current row record */}
-
-
-
-
-
-
+
+
+ {/* The recordData here is only provided when the popup is opened, not the current row record */}
+
+
+
+
+
+
+
);
};
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 0b5f1a59e2..dad9a33741 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
@@ -14,6 +14,7 @@ import { spliceArrayState } from '@formily/core/esm/shared/internals';
import { observer, useFieldSchema } from '@formily/react';
import { action } from '@formily/reactive';
import { each } from '@formily/shared';
+import { transformMultiColumnToSingleColumn } from '@nocobase/utils/client';
import { useUpdate } from 'ahooks';
import { Button, Card, Divider, Space, Tooltip } from 'antd';
import React, { useCallback, useContext, useMemo, useState } from 'react';
@@ -41,6 +42,7 @@ import {
useRefreshComponent,
} from '../../../formily/NocoBaseRecursionField';
import { RecordIndexProvider, RecordProvider } from '../../../record-provider';
+import { useMobileLayout } from '../../../route-switch/antd/admin-layout';
import { isPatternDisabled, isSystemField } from '../../../schema-settings';
import {
DefaultValueProvider,
@@ -140,6 +142,16 @@ const ToManyNester = observer(
const recordData = useCollectionRecordData();
const collection = useCollection();
const update = useUpdate();
+ const { isMobileLayout } = useMobileLayout();
+
+ const newSchema = useMemo(
+ () => (isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema) : fieldSchema),
+ [isMobileLayout, fieldSchema],
+ );
+ const newParentSchema = useMemo(
+ () => (isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema.parent) : fieldSchema.parent),
+ [isMobileLayout, fieldSchema.parent],
+ );
const refreshComponent = useRefreshComponent();
const refresh = useCallback(() => {
@@ -236,6 +248,7 @@ const ToManyNester = observer(
const filter = list.length ? { $and: [{ [`${targetKey}.$ne`]: list }] } : {};
return filter;
};
+
return field.value.length > 0 ? (
- {!field.readPretty && allowed && (
+ {!field.readPretty && allowed && (!fieldSchema['x-template-uid'] || index > 0) && (
@@ -356,7 +369,7 @@ const ToManyNester = observer(
{
return s['x-component'] === 'AssociationField.Selector';
}}
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 bcfe463899..5b15461293 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
@@ -70,6 +70,15 @@ const subTableContainer = css`
.ant-table-thead .ant-table-cell {
font-weight: normal;
}
+ .ant-pagination {
+ position: absolute;
+ right: 0px;
+ bottom: 0px;
+ }
+ .ant-table-footer {
+ margin-top: 10px;
+ background: inherit;
+ }
`;
const tableClassName = css`
@@ -97,7 +106,7 @@ export const SubTable: any = observer(
const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label');
const recordV2 = useCollectionRecord();
const collection = useCollection();
- const { allowSelectExistingRecord, allowAddnew, allowDisassociation } = field.componentProps;
+ const { allowSelectExistingRecord, allowAddnew, allowDisassociation, enableIndexÏColumn } = field.componentProps;
useSubTableSpecialCase({ rootField: field, rootSchema: schema });
@@ -189,6 +198,7 @@ export const SubTable: any = observer(
onChange: (page, pageSize) => {
setCurrentPage(page);
setPageSize(pageSize);
+ field.componentProps.pageSize = pageSize;
field.onInput(field.value);
},
showSizeChanger: true,
@@ -217,7 +227,6 @@ export const SubTable: any = observer(
},
};
};
-
return (
@@ -251,38 +260,40 @@ export const SubTable: any = observer(
locale={{
emptyText: {field.editable ? t('Please add or select record') : t('No data')} ,
}}
+ enableIndexÏColumn={enableIndexÏColumn !== false}
+ footer={() => (
+
+ {field.editable && (
+
+ {allowAddnew !== false && (
+
+ {t('Add new')}
+
+ }
+ />
+ )}
+ {allowSelectExistingRecord && (
+
+ {t('Select record')}
+
+ }
+ />
+ )}
+
+ )}
+
+ )}
/>
- {field.editable && (
-
- {allowAddnew !== false && (
-
- {t('Add new')}
-
- }
- />
- )}
- {allowSelectExistingRecord && (
-
- {t('Select record')}
-
- }
- />
- )}
-
- )}
diff --git a/packages/core/client/src/schema-component/antd/association-field/Table.tsx b/packages/core/client/src/schema-component/antd/association-field/Table.tsx
index d50eb018b1..867ad6a9e4 100644
--- a/packages/core/client/src/schema-component/antd/association-field/Table.tsx
+++ b/packages/core/client/src/schema-component/antd/association-field/Table.tsx
@@ -144,7 +144,7 @@ const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }, paginat
-
+
@@ -602,12 +602,6 @@ const InternalNocoBaseTable = React.memo(
height: 100%;
display: flex;
flex-direction: column;
- .ant-table-expanded-row-fixed {
- min-height: ${tableHeight}px;
- }
- .ant-table-body {
- min-height: ${tableHeight}px;
- }
.ant-table-cell {
padding: 16px 8px;
}
@@ -679,6 +673,7 @@ export const Table: any = withDynamicSchemaProps(
onExpand,
loading,
onClickRow,
+ enableIndexÏColumn,
...others
} = { ...others1, ...others2 } as any;
const field = useArrayField(others);
@@ -831,65 +826,67 @@ export const Table: any = withDynamicSchemaProps(
const restProps = useMemo(
() => ({
- rowSelection: memoizedRowSelection
- ? {
- type: 'checkbox',
- selectedRowKeys: selectedRowKeys,
- onChange(selectedRowKeys: any[], selectedRows: any[]) {
- field.data = field.data || {};
- field.data.selectedRowKeys = selectedRowKeys;
- field.data.selectedRowData = selectedRows;
- setSelectedRowKeys(selectedRowKeys);
- onRowSelectionChange?.(selectedRowKeys, selectedRows);
- },
- getCheckboxProps(record) {
- return {
- 'aria-label': `checkbox`,
- };
- },
- renderCell: (checked, record, index, originNode) => {
- if (!dragSort && !showIndex) {
- return originNode;
- }
- const current = paginationProps?.current;
+ rowSelection: enableIndexÏColumn
+ ? memoizedRowSelection
+ ? {
+ type: 'checkbox',
+ selectedRowKeys: selectedRowKeys,
+ onChange(selectedRowKeys: any[], selectedRows: any[]) {
+ field.data = field.data || {};
+ field.data.selectedRowKeys = selectedRowKeys;
+ field.data.selectedRowData = selectedRows;
+ setSelectedRowKeys(selectedRowKeys);
+ onRowSelectionChange?.(selectedRowKeys, selectedRows);
+ },
+ getCheckboxProps(record) {
+ return {
+ 'aria-label': `checkbox`,
+ };
+ },
+ renderCell: (checked, record, index, originNode) => {
+ if (!dragSort && !showIndex) {
+ return originNode;
+ }
+ const current = paginationProps?.current;
- const pageSize = paginationProps?.pageSize || 20;
- if (current) {
- index = index + (current - 1) * pageSize + 1;
- } else {
- index = index + 1;
- }
- if (record.__index) {
- index = extractIndex(record.__index);
- }
- return (
-
-
- {dragSort &&
}
- {showIndex &&
}
-
- {isRowSelect && (
-
- {originNode}
+ const pageSize = paginationProps?.pageSize || 20;
+ if (current) {
+ index = index + (current - 1) * pageSize + 1;
+ } else {
+ index = index + 1;
+ }
+ if (record.__index) {
+ index = extractIndex(record.__index);
+ }
+ return (
+
+
+ {dragSort &&
}
+ {showIndex &&
}
- )}
-
- );
- },
- ...memoizedRowSelection,
- }
+ {isRowSelect && (
+
+ {originNode}
+
+ )}
+
+ );
+ },
+ ...memoizedRowSelection,
+ }
+ : undefined
: undefined,
}),
[
@@ -903,6 +900,7 @@ export const Table: any = withDynamicSchemaProps(
isRowSelect,
memoizedRowSelection,
paginationProps,
+ enableIndexÏColumn,
],
);
diff --git a/packages/core/client/src/schema-component/antd/association-field/hooks.tsx b/packages/core/client/src/schema-component/antd/association-field/hooks.tsx
index f44f67906f..05b042461d 100644
--- a/packages/core/client/src/schema-component/antd/association-field/hooks.tsx
+++ b/packages/core/client/src/schema-component/antd/association-field/hooks.tsx
@@ -18,6 +18,7 @@ import { Collection } from '../../../data-source';
import { isInFilterFormBlock } from '../../../filter-provider';
import { mergeFilter } from '../../../filter-provider/utils';
import { useRecord } from '../../../record-provider';
+import { useMobileLayout } from '../../../route-switch/antd/admin-layout';
import { useDesignable } from '../../hooks';
import { AssociationFieldMode } from './AssociationFieldModeProvider';
import { AssociationFieldContext } from './context';
@@ -25,8 +26,14 @@ import { AssociationFieldContext } from './context';
export const useInsertSchema = (component) => {
const fieldSchema = useFieldSchema();
const { insertAfterBegin } = useDesignable();
+ const { isMobileLayout } = useMobileLayout();
const insert = useCallback(
(ss) => {
+ // 移动端的布局更改了本地的 schema 的结构(数据库里的没改),所以不能插入新的 schema,否则可能会导致 schema 的结构出问题
+ if (isMobileLayout) {
+ return;
+ }
+
const schema = fieldSchema.reduceProperties((buf, s) => {
if (s['x-component'] === 'AssociationField.' + component) {
return s;
@@ -37,7 +44,7 @@ export const useInsertSchema = (component) => {
insertAfterBegin(cloneDeep(ss));
}
},
- [component, fieldSchema, insertAfterBegin],
+ [component, fieldSchema, insertAfterBegin, isMobileLayout],
);
return insert;
};
@@ -53,6 +60,7 @@ export function useAssociationFieldContext
() {
};
}
+// 用于获取关系字段请求数据时所需的一些参数
export default function useServiceOptions(props) {
const { action = 'list', service, useOriginalFilter } = props;
const fieldSchema = useFieldSchema();
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 c439277ae6..eb163b25bd 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
@@ -41,6 +41,7 @@ export const AssociationFilterItem = withDynamicSchemaProps(
handleSearchInput: _handleSearchInput,
params,
run,
+ dataScopeFilter,
valueKey: _valueKey,
labelKey: _labelKey,
defaultCollapse,
@@ -94,7 +95,7 @@ export const AssociationFilterItem = withDynamicSchemaProps(
if (searchVisible || filter) {
run({
...params?.[0],
- filter: undefined,
+ filter: dataScopeFilter,
});
}
setSearchVisible(!searchVisible);
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 97d07c76c5..35dbbcaa70 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
@@ -107,7 +107,7 @@ AssociationFilter.BlockDesigner = AssociationFilterBlockDesigner;
AssociationFilter.useAssociationField = () => {
const fieldSchema = useFieldSchema();
const collection = useCollection();
- return React.useMemo(() => collection.getField(fieldSchema.name as any), [fieldSchema.name]);
+ 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/BlockItem.tsx b/packages/core/client/src/schema-component/antd/block-item/BlockItem.tsx
index deb129be5a..6e6b2c4172 100644
--- a/packages/core/client/src/schema-component/antd/block-item/BlockItem.tsx
+++ b/packages/core/client/src/schema-component/antd/block-item/BlockItem.tsx
@@ -6,10 +6,9 @@
* 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, { useMemo } from 'react';
import { useFieldSchema } from '@formily/react';
import cls from 'classnames';
-import React, { useMemo } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useSchemaToolbarRender } from '../../../application';
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
@@ -18,6 +17,8 @@ import { useProps } from '../../hooks';
import { ErrorFallback } from '../error-fallback';
import { useStyles } from './BlockItem.style';
import { useGetAriaLabelOfBlockItem } from './hooks/useGetAriaLabelOfBlockItem';
+import { useCollection } from '../../../data-source';
+import { BlockLinkageRuleProvider } from '../../../modules/blocks/BlockLinkageRuleProvider';
export interface BlockItemProps {
name?: string;
@@ -35,8 +36,9 @@ export const BlockItem: React.FC = withDynamicSchemaProps(
const { render } = useSchemaToolbarRender(fieldSchema);
const { getAriaLabel } = useGetAriaLabelOfBlockItem(props.name);
const label = useMemo(() => getAriaLabel(), [getAriaLabel]);
-
- return (
+ const collection = useCollection();
+ const markdownField = fieldSchema['x-decorator'] === 'FormItem' && fieldSchema['x-block-linkage-rules'];
+ const content = (
= withDynamicSchemaProps(
);
+
+ return collection && !markdownField ? content : {content} ;
},
{ displayName: 'BlockItem' },
);
diff --git a/packages/core/client/src/schema-component/antd/block-item/BlockItemCard.tsx b/packages/core/client/src/schema-component/antd/block-item/BlockItemCard.tsx
index e23bfb7b10..8845d27153 100644
--- a/packages/core/client/src/schema-component/antd/block-item/BlockItemCard.tsx
+++ b/packages/core/client/src/schema-component/antd/block-item/BlockItemCard.tsx
@@ -9,8 +9,11 @@
import { Card, CardProps } from 'antd';
import React, { useMemo, useRef, useEffect, createContext, useState } from 'react';
+import { useTranslation } from 'react-i18next';
import { useToken } from '../../../style';
import { MarkdownReadPretty } from '../markdown';
+import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant';
+import { useCollection } from '../../../data-source';
export const BlockItemCardContext = createContext({});
@@ -22,6 +25,9 @@ export const BlockItemCard = React.forwardRef((
}, [token.marginBlock]);
const [titleHeight, setTitleHeight] = useState(0);
const titleRef = useRef(null);
+ const { t } = useTranslation();
+ const collection = useCollection();
+ console.log();
useEffect(() => {
const timer = setTimeout(() => {
if (titleRef.current) {
@@ -38,10 +44,10 @@ export const BlockItemCard = React.forwardRef((
}, [blockTitle, description]);
const title = (blockTitle || description) && (
-
{blockTitle}
+
{t(blockTitle, { ns: NAMESPACE_UI_SCHEMA })}
{description && (
{
let { name: blockName } = useBlockContext() || {};
// eslint-disable-next-line prefer-const
let { name: collectionName, getField } = useCollection_deprecated();
- blockName = name || blockName;
+ blockName = name || (blockName !== 'action' ? blockName : '');
const title = compile(fieldSchema['title']) || compile(getField(fieldSchema.name)?.uiSchema?.title);
diff --git a/packages/core/client/src/schema-component/antd/cascader/Cascader.tsx b/packages/core/client/src/schema-component/antd/cascader/Cascader.tsx
index addae97746..055e169f5c 100644
--- a/packages/core/client/src/schema-component/antd/cascader/Cascader.tsx
+++ b/packages/core/client/src/schema-component/antd/cascader/Cascader.tsx
@@ -116,6 +116,7 @@ export const Cascader = withDynamicSchemaProps(
{
expect(container).toMatchInlineSnapshot(`