mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-04 21:19:27 +08:00
Merge branch 'next' into fix(auth)/user-have-no-role
This commit is contained in:
commit
d44244d1f4
119
.github/workflows/build-internal-image.yml
vendored
Normal file
119
.github/workflows/build-internal-image.yml
vendored
Normal file
@ -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 }}
|
4
.github/workflows/manual-merge.yml
vendored
4
.github/workflows/manual-merge.yml
vendored
@ -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:
|
||||
|
619
.github/workflows/manual-npm-publish-license-kit.yml
vendored
Normal file
619
.github/workflows/manual-npm-publish-license-kit.yml
vendored
Normal file
@ -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 }}
|
70
.github/workflows/nocobase-test-backend.yml
vendored
70
.github/workflows/nocobase-test-backend.yml
vendored
@ -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:
|
||||
|
4
.github/workflows/nocobase-test-windows.yml
vendored
4
.github/workflows/nocobase-test-windows.yml
vendored
@ -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
|
||||
|
828
CHANGELOG.md
828
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
|
||||
|
@ -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
|
||||
|
||||
### 🐛 修复
|
||||
|
@ -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.
|
||||
|
||||
|
@ -2,14 +2,10 @@
|
||||
|
||||
https://github.com/user-attachments/assets/cf08bfe5-e6e6-453c-8b96-350a6a8bed17
|
||||
|
||||
## ご協力ありがとうございます!
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/4112" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4112" alt="nocobase%2Fnocobase | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
<a href="https://www.producthunt.com/posts/nocobase?embed=true&utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-nocobase" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=456520&theme=light&period=weekly&topic_id=267" alt="NocoBase - Scalability-first, open-source no-code platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
## リリースノート
|
||||
|
||||
リリースノートは[ブログ](https://www.nocobase.com/ja/blog/timeline)で随時更新され、週ごとにまとめて公開しています。
|
||||
</p>
|
||||
|
||||
## 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. データモデル駆動
|
||||
|
22
README.md
22
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!
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/4112" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4112" alt="nocobase%2Fnocobase | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
<a href="https://www.producthunt.com/posts/nocobase?embed=true&utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-nocobase" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=456520&theme=light&period=weekly&topic_id=267" alt="NocoBase - Scalability-first, open-source no-code platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
## Release Notes
|
||||
|
||||
Our [blog](https://www.nocobase.com/en/blog/timeline) is regularly updated with release notes and provides a weekly summary.
|
||||
</p>
|
||||
|
||||
## 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
|
||||
|
@ -2,13 +2,10 @@
|
||||
|
||||
https://github.com/nocobase/nocobase/assets/1267426/29623e45-9a48-4598-bb9e-9dd173ade553
|
||||
|
||||
## 感谢支持
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/4112" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4112" alt="nocobase%2Fnocobase | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
<a href="https://www.producthunt.com/posts/nocobase?embed=true&utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-nocobase" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=456520&theme=light&period=weekly&topic_id=267" alt="NocoBase - Scalability-first, open-source no-code platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
## 发布日志
|
||||
我们的[博客](https://www.nocobase.com/cn/blog/timeline)会及时更新发布日志,并每周进行汇总。
|
||||
</p>
|
||||
|
||||
## 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. 数据模型驱动
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 \
|
||||
|
@ -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;
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.6.0-beta.19",
|
||||
"version": "1.7.0-beta.26",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"npmClientArgs": ["--ignore-engines"],
|
||||
|
@ -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": {}
|
||||
}
|
||||
}
|
@ -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": {
|
||||
|
579
packages/core/acl/src/__tests__/acl-role.test.ts
Normal file
579
packages/core/acl/src/__tests__/acl-role.test.ts
Normal file
@ -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']),
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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) {
|
||||
|
@ -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<CanArgs, 'role'>) => {
|
||||
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<CanArgs, 'role'>) => {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -14,3 +14,4 @@ export * from './acl-resource';
|
||||
export * from './acl-role';
|
||||
export * from './skip-middleware';
|
||||
export * from './errors';
|
||||
export * from './utils';
|
||||
|
269
packages/core/acl/src/utils/acl-role.ts
Normal file
269
packages/core/acl/src/utils/acl-role.ts
Normal file
@ -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<string, any> = {
|
||||
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<string, object>;
|
||||
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<string>();
|
||||
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<string, Set<number>>();
|
||||
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<string>();
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
10
packages/core/acl/src/utils/index.ts
Normal file
10
packages/core/acl/src/utils/index.ts
Normal file
@ -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';
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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();
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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'],
|
||||
|
@ -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);
|
||||
}
|
||||
|
4
packages/core/cache/package.json
vendored
4
packages/core/cache/package.json
vendored
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
81
packages/core/cli/src/commands/locale.js
Normal file
81
packages/core/cli/src/commands/locale.js
Normal file
@ -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) => {});
|
||||
};
|
122
packages/core/cli/src/commands/locale/cronstrue.js
Normal file
122
packages/core/cli/src/commands/locale/cronstrue.js
Normal file
@ -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;
|
||||
};
|
@ -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"
|
||||
]
|
||||
}
|
17
packages/core/cli/src/commands/locale/react-js-cron/index.js
vendored
Normal file
17
packages/core/cli/src/commands/locale/react-js-cron/index.js
vendored
Normal file
@ -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'];
|
||||
}
|
@ -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": ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"]
|
||||
}
|
@ -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": ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"]
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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') {
|
||||
|
71
packages/core/cli/src/commands/update-deps.js
Normal file
71
packages/core/cli/src/commands/update-deps.js
Normal file
@ -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();
|
||||
});
|
||||
};
|
@ -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();
|
||||
});
|
||||
};
|
||||
|
@ -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 () {
|
||||
|
39
packages/core/cli/templates/create-app-package.json
Normal file
39
packages/core/cli/templates/create-app-package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
1
packages/core/cli/templates/plugin/src/locale/nl-NL.json
Normal file
1
packages/core/cli/templates/plugin/src/locale/nl-NL.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
@ -234,6 +234,10 @@ export default defineConfig({
|
||||
"title": "Filter",
|
||||
"link": "/components/filter"
|
||||
},
|
||||
{
|
||||
"title": "LinkageFilter",
|
||||
"link": "/components/linkage-filter"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -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",
|
||||
|
@ -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 <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
|
||||
}
|
||||
//视图表无编辑权限时不显示
|
||||
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 <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
|
||||
}
|
||||
return null;
|
||||
return <ACLActionParamsContext.Provider value={false}>{props.children}</ACLActionParamsContext.Provider>;
|
||||
}
|
||||
return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
|
||||
};
|
||||
|
||||
export const useACLFieldWhitelist = () => {
|
||||
const params = useContext(ACLActionParamsContext);
|
||||
const whitelist = useMemo(() => {
|
||||
|
@ -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: (
|
||||
<>
|
||||
<Checkbox
|
||||
checked={allChecked}
|
||||
onChange={async () => {
|
||||
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 <Checkbox checked={checked} onChange={() => handleChange(checked, record)} />;
|
||||
{
|
||||
dataIndex: 'accessible',
|
||||
title: (
|
||||
<>
|
||||
<Checkbox
|
||||
checked={allChecked}
|
||||
onChange={async () => {
|
||||
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 <Checkbox checked={checked} onChange={() => handleChange(checked, record)} />;
|
||||
},
|
||||
},
|
||||
},
|
||||
]}
|
||||
] as TableProps['columns']
|
||||
}
|
||||
dataSource={settings
|
||||
.filter((v) => {
|
||||
return v.isTopLevel !== false;
|
||||
|
@ -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: (
|
||||
<>
|
||||
<Checkbox
|
||||
checked={allChecked}
|
||||
onChange={async (value) => {
|
||||
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 <Checkbox checked={checked} onChange={() => handleChange(checked, schema)} />;
|
||||
columns={
|
||||
[
|
||||
{
|
||||
dataIndex: 'title',
|
||||
title: t('Menu item title'),
|
||||
},
|
||||
},
|
||||
]}
|
||||
{
|
||||
dataIndex: 'accessible',
|
||||
title: (
|
||||
<>
|
||||
<Checkbox
|
||||
checked={allChecked}
|
||||
onChange={async (value) => {
|
||||
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 <Checkbox checked={checked} onChange={() => handleChange(checked, schema)} />;
|
||||
},
|
||||
},
|
||||
] as TableProps['columns']
|
||||
}
|
||||
dataSource={translateTitle(items)}
|
||||
/>
|
||||
);
|
||||
|
@ -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 ? (
|
||||
<Tag color={'green'}>{t('Action on new records')}</Tag>
|
||||
) : (
|
||||
<Tag color={'geekblue'}>{t('Action on existing records')}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
dataIndex: 'enabled',
|
||||
title: t('Allow'),
|
||||
render: (enabled, action) => (
|
||||
<Checkbox
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
toggleAction(action.name);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
dataIndex: 'scope',
|
||||
title: t('Data scope'),
|
||||
render: (value, action) =>
|
||||
!action.onNewRecord && (
|
||||
<ScopeSelect
|
||||
value={value}
|
||||
onChange={(scope) => {
|
||||
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 ? (
|
||||
<Tag color={'green'}>{t('Action on new records')}</Tag>
|
||||
) : (
|
||||
<Tag color={'geekblue'}>{t('Action on existing records')}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
dataIndex: 'enabled',
|
||||
title: t('Allow'),
|
||||
render: (enabled, action) => (
|
||||
<Checkbox
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
toggleAction(action.name);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
},
|
||||
{
|
||||
dataIndex: 'scope',
|
||||
title: t('Data scope'),
|
||||
render: (value, action) =>
|
||||
!action.onNewRecord && (
|
||||
<ScopeSelect
|
||||
value={value}
|
||||
onChange={(scope) => {
|
||||
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: (
|
||||
<>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={() => {
|
||||
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) => (
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
aria-label={`${action.name}_checkbox`}
|
||||
onChange={() => {
|
||||
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) => (
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
aria-label={`${action.name}_checkbox`}
|
||||
onChange={() => {
|
||||
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']
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
</FormLayout>
|
||||
|
@ -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 ? (
|
||||
<Tag color={'green'}>{t('Action on new records')}</Tag>
|
||||
) : (
|
||||
<Tag color={'geekblue'}>{t('Action on existing records')}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
dataIndex: 'enabled',
|
||||
title: t('Allow'),
|
||||
render: (enabled, action) => (
|
||||
<Checkbox
|
||||
checked={enabled}
|
||||
aria-label={`${action.name}_checkbox`}
|
||||
onChange={(e) => {
|
||||
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 && (
|
||||
<Select
|
||||
data-testid="select-data-scope"
|
||||
popupMatchSelectWidth={false}
|
||||
size={'small'}
|
||||
value={scope}
|
||||
options={[
|
||||
{ label: t('All records'), value: 'all' },
|
||||
{ label: t('Own records'), value: 'own' },
|
||||
]}
|
||||
onChange={(value) => {
|
||||
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 ? (
|
||||
<Tag color={'green'}>{t('Action on new records')}</Tag>
|
||||
) : (
|
||||
<Tag color={'geekblue'}>{t('Action on existing records')}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
dataIndex: 'enabled',
|
||||
title: t('Allow'),
|
||||
render: (enabled, action) => (
|
||||
<Checkbox
|
||||
checked={enabled}
|
||||
aria-label={`${action.name}_checkbox`}
|
||||
onChange={(e) => {
|
||||
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 && (
|
||||
<Select
|
||||
data-testid="select-data-scope"
|
||||
popupMatchSelectWidth={false}
|
||||
size={'small'}
|
||||
value={scope}
|
||||
options={[
|
||||
{ label: t('All records'), value: 'all' },
|
||||
{ label: t('Own records'), value: 'own' },
|
||||
]}
|
||||
onChange={(value) => {
|
||||
scopes[action.name] = value;
|
||||
onChange(toFieldValue(scopes));
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
] as TableProps['columns']
|
||||
}
|
||||
dataSource={availableActions?.map((item) => {
|
||||
let scope = 'all';
|
||||
let enabled = false;
|
||||
|
@ -236,7 +236,6 @@ export const scopesSchema: ISchema = {
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
icon: 'EditOutlined',
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
|
@ -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 ||
|
||||
|
@ -108,6 +108,7 @@ export class Application {
|
||||
public name: string;
|
||||
public favicon: string;
|
||||
public globalVars: Record<string, any> = {};
|
||||
public globalVarCtxs: Record<string, any> = {};
|
||||
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 = () => {
|
||||
|
@ -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<T = any> = ComponentType<T> | string;
|
||||
export interface RouteType extends Omit<RouteObject, 'children' | 'Component'> {
|
||||
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<RouteType>(routes, pathname, this.basename);
|
||||
}
|
||||
|
||||
isSkippedAuthCheckRoute(pathname: string) {
|
||||
const matchedRoutes = this.matchRoutes(pathname);
|
||||
return matchedRoutes.some((match) => {
|
||||
return match?.route?.skipAuthCheck === true;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
@ -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: <Hello />, 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 <div>BaseLayout {props.children}</div>;
|
||||
};
|
||||
render(<RouterComponent BaseLayout={BaseLayout} />);
|
||||
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 <div>BaseLayout {props.children}</div>;
|
||||
};
|
||||
render(<RouterComponent BaseLayout={BaseLayout} />);
|
||||
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', () => {
|
||||
|
@ -11,10 +11,11 @@ import React, { FC } from 'react';
|
||||
import { MainComponent } from './MainComponent';
|
||||
|
||||
const Loading: FC = () => <div>Loading...</div>;
|
||||
const AppError: FC<{ error: Error }> = ({ error }) => {
|
||||
const AppError: FC<{ error: Error & { title?: string } }> = ({ error }) => {
|
||||
const title = error?.title || 'App Error';
|
||||
return (
|
||||
<div>
|
||||
<div>App Error</div>
|
||||
<div>{title}</div>
|
||||
{error?.message}
|
||||
{process.env.__TEST__ && error?.stack}
|
||||
</div>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -12,3 +12,4 @@ export * from './useAppSpin';
|
||||
export * from './usePlugin';
|
||||
export * from './useRouter';
|
||||
export * from './useGlobalVariable';
|
||||
export * from './useAclSnippets';
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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 <SchemaInitializerMenu items={menuItems}></SchemaInitializerMenu>;
|
||||
return <SchemaInitializerMenu disabled={disabled} items={menuItems}></SchemaInitializerMenu>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
|
@ -34,20 +34,6 @@ export interface SchemaSettingsChildrenProps {
|
||||
children: SchemaSettingsItemType[];
|
||||
}
|
||||
|
||||
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 SchemaSettingsChildErrorFallback: FC<
|
||||
FallbackProps & {
|
||||
title: string;
|
||||
@ -113,6 +99,19 @@ export const SchemaSettingsChild: FC<SchemaSettingsItemType> = (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();
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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) => {
|
||||
}}
|
||||
>
|
||||
<DefaultValueProvider isAllowToSetDefaultValue={() => false}>
|
||||
<FormBlockProvider name="filter-form" {...props}></FormBlockProvider>
|
||||
<FormBlockProvider name="filter-form" {...props} confirmBeforeClose={false}></FormBlockProvider>
|
||||
</DefaultValueProvider>
|
||||
</ActionBarProvider>
|
||||
</DatePickerProvider>
|
||||
|
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -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, any>): 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('.'));
|
||||
}
|
||||
});
|
||||
|
@ -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<MenuProps['items']>(() => {
|
||||
return getFieldOptions()
|
||||
.map((option) => {
|
||||
.map((option): ItemType & { title: string; children?: ItemType[] } => {
|
||||
if (option?.children?.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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")}}',
|
||||
|
@ -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")}}',
|
||||
|
@ -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)) {
|
||||
|
@ -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<string, CollectionFieldOptions[]> = {};
|
||||
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<InheritanceCollectionMixin>(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();
|
||||
|
@ -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<InheritanceCollectionMixin>('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<InheritanceCollectionMixin>('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<InheritanceCollectionMixin>('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<InheritanceCollectionMixin>('child');
|
||||
const grandChild = collectionManager.getCollection<InheritanceCollectionMixin>('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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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),
|
||||
}),
|
||||
|
30
packages/core/client/src/common/AppNotFound.tsx
Normal file
30
packages/core/client/src/common/AppNotFound.tsx
Normal file
@ -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 (
|
||||
<Result
|
||||
status="404"
|
||||
title="404"
|
||||
subTitle={t('Sorry, the page you visited does not exist.')}
|
||||
extra={
|
||||
<Button onClick={() => navigate('/', { replace: true })} type="primary">
|
||||
Back Home
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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<any>(null);
|
||||
return (
|
||||
@ -36,6 +43,7 @@ export function SelectWithTitle({ title, defaultValue, onChange, options, fieldN
|
||||
>
|
||||
{title}
|
||||
<Select
|
||||
{...others}
|
||||
open={open}
|
||||
data-testid={`select-${title}`}
|
||||
popupMatchSelectWidth={false}
|
||||
|
@ -7,5 +7,6 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export * from './AppNotFound';
|
||||
export * from './SelectWithTitle';
|
||||
export * from './useFieldComponentName';
|
||||
|
@ -37,6 +37,7 @@ export const CSSVariableProvider = ({ children }) => {
|
||||
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;
|
||||
|
@ -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 <Component {...props} {...dynamicProps} />;
|
||||
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 <Component {...mergedProps} />;
|
||||
};
|
||||
|
||||
export const CollectionField = connect((props) => {
|
||||
|
@ -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<Partial<AllDataBlockProps>> = withDynamicSche
|
||||
<CollectionManagerProvider dataSource={dataSource}>
|
||||
<AssociationOrCollectionProvider collection={collection} association={association}>
|
||||
<ACLCollectionProvider>
|
||||
<DataBlockResourceProvider>
|
||||
<BlockRequestProvider>
|
||||
<DataBlockCollector params={props.params}>
|
||||
<RerenderDataBlockProvider>{children}</RerenderDataBlockProvider>
|
||||
</DataBlockCollector>
|
||||
</BlockRequestProvider>
|
||||
</DataBlockResourceProvider>
|
||||
<BlockLinkageRuleProvider>
|
||||
<DataBlockResourceProvider>
|
||||
<BlockRequestProvider>
|
||||
<DataBlockCollector params={props.params}>
|
||||
<RerenderDataBlockProvider>{children}</RerenderDataBlockProvider>
|
||||
</DataBlockCollector>
|
||||
</BlockRequestProvider>
|
||||
</DataBlockResourceProvider>
|
||||
</BlockLinkageRuleProvider>
|
||||
</ACLCollectionProvider>
|
||||
</AssociationOrCollectionProvider>
|
||||
</CollectionManagerProvider>
|
||||
|
@ -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<InheritanceCollectionMixin>(collectionName)
|
||||
?.getAllCollectionsInheritChain();
|
||||
?.getInheritChain();
|
||||
},
|
||||
[dm],
|
||||
);
|
||||
|
@ -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",
|
||||
|
@ -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<HTMLDivElement | null>(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));
|
||||
},
|
||||
|
@ -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: [
|
||||
|
47
packages/core/client/src/filter-provider/highlightBlock.ts
Normal file
47
packages/core/client/src/filter-provider/highlightBlock.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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];
|
||||
}
|
||||
|
@ -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<CollectionFieldOptions>({});
|
||||
@ -266,6 +267,7 @@ export const NocoBaseRecursionField: ReactFC<INocoBaseRecursionFieldProps> = Rea
|
||||
values,
|
||||
isUseFormilyField = true,
|
||||
uiSchema,
|
||||
parentSchema,
|
||||
} = props;
|
||||
const basePath = useBasePath(props);
|
||||
const newFieldSchemaRef = useRef(null);
|
||||
@ -279,6 +281,14 @@ export const NocoBaseRecursionField: ReactFC<INocoBaseRecursionFieldProps> = 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);
|
||||
|
@ -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__,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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 {
|
||||
|
12
packages/core/client/src/i18n/constant.ts
Normal file
12
packages/core/client/src/i18n/constant.ts
Normal file
@ -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 };
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user