Merge branch 'next' into T-4910

This commit is contained in:
mytharcher 2024-10-14 17:23:56 +08:00
commit 2ac5457583
841 changed files with 32419 additions and 5688 deletions

View File

@ -37,7 +37,7 @@ CLUSTER_MODE=
################# DATABASE ################# ################# DATABASE #################
# postgres | msysql | mariadb | sqlite # postgres | mysql | mariadb | sqlite
DB_DIALECT=postgres DB_DIALECT=postgres
DB_TABLE_PREFIX= DB_TABLE_PREFIX=
DB_HOST=localhost DB_HOST=localhost

View File

@ -1,4 +1,4 @@
name: auto-merge name: Auto merge main -> next
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@ -13,17 +13,29 @@ jobs:
push-commit: push-commit:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.NOCOBASE_APP_ID }}
private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }}
skip-token-revoke: true
- name: Get GitHub App User ID
id: get-user-id
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: nocobase/nocobase repository: nocobase/nocobase
ssh-key: ${{ secrets.NOCOBASE_DEPLOY_KEY }} token: ${{ steps.app-token.outputs.token }}
persist-credentials: true persist-credentials: true
fetch-depth: 0 fetch-depth: 0
- name: main -> next(nocobase) - name: main -> next(nocobase)
run: | run: |
git config --global user.email "actions@github.com" git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.name "GitHub Actions Bot" git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>'
git checkout main git checkout main
git pull origin main git pull origin main
git checkout next git checkout next
@ -33,7 +45,7 @@ jobs:
uses: ad-m/github-push-action@master uses: ad-m/github-push-action@master
with: with:
branch: next branch: next
ssh: true github_token: ${{ steps.app-token.outputs.token }}
repository: nocobase/nocobase repository: nocobase/nocobase
tags: true tags: true
atomic: true atomic: true

View File

@ -1,4 +1,4 @@
name: Build Docker Image name: Build docker image
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@ -93,7 +93,7 @@ jobs:
file: Dockerfile file: Dockerfile
build-args: | build-args: |
VERDACCIO_URL=http://localhost:4873/ VERDACCIO_URL=http://localhost:4873/
COMMIT_HASH=${GITHUB_SHA} COMMIT_HASH=${{ github.sha }}
push: true push: true
tags: ${{ steps.set-tags.outputs.tags }} tags: ${{ steps.set-tags.outputs.tags }}

View File

@ -1,4 +1,4 @@
name: Build Pro Image name: Build pro image
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@ -12,16 +12,12 @@ on:
- 'next' - 'next'
paths: paths:
- 'packages/**' - 'packages/**'
- 'Dockerfile' - 'Dockerfile.pro'
- '.github/workflows/build-pro-image.yml' - '.github/workflows/build-pro-image.yml'
jobs: jobs:
app-token:
if: github.event.pull_request.head.repo.fork != true
uses: nocobase/nocobase/.github/workflows/get-nocobase-app-token.yml@main
secrets: inherit
build-and-push: build-and-push:
needs: app-token if: github.event.pull_request.head.repo.fork != true
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
verdaccio: verdaccio:
@ -29,14 +25,21 @@ jobs:
ports: ports:
- 4873:4873 - 4873:4873
steps: steps:
- name: Decrypt app token - name: Get pro plugins
id: app-token id: get-pro-plugins
shell: bash
run: | run: |
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }}; if [[ "${{ github.head_ref || github.ref_name }}" == "main" ]]; then
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode); echo "proRepos=$(echo '${{ vars.PRO_PLUGIN_REPOS }}')" >> $GITHUB_OUTPUT
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}"); else
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT echo "proRepos=$(echo '${{ vars.NEXT_PRO_PLUGIN_REPOS }}')" >> $GITHUB_OUTPUT
fi
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.NOCOBASE_APP_ID }}
private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
repositories: nocobase,pro-plugins,${{ join(fromJSON(steps.get-pro-plugins.outputs.proRepos), ',') }}
skip-token-revoke: true
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
@ -50,13 +53,6 @@ jobs:
path: packages/pro-plugins path: packages/pro-plugins
fetch-depth: 0 fetch-depth: 0
token: ${{ steps.app-token.outputs.token }} token: ${{ steps.app-token.outputs.token }}
- name: Clone pro repos
shell: bash
run: |
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }}
do
git clone -b main https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
done
- run: | - run: |
cd packages/pro-plugins && cd packages/pro-plugins &&
if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then
@ -68,8 +64,15 @@ jobs:
git checkout main git checkout main
fi fi
fi fi
- name: Clone pro repos
shell: bash
run: |
for repo in ${{ join(fromJSON(steps.get-pro-plugins.outputs.proRepos), ' ') }}
do
git clone -b main https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
done
- run: | - run: |
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} for repo in ${{ join(fromJSON(steps.get-pro-plugins.outputs.proRepos), ' ') }}
do do
cd ./packages/pro-plugins/@nocobase/$repo cd ./packages/pro-plugins/@nocobase/$repo
if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then
@ -86,7 +89,7 @@ jobs:
- name: rm .git - name: rm .git
run: | run: |
rm -rf packages/pro-plugins/.git rm -rf packages/pro-plugins/.git
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} for repo in ${{ join(fromJSON(steps.get-pro-plugins.outputs.proRepos), ' ') }}
do do
rm -rf packages/pro-plugins/@nocobase/$repo/.git rm -rf packages/pro-plugins/@nocobase/$repo/.git
done done
@ -137,7 +140,7 @@ jobs:
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
with: with:
context: . context: .
file: Dockerfile file: Dockerfile.pro
build-args: | build-args: |
VERDACCIO_URL=http://localhost:4873/ VERDACCIO_URL=http://localhost:4873/
COMMIT_HASH=${GITHUB_SHA} COMMIT_HASH=${GITHUB_SHA}

View File

@ -0,0 +1,82 @@
name: Write changelog and create release
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
inputs:
version:
type: choice
description: Please choose a version
options:
- beta
- alpha
default: beta
push:
tags:
- 'v*-beta'
jobs:
write-changelog-and-release:
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.NOCOBASE_APP_ID }}
private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }}
skip-token-revoke: true
- name: Get GitHub App User ID
id: get-user-id
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Checkout
uses: actions/checkout@v4
with:
repository: nocobase/nocobase
ref: main
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true
fetch-depth: 0
- name: Checkout pro-plugins
uses: actions/checkout@v4
with:
repository: nocobase/pro-plugins
path: packages/pro-plugins
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true
- name: Clone pro repos
shell: bash
run: |
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }}
do
git clone -b main https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
done
- name: Set user
run: |
git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>'
- name: Set Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run script
shell: bash
run: |
node scripts/release/changelogAndRelease.js --ver ${{ inputs.version }} --cmsURL ${{ secrets.CMS_URL }} --cmsToken ${{ secrets.CMS_TOKEN }}
env:
PRO_PLUGIN_REPOS: ${{ vars.PRO_PLUGIN_REPOS }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Commit and push
run: |
git pull origin main
git add .
git commit -m "docs: update changelogs"
git push origin main

View File

@ -1,4 +1,4 @@
name: deploy client docs name: Deploy client docs
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@ -25,30 +25,30 @@ jobs:
name: Build name: Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 18
cache: 'yarn' cache: 'yarn'
- run: yarn install - run: yarn install
- name: Build zh-CN - name: Build zh-CN
run: yarn doc build core/client --lang=zh-CN run: yarn doc build core/client --lang=zh-CN
- name: Build en-US - name: Build en-US
run: yarn doc build core/client --lang=en-US run: yarn doc build core/client --lang=en-US
- name: Set tags - name: Set tags
id: set-tags id: set-tags
run: | run: |
if [[ "${{ github.ref_name }}" == "main" ]]; then if [[ "${{ github.ref_name }}" == "main" ]]; then
echo "::set-output name=tags::${{ github.ref_name }}" echo "::set-output name=tags::${{ github.ref_name }}"
else else
echo "::set-output name=tags::pr-${{ github.event.pull_request.number }}" echo "::set-output name=tags::pr-${{ github.event.pull_request.number }}"
fi fi
- name: copy files via ssh - ${{ steps.set-tags.outputs.tags }} - name: copy files via ssh - ${{ steps.set-tags.outputs.tags }}
uses: appleboy/scp-action@v0.1.4 uses: appleboy/scp-action@v0.1.4
with: with:
host: ${{ secrets.CN_CLIENT_HOST }} host: ${{ secrets.CN_CLIENT_HOST }}
username: ${{ secrets.CN_CLIENT_USERNAME }} username: ${{ secrets.CN_CLIENT_USERNAME }}
key: ${{ secrets.CN_CLIENT_KEY }} key: ${{ secrets.CN_CLIENT_KEY }}
port: ${{ secrets.CN_CLIENT_PORT }} port: ${{ secrets.CN_CLIENT_PORT }}
source: "packages/core/client/dist/*" source: 'packages/core/client/dist/*'
target: ${{ secrets.CN_CLIENT_TARGET }}/${{ steps.set-tags.outputs.tags }} target: ${{ secrets.CN_CLIENT_TARGET }}/${{ steps.set-tags.outputs.tags }}

View File

@ -1,40 +0,0 @@
name: Get nocobase app github token
on:
workflow_call:
outputs:
token:
value: ${{ jobs.get-app-token.outputs.token }}
user-id:
value: ${{ jobs.get-app-token.outputs.user-id }}
app-slug:
value: ${{ jobs.get-app-token.outputs.app-slug }}
jobs:
get-app-token:
runs-on: ubuntu-latest
outputs:
token: ${{ steps.encrypt-token.outputs.token }}
app-slug: ${{ steps.app-token.outputs.app-slug }}
user-id: ${{ steps.get-user-id.outputs.user-id }}
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.NOCOBASE_APP_ID }}
private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }}
skip-token-revoke: true
- name: Encrypt token
id: encrypt-token
shell: bash
run: |
APP_TOKEN=${{ steps.app-token.outputs.token }};
BINARY_ENCRYPTED_SECRET=$(echo -n "$APP_TOKEN" | openssl enc -aes-256-cbc -pbkdf2 -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
ENCRYPTED_SECRET=$(echo -n "$BINARY_ENCRYPTED_SECRET" | base64 -w 0);
echo "token=$ENCRYPTED_SECRET" >> $GITHUB_OUTPUT
- name: Get GitHub App User ID
id: get-user-id
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}

View File

@ -1,4 +1,4 @@
name: manual-build-pr-docker-image name: Manual build pr docker image
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}

View File

@ -1,4 +1,4 @@
name: manual-build-pro-image name: Manual build pro image
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@ -16,12 +16,8 @@ on:
required: true required: true
jobs: jobs:
app-token:
if: github.event.pull_request.head.repo.fork != true
uses: nocobase/nocobase/.github/workflows/get-nocobase-app-token.yml@main
secrets: inherit
build-and-push: build-and-push:
needs: app-token if: github.event.pull_request.head.repo.fork != true
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
verdaccio: verdaccio:
@ -29,14 +25,21 @@ jobs:
ports: ports:
- 4873:4873 - 4873:4873
steps: steps:
- name: Decrypt app token - name: Get pro plugins
id: app-token id: get-pro-plugins
shell: bash
run: | run: |
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }}; if [[ "${{ github.head_ref || github.ref_name }}" == "main" ]]; then
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode); echo "proRepos=$(echo '${{ vars.PRO_PLUGIN_REPOS }}')" >> $GITHUB_OUTPUT
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}"); else
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT echo "proRepos=$(echo '${{ vars.NEXT_PRO_PLUGIN_REPOS }}')" >> $GITHUB_OUTPUT
fi
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.NOCOBASE_APP_ID }}
private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
repositories: nocobase,pro-plugins,${{ join(fromJSON(steps.get-pro-plugins.outputs.proRepos), ',') }}
skip-token-revoke: true
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@ -59,14 +62,14 @@ jobs:
- name: Clone pro repos - name: Clone pro repos
shell: bash shell: bash
run: | run: |
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} for repo in ${{ join(fromJSON(steps.get-pro-plugins.outputs.proRepos), ' ') }}
do do
git clone -b ${{ steps.set_pro_pr_branch.outputs.pr_branch || 'main' }} https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo git clone -b ${{ github.head_ref || github.ref_name }} https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
done done
- name: rm .git - name: rm .git
run: | run: |
rm -rf packages/pro-plugins/.git rm -rf packages/pro-plugins/.git
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} for repo in ${{ join(fromJSON(steps.get-pro-plugins.outputs.proRepos), ' ') }}
do do
rm -rf packages/pro-plugins/@nocobase/$repo/.git rm -rf packages/pro-plugins/@nocobase/$repo/.git
done done

View File

@ -0,0 +1,131 @@
name: Build pro plugin docker image
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
inputs:
pro_plugin:
description: 'Please enter a pro plugin name'
required: true
pr_number:
description: 'Please enter the pr number of pro plugin repository'
required: false
nocobase_pr_number:
description: 'Please enter the pr number of nocobase/nocobase repository'
required: false
jobs:
build-and-push:
runs-on: ubuntu-latest
services:
verdaccio:
image: verdaccio/verdaccio:latest
ports:
- 4873:4873
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.NOCOBASE_APP_ID }}
private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
repositories: nocobase,pro-plugins,plugin-${{ inputs.pro_plugin }},${{ join(fromJSON(vars.NEXT_PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }}
skip-token-revoke: true
- name: Checkout nocobase/nocobase
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref || github.ref_name }}
fetch-depth: 0
- name: Checkout nocobase/nocobase pr
if: ${{ inputs.nocobase_pr_number != '' }}
shell: bash
run: |
gh pr checkout ${{ inputs.nocobase_pr_number }}
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Checkout plugin
uses: actions/checkout@v3
with:
ref: ${{ github.head_ref || github.ref_name }}
token: ${{ steps.app-token.outputs.token }}
repository: nocobase/plugin-${{ inputs.pro_plugin }}
path: packages/pro-plugins/@nocobase/plugin-${{ inputs.pro_plugin }}
- name: Checkout pr
if: ${{ inputs.pr_number != '' }}
shell: bash
run: |
cd ./packages/pro-plugins/@nocobase/plugin-${{ inputs.pro_plugin }}
gh pr checkout ${{ inputs.pr_number }}
cd ../../../../
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
- name: rm .git
run: rm -rf packages/pro-plugins/@nocobase/plugin-${{ inputs.pro_plugin }}/.git && git config --global user.email "you@example.com" && git config --global user.name "Your Name" && git add -A && git commit -m "tmp commit"
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
driver-opts: network=host
- name: Login to Aliyun Container Registry
uses: docker/login-action@v2
with:
registry: ${{ secrets.ALI_DOCKER_REGISTRY }}
username: ${{ secrets.ALI_DOCKER_USERNAME }}
password: ${{ secrets.ALI_DOCKER_PASSWORD }}
- name: Get tag
id: get-tag
run: |
if [ "${{ inputs.pr_number }}" != "" ]; then
echo "tag=pr-${{ inputs.pr_number }}" >> "$GITHUB_OUTPUT"
else
echo "tag=${{ github.head_ref || github.ref_name }}" >> "$GITHUB_OUTPUT"
fi
- name: Set tags
id: set-tags
run: |
echo "::set-output name=tags::${{ secrets.ALI_DOCKER_REGISTRY }}/nocobase/nocobase:${{ steps.get-tag.outputs.tag }}-${{ inputs.pro_plugin }}"
- name: IMAGE_TAG
env:
IMAGE_TAG: ${{ steps.get-tag.outputs.tag }}
run: |
echo $IMAGE_TAG
- name: Set variables
run: |
target_directory="./packages/pro-plugins/@nocobase"
subdirectories=$(find "$target_directory" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | tr '\n' ' ')
trimmed_variable=$(echo "$subdirectories" | xargs)
packageNames="@nocobase/${trimmed_variable// / @nocobase/}"
pluginNames="${trimmed_variable//plugin-/}"
BEFORE_PACK_NOCOBASE="yarn add $packageNames -W"
APPEND_PRESET_LOCAL_PLUGINS="${pluginNames// /,}"
echo "var1=$BEFORE_PACK_NOCOBASE" >> $GITHUB_OUTPUT
echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT
id: vars
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
file: Dockerfile
build-args: |
VERDACCIO_URL=http://localhost:4873/
COMMIT_HASH=${GITHUB_SHA}
PLUGINS_DIRS=pro-plugins
BEFORE_PACK_NOCOBASE=${{ steps.vars.outputs.var1 }}
APPEND_PRESET_LOCAL_PLUGINS=${{ steps.vars.outputs.var2 }}
push: true
tags: ${{ steps.set-tags.outputs.tags }}
- name: Deploy NocoBase
env:
IMAGE_TAG: ${{ steps.get-tag.outputs.tag }}
run: |
echo $IMAGE_TAG
export APP_NAME=$(echo $IMAGE_TAG | cut -d ":" -f 2)-${{ inputs.pro_plugin }}
echo $APP_NAME
curl --retry 2 --location --request POST "${{secrets.NOCOBASE_DEPLOY_HOST}}$APP_NAME" \
--header 'Content-Type: application/json' \
-d "{
\"tag\": \"$APP_NAME\",
\"dialect\": \"postgres\"
}"

View File

@ -1,4 +1,4 @@
name: manual-e2e name: Manual e2e
on: on:
workflow_dispatch: workflow_dispatch:

View File

@ -1,4 +1,4 @@
name: manual-release name: Manual release
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@ -12,27 +12,28 @@ on:
type: boolean type: boolean
jobs: jobs:
app-token:
uses: nocobase/nocobase/.github/workflows/get-nocobase-app-token.yml@main
secrets: inherit
pre-merge-main-into-next: pre-merge-main-into-next:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: app-token
strategy: strategy:
matrix: matrix:
repo: repo:
- 'nocobase' - 'nocobase'
- 'pro-plugins' - 'pro-plugins'
- ${{ fromJSON(vars.PRO_PLUGIN_REPOS) }} - ${{ fromJSON(vars.PRO_PLUGIN_REPOS) }}
- ${{ fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS) }}
steps: steps:
- name: Decrypt app token - uses: actions/create-github-app-token@v1
id: app-token id: app-token
shell: bash with:
run: | app-id: ${{ vars.NOCOBASE_APP_ID }}
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }}; private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode); repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }}
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}"); skip-token-revoke: true
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT - name: Get GitHub App User ID
id: get-user-id
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@ -42,8 +43,8 @@ jobs:
token: ${{ steps.app-token.outputs.token }} token: ${{ steps.app-token.outputs.token }}
- name: main -> next (nocobase/${{ matrix.repo }}) - name: main -> next (nocobase/${{ matrix.repo }})
run: | run: |
git config --global user.name '${{ needs.app-token.outputs.app-slug }}[bot]' git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.email '${{ needs.app-token.outputs.user-id }}+${{ needs.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>' git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>'
git checkout main git checkout main
git pull origin main git pull origin main
git checkout next git checkout next
@ -51,18 +52,21 @@ jobs:
git push origin next --tags --atomic git push origin next --tags --atomic
update-version: update-version:
needs: needs:
- app-token
- pre-merge-main-into-next - pre-merge-main-into-next
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Decrypt app token - uses: actions/create-github-app-token@v1
id: app-token id: app-token
shell: bash with:
run: | app-id: ${{ vars.NOCOBASE_APP_ID }}
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }}; private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode); repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }}
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}"); skip-token-revoke: true
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT - name: Get GitHub App User ID
id: get-user-id
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@ -81,7 +85,7 @@ jobs:
- name: Clone pro repos - name: Clone pro repos
shell: bash shell: bash
run: | run: |
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} ${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ' ') }}
do do
git clone -b main https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo git clone -b main https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
done done
@ -96,23 +100,24 @@ jobs:
run: | run: |
cd ./packages/pro-plugins cd ./packages/pro-plugins
git checkout main git checkout main
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} ${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ' ') }}
do do
echo "@nocobase/$repo" >> .git/info/exclude echo "@nocobase/$repo" >> .git/info/exclude
done done
echo "$(<.git/info/exclude )" echo "$(<.git/info/exclude )"
cd ./../.. cd ./../..
git checkout main git checkout main
git config --global user.name '${{ needs.app-token.outputs.app-slug }}[bot]' git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.email '${{ needs.app-token.outputs.user-id }}+${{ needs.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>' git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>'
echo "packages/pro-plugins" >> .git/info/exclude echo "packages/pro-plugins" >> .git/info/exclude
bash release.sh $IS_FEAT bash release.sh $IS_FEAT
env: env:
IS_FEAT: ${{ inputs.is_feat && '--is-feat' || '' }} IS_FEAT: ${{ inputs.is_feat && '--is-feat' || '' }}
PRO_PLUGIN_REPOS: ${{ vars.PRO_PLUGIN_REPOS }} PRO_PLUGIN_REPOS: ${{ vars.PRO_PLUGIN_REPOS }}
CUSTOM_PRO_PLUGIN_REPOS: ${{ vars.CUSTOM_PRO_PLUGIN_REPOS }}
- name: Push and merge into next - name: Push and merge into next
run: | run: |
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} ${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ' ') }}
do do
cd ./packages/pro-plugins/@nocobase/$repo cd ./packages/pro-plugins/@nocobase/$repo
git push origin main --atomic --tags git push origin main --atomic --tags

View File

@ -1,4 +1,4 @@
name: NocoBase Backend Test name: NocoBase backend test
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}

View File

@ -1,4 +1,4 @@
name: NocoBase FrontEnd Test name: NocoBase frontEnd test
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@ -33,7 +33,7 @@ jobs:
frontend-test: frontend-test:
strategy: strategy:
matrix: matrix:
node_version: [ '18' ] node_version: ['18']
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: node:${{ matrix.node_version }} container: node:${{ matrix.node_version }}
steps: steps:

View File

@ -1,4 +1,4 @@
name: Release Next name: Release next
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@ -8,26 +8,27 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
app-token:
uses: nocobase/nocobase/.github/workflows/get-nocobase-app-token.yml@main
secrets: inherit
publish-npm: publish-npm:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: node:18 container: node:18
needs: app-token
steps: steps:
- name: Decrypt app token - uses: actions/create-github-app-token@v1
id: app-token id: app-token
shell: bash with:
run: | app-id: ${{ vars.NOCOBASE_APP_ID }}
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }}; private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode); repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.NEXT_PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }}
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}"); skip-token-revoke: true
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT - name: Get GitHub App User ID
id: get-user-id
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: next ref: next
fetch-depth: 0
- name: Send curl request and parse response - name: Send curl request and parse response
env: env:
PKG_USERNAME: ${{ secrets.PKG_USERNAME }} PKG_USERNAME: ${{ secrets.PKG_USERNAME }}
@ -76,8 +77,8 @@ jobs:
- name: publish npmjs.org - name: publish npmjs.org
continue-on-error: true continue-on-error: true
run: | run: |
git config --global user.email "test@mail.com" git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.name "test" git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>'
git config --global --add safe.directory /__w/nocobase/nocobase git config --global --add safe.directory /__w/nocobase/nocobase
npm config set access public npm config set access public
npm config set registry https://registry.npmjs.org/ npm config set registry https://registry.npmjs.org/
@ -99,11 +100,12 @@ jobs:
repository: nocobase/pro-plugins repository: nocobase/pro-plugins
path: packages/pro-plugins path: packages/pro-plugins
ref: next ref: next
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }} token: ${{ steps.app-token.outputs.token }}
- name: Clone pro repos - name: Clone pro repos
shell: bash shell: bash
run: | run: |
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} for repo in ${{ join(fromJSON(vars.NEXT_PRO_PLUGIN_REPOS), ' ') }} ${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ' ') }}
do do
git clone -b next https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo git clone -b next https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
done done
@ -124,3 +126,29 @@ jobs:
bash generate-npmignore.sh ignore-src bash generate-npmignore.sh ignore-src
npm config set //pkg-src.nocobase.com/:_authToken=${{ env.PKG_SRC_NOCOBASE_TOKEN }} npm config set //pkg-src.nocobase.com/:_authToken=${{ env.PKG_SRC_NOCOBASE_TOKEN }}
yarn release:force --no-verify-access --no-git-reset --registry https://pkg-src.nocobase.com --dist-tag=next yarn release:force --no-verify-access --no-git-reset --registry https://pkg-src.nocobase.com --dist-tag=next
- name: Tag
run: |
git reset --hard HEAD~
git tag v${{ env.NEWVERSION }}
git push origin v${{ env.NEWVERSION }}
cd ./packages/pro-plugins
git reset --hard
git tag v${{ env.NEWVERSION }}
git push origin v${{ env.NEWVERSION }}
cd ../../
for repo in ${{ join(fromJSON(vars.NEXT_PRO_PLUGIN_REPOS), ' ') }} ${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ' ') }}
do
cd ./packages/pro-plugins/@nocobase/$repo
git reset --hard
git tag v${{ env.NEWVERSION }}
git push origin v${{ env.NEWVERSION }}
cd ../../../../
done
- name: Run release script
shell: bash
run: |
git fetch
node scripts/release/changelogAndRelease.js --ver alpha --cmsURL ${{ secrets.CMS_URL }} --cmsToken ${{ secrets.CMS_TOKEN }}
env:
PRO_PLUGIN_REPOS: ${{ vars.NEXT_PRO_PLUGIN_REPOS }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}

View File

@ -10,22 +10,17 @@ on:
- 'v*' - 'v*'
jobs: jobs:
app-token:
uses: nocobase/nocobase/.github/workflows/get-nocobase-app-token.yml@main
secrets: inherit
publish-npm: publish-npm:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: node:18 container: node:18
needs: app-token
steps: steps:
- name: Decrypt app token - uses: actions/create-github-app-token@v1
id: app-token id: app-token
shell: bash with:
run: | app-id: ${{ vars.NOCOBASE_APP_ID }}
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }}; private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode); repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }}
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}"); skip-token-revoke: true
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Send curl request and parse response - name: Send curl request and parse response
@ -76,7 +71,7 @@ jobs:
- name: Clone pro repos - name: Clone pro repos
shell: bash shell: bash
run: | run: |
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} ${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ' ') }}
do do
git clone -b main https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo git clone -b main https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
done done

1
.gitignore vendored
View File

@ -32,6 +32,7 @@ storage/plugins
storage/tar storage/tar
storage/tmp storage/tmp
storage/app.watch.ts storage/app.watch.ts
storage/.upgrading
storage/logs-e2e storage/logs-e2e
storage/uploads-e2e storage/uploads-e2e
storage/.pm2-* storage/.pm2-*

View File

@ -5,7 +5,402 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). ## [v1.3.32-beta](https://github.com/nocobase/nocobase/compare/v1.3.31-beta...v1.3.32-beta) - 2024-10-13
### 🐛 Bug Fixes
- **[client]** required relational field still triggers validation error after selecting a value with a variable in data scope ([#5399](https://github.com/nocobase/nocobase/pull/5399)) by @katherinehhh
## [v1.3.31-beta](https://github.com/nocobase/nocobase/compare/v1.3.30-beta...v1.3.31-beta) - 2024-10-11
### 🐛 Bug Fixes
- **[client]** Fix the issue where using the chinaRegion field in the filter form fails to correctly filter out values ([#5390](https://github.com/nocobase/nocobase/pull/5390)) by @zhangzhonghe
- **[Action: Import records]** fix import error with wps file ([#5397](https://github.com/nocobase/nocobase/pull/5397)) by @chareice
## [v1.3.30-beta](https://github.com/nocobase/nocobase/compare/v1.3.29-beta...v1.3.30-beta) - 2024-10-11
### 🐛 Bug Fixes
- **[client]**
- Fix the rendering error that occurs when displaying file collection relationship fields on mobile devices ([#5387](https://github.com/nocobase/nocobase/pull/5387)) by @zhangzhonghe
- Fix Create Block menu not loading more data collections ([#5388](https://github.com/nocobase/nocobase/pull/5388)) by @zhangzhonghe
- **[Workflow: Custom action event]**
- Fix custom workflow event did not redirect after successful submission by @katherinehhh
- Fix custom workflow event did not redirect after successful submission by @katherinehhh
## [v1.3.29-beta](https://github.com/nocobase/nocobase/compare/v1.3.28-beta...v1.3.29-beta) - 2024-10-10
### 🚀 Improvements
- **[client]** Date variables are also not prohibited in create form ([#5376](https://github.com/nocobase/nocobase/pull/5376)) by @zhangzhonghe
### 🐛 Bug Fixes
- **[Workflow: SQL node]** fix error when no result when calling stored procedure in SQL instruction ([#5385](https://github.com/nocobase/nocobase/pull/5385)) by @mytharcher
- **[Workflow]** fix date field based schedule trigger caused app crash, and also support other data source ([#5364](https://github.com/nocobase/nocobase/pull/5364)) by @mytharcher
## [v1.3.28-beta](https://github.com/nocobase/nocobase/compare/v1.3.27-beta...v1.3.28-beta) - 2024-10-09
### 🚀 Improvements
- **[client]** Save cdn links as local resources to prevent requesting external resources when deploying on the intranet ([#5375](https://github.com/nocobase/nocobase/pull/5375)) by @zhangzhonghe
### 🐛 Bug Fixes
- **[client]**
- Fix the issue where popups opened in the "Users & Permissions" configuration page are obscured by other popups ([#5373](https://github.com/nocobase/nocobase/pull/5373)) by @zhangzhonghe
- Fix the problem that after deleting a tab in a subpage, it does not take effect after opening it again ([#5362](https://github.com/nocobase/nocobase/pull/5362)) by @zhangzhonghe
- Fix the issue where inherited collection association fields cannot properly use variables ([#5346](https://github.com/nocobase/nocobase/pull/5346)) by @zhangzhonghe
- Fix the issue of current and association collection fields affecting each other in configuration ([#5343](https://github.com/nocobase/nocobase/pull/5343)) by @katherinehhh
- **[Action: Import records]** fixed issue with incorrect results for importing large dates ([#5356](https://github.com/nocobase/nocobase/pull/5356)) by @chareice
- **[Workflow]** fix switching component of association field in assigned fields caused page crash in create/update node ([#5366](https://github.com/nocobase/nocobase/pull/5366)) by @mytharcher
- **[Block: Gantt]** Fix the issue where opening a popup in the Gantt block and then closing it causes the subpage to also close ([#5370](https://github.com/nocobase/nocobase/pull/5370)) by @zhangzhonghe
## [v1.3.27-beta](https://github.com/nocobase/nocobase/compare/v1.3.26-beta...v1.3.27-beta) - 2024-09-30
### 🐛 Bug Fixes
- **[client]** Fix variable "Table selected records" ([#5337](https://github.com/nocobase/nocobase/pull/5337)) by @zhangzhonghe
- **[Workflow: Custom action event]** fix custom action event not triggers in association block by @mytharcher
## [v1.3.26-beta](https://github.com/nocobase/nocobase/compare/v1.3.25-beta...v1.3.26-beta) - 2024-09-29
### 🚀 Improvements
- **[client]** Hide scrollbars on mobile ([#5339](https://github.com/nocobase/nocobase/pull/5339)) by @zhangzhonghe
### 🐛 Bug Fixes
- **[client]**
- Fix the issue of not being able to open sub-pages in embedded pages ([#5335](https://github.com/nocobase/nocobase/pull/5335)) by @zhangzhonghe
- Fix the issue of pop-up windows being obscured ([#5338](https://github.com/nocobase/nocobase/pull/5338)) by @zhangzhonghe
- Fix the issue of abnormal style when creating blocks with data templates in mobile subpages ([#5340](https://github.com/nocobase/nocobase/pull/5340)) by @zhangzhonghe
- Fix the issue of not refreshing the page block data when closing a subpage via the page menu ([#5331](https://github.com/nocobase/nocobase/pull/5331)) by @zhangzhonghe
- **[Action: Export records]** fix export format for decimal type fields ([#5316](https://github.com/nocobase/nocobase/pull/5316)) by @chareice
- **[Block: Kanban]** Fix the issue that the popup window could not be opened after clicking on the Kanban card in the embedded page ([#5326](https://github.com/nocobase/nocobase/pull/5326)) by @zhangzhonghe
## [v1.3.25-beta](https://github.com/nocobase/nocobase/compare/v1.3.24-beta...v1.3.25-beta) - 2024-09-25
### 🚀 Improvements
- **[client]** update and improve Japanese translations in ja_JP files ([#5292](https://github.com/nocobase/nocobase/pull/5292)) by @Albert-mah
- **[Workflow]** add error handling for unregistered node type ([#5319](https://github.com/nocobase/nocobase/pull/5319)) by @mytharcher
### 🐛 Bug Fixes
- **[client]** Fix for not displaying full fields in variables ([#5310](https://github.com/nocobase/nocobase/pull/5310)) by @zhangzhonghe
- **[Workflow]** fix non-existed field in collection trigger causes error ([#5318](https://github.com/nocobase/nocobase/pull/5318)) by @mytharcher
- **[Action: Export records]** Fix fields from assicated tables are not processed by the field interface ([#5296](https://github.com/nocobase/nocobase/pull/5296)) by @gchust
## [v1.3.24-beta](https://github.com/nocobase/nocobase/compare/v1.3.23-beta...v1.3.24-beta) - 2024-09-23
### 🐛 Bug Fixes
- **[client]**
- markdown report error with using `#each` in handlebars ([#5305](https://github.com/nocobase/nocobase/pull/5305)) by @katherinehhh
- Fix issue where the collection from external data source does not support sorting on table columns ([#5293](https://github.com/nocobase/nocobase/pull/5293)) by @katherinehhh
- **[Data visualization]** Fix style issues of chart blocks when using dark mode themes ([#5302](https://github.com/nocobase/nocobase/pull/5302)) by @2013xile
## [v1.3.23-beta](https://github.com/nocobase/nocobase/compare/v1.3.22-beta...v1.3.23-beta) - 2024-09-19
### 🚀 Improvements
- **[Users]** Optimize performance for rendering the user management table ([#5276](https://github.com/nocobase/nocobase/pull/5276)) by @2013xile
- **[Departments]** Optimize performance for rendering the user table in department management by @2013xile
### 🐛 Bug Fixes
- **[client]**
- Fix incorrect `rowKey` of the `General action permissions table` in Users & Permissions page ([#5287](https://github.com/nocobase/nocobase/pull/5287)) by @gchust
- Fix the issue where setting a date variable for the date field in the filter form results in incorrect filter results ([#5257](https://github.com/nocobase/nocobase/pull/5257)) by @zhangzhonghe
- column width issue with scroll.y when table has no data ([#5256](https://github.com/nocobase/nocobase/pull/5256)) by @katherinehhh
- Fix the problem of blank rows at the beginning of a table block ([#5284](https://github.com/nocobase/nocobase/pull/5284)) by @zhangzhonghe
- **[create-nocobase-app]** Fix the issue where the popup for configuring Sequence rules lacked a submit button when adding a new Sequence field ([#5281](https://github.com/nocobase/nocobase/pull/5281)) by @zhangzhonghe
- **[database]** import with checkbox field ([#4992](https://github.com/nocobase/nocobase/pull/4992)) by @chareice
- **[evaluators]** Fix error caused by `Matrix` type from mathjs ([#5270](https://github.com/nocobase/nocobase/pull/5270)) by @mytharcher
- **[Calendar]** Cannot select the option to delete the schedule popup ([#5274](https://github.com/nocobase/nocobase/pull/5274)) by @katherinehhh
- **[Action: Export records]** Fix missing request context when generating data sheet in export action ([#5286](https://github.com/nocobase/nocobase/pull/5286)) by @gchust
## [v1.3.22-beta](https://github.com/nocobase/nocobase/compare/v1.3.21-beta...v1.3.22-beta) - 2024-09-12
### 🎉 New Features
- **[Action: Custom request]** Support for API token variables in the "Custom Request Button" configuration ([#5263](https://github.com/nocobase/nocobase/pull/5263)) by @zhangzhonghe
Reference: [Custom request](https://docs.nocobase.com/handbook/action-custom-request)
### 🚀 Improvements
- **[Collection field: Markdown(Vditor)]** Support Vidtor when selecting fields in the UI for external data sources ([#5246](https://github.com/nocobase/nocobase/pull/5246)) by @katherinehhh
### 🐛 Bug Fixes
- **[Calendar]** issue where the calendar block cannot display correctly when the end date crosses months ([#5239](https://github.com/nocobase/nocobase/pull/5239)) by @katherinehhh
## [v1.3.21-beta](https://github.com/nocobase/nocobase/compare/v1.3.20-beta...v1.3.21-beta) - 2024-09-10
### 🐛 Bug Fixes
- **[client]** Fix error when using linkage rules (NocoBase installed via create-nocobase-app) ([#5249](https://github.com/nocobase/nocobase/pull/5249)) by @zhangzhonghe
## [v1.3.20-beta](https://github.com/nocobase/nocobase/compare/v1.3.19-beta...v1.3.20-beta) - 2024-09-10
### 🚀 Improvements
- **[client]** Support for displaying deeper level association fields in data blocks ([#5243](https://github.com/nocobase/nocobase/pull/5243)) by @zhangzhonghe
### 🐛 Bug Fixes
- **[client]**
- Menu modifications do not take effect in real-time ([#5207](https://github.com/nocobase/nocobase/pull/5207)) by @katherinehhh
- Support association field preloading in Handlebars templates ([#5236](https://github.com/nocobase/nocobase/pull/5236)) by @katherinehhh
- **[Data visualization]** Fix incorrect data source context when multiple data sources exist ([#5237](https://github.com/nocobase/nocobase/pull/5237)) by @2013xile
## [v1.3.19-beta](https://github.com/nocobase/nocobase/compare/v1.3.18-beta...v1.3.19-beta) - 2024-09-08
### 🐛 Bug Fixes
- **[client]** Fix URL anomalies caused by using popups together with Link buttons ([#5219](https://github.com/nocobase/nocobase/pull/5219)) by @zhangzhonghe
## [v1.3.18-beta](https://github.com/nocobase/nocobase/compare/v1.3.17-beta...v1.3.18-beta) - 2024-09-08
### 🐛 Bug Fixes
- **[Collection field: Many to many (array)]** Fix the error when deleting a collection contains m2m array fields ([#5231](https://github.com/nocobase/nocobase/pull/5231)) by @2013xile
## [v1.3.17-beta](https://github.com/nocobase/nocobase/compare/v1.3.16-beta...v1.3.17-beta) - 2024-09-07
### 🎉 New Features
- **[client]** Supports configuration of linkage rules in sub-forms and sub-forms. ([#5159](https://github.com/nocobase/nocobase/pull/5159)) by @zhangzhonghe
### 🚀 Improvements
- **[client]**
- default time for display is 00:00:00 ([#5226](https://github.com/nocobase/nocobase/pull/5226)) by @chenos
- plugins can also be enabled when the plugin dependency version is inconsistent ([#5225](https://github.com/nocobase/nocobase/pull/5225)) by @chenos
- **[server]** provide more user-friendly application-level error messages ([#5220](https://github.com/nocobase/nocobase/pull/5220)) by @chenos
### 🐛 Bug Fixes
- **[client]** Fix the "Maximum call stack size exceeded" error that occurs in the details block ([#5228](https://github.com/nocobase/nocobase/pull/5228)) by @zhangzhonghe
- **[Collection field: Many to many (array)]** Fix the error where setting a field of `uid` type as target key for a many to many (array) field ([#5229](https://github.com/nocobase/nocobase/pull/5229)) by @2013xile
- **[UI schema storage]** Fix the issue that member roles clicking the button reported no permission ([#5206](https://github.com/nocobase/nocobase/pull/5206)) by @zhangzhonghe
- **[Workflow]** Fix trigger type column showing wrong text after new workflow created ([#5222](https://github.com/nocobase/nocobase/pull/5222)) by @mytharcher
- **[Users]** Remove phone format validation when editing user phones in user management ([#5221](https://github.com/nocobase/nocobase/pull/5221)) by @2013xile
## [v1.3.16-beta](https://github.com/nocobase/nocobase/compare/v1.3.15-beta...v1.3.16-beta) - 2024-09-06
### 🚀 Improvements
- **[client]**
- Placeholder added when the user has UI configuration permissions but no view permissions for the collection ([#5208](https://github.com/nocobase/nocobase/pull/5208)) by @katherinehhh
- Display system title when logo is missing. ([#5175](https://github.com/nocobase/nocobase/pull/5175)) by @maoyutofu
- **[Authentication]** support line break in system title ([#5211](https://github.com/nocobase/nocobase/pull/5211)) by @chenos
- **[Workflow: SQL node]** Change result data structure of SQL node to only contains data. ([#5189](https://github.com/nocobase/nocobase/pull/5189)) by @mytharcher
Reference: [SQL Operation](https://docs.nocobase.com/handbook/workflow/nodes/sql)
- **[Access control]** Make the `Permissions` Tab pannel of the `Users & Permissions` configuration page expandable. ([#5216](https://github.com/nocobase/nocobase/pull/5216)) by @zhangzhonghe
Reference: [Development Guide](https://docs.nocobase.com/handbook/acl#development-guide)
- **[Action: Batch edit]** batch updated and batch edit, change 'All' to 'Entire collection" ([#5200](https://github.com/nocobase/nocobase/pull/5200)) by @katherinehhh
### 🐛 Bug Fixes
- **[client]**
- component display error when switching assignment types in linkage rules ([#5180](https://github.com/nocobase/nocobase/pull/5180)) by @katherinehhh
- Fix an issue where using variables in data scope reported an error. ([#5195](https://github.com/nocobase/nocobase/pull/5195)) by @zhangzhonghe
- issue with custom request refreshDataBlockRequest ([#5188](https://github.com/nocobase/nocobase/pull/5188)) by @katherinehhh
- **[Data visualization]** Fixed the issue of getting wrong value when aggregating select fields ([#5214](https://github.com/nocobase/nocobase/pull/5214)) by @2013xile
- **[Data source manager]** Fixed incorrect `rowKey` of the datasource table in `Users & Permissions` page ([#5215](https://github.com/nocobase/nocobase/pull/5215)) by @gchust
- **[Workflow: HTTP request node]** Fix error when using non-string variable in request parameters. ([#5204](https://github.com/nocobase/nocobase/pull/5204)) by @mytharcher
- **[Collection field: Formula]** fix formula field serve test ([#5197](https://github.com/nocobase/nocobase/pull/5197)) by @katherinehhh
- **[App backup & restore (deprecated)]** fix test case errors ([#5201](https://github.com/nocobase/nocobase/pull/5201)) by @chenos
- **[Data source: REST API]**
- collection name should be disabled in rest-api collection by @katherinehhh
- Rest api locale improve by @katherinehhh
## [v1.3.15-beta](https://github.com/nocobase/nocobase/compare/v1.3.14-beta...v1.3.15-beta) - 2024-09-04
### 🐛 Bug Fixes
- **[Workflow]** Fix missed fields in workflow variables. ([#5187](https://github.com/nocobase/nocobase/pull/5187)) by @mytharcher
- **[Collection field: Markdown(Vditor)]** issue with markdown(Vditor) ([#5176](https://github.com/nocobase/nocobase/pull/5176)) by @katherinehhh
## [v1.3.14-beta](https://github.com/nocobase/nocobase/compare/v1.3.13-beta...v1.3.14-beta) - 2024-09-03
### 🎉 New Features
- **[client]** Add support for many-to-many association fields. ([#5178](https://github.com/nocobase/nocobase/pull/5178)) by @zhangzhonghe
### 🚀 Improvements
- **[Action: Custom request]** remove linkageRule for custom request in create form ([#5179](https://github.com/nocobase/nocobase/pull/5179)) by @katherinehhh
### 🐛 Bug Fixes
- **[Collection field: Formula]** formula field adaptation time field ([#5168](https://github.com/nocobase/nocobase/pull/5168)) by @katherinehhh
## [v1.3.13-beta](https://github.com/nocobase/nocobase/compare/v1.3.12-beta...v1.3.13-beta) - 2024-09-03
### 🐛 Bug Fixes
- **[Action: Export records]** Fixed incorrect export of relational data ([#5170](https://github.com/nocobase/nocobase/pull/5170)) by @chareice
## [v1.3.12-beta](https://github.com/nocobase/nocobase/compare/v1.3.11-beta...v1.3.12-beta) - 2024-09-01
### Merged
- fix(mobile): fix permission [`#5163`](https://github.com/nocobase/nocobase/pull/5163)
### Commits
- chore(versions): 😊 publish v1.3.12-beta [`774c296`](https://github.com/nocobase/nocobase/commit/774c2961d47aa17d1a9da7a595bb070f34aee11b)
- chore: update changelog [`7f9a116`](https://github.com/nocobase/nocobase/commit/7f9a11698f3126257529ce4a91670239900f2ec3)
- chore: update e2e test [`49db3e4`](https://github.com/nocobase/nocobase/commit/49db3e490821cd59aaba2f58ed2bb78051a86ad9)
## [v1.3.11-beta](https://github.com/nocobase/nocobase/compare/v1.3.10-beta...v1.3.11-beta) - 2024-08-31
### Commits
- chore(versions): 😊 publish v1.3.11-beta [`517e199`](https://github.com/nocobase/nocobase/commit/517e199ed7a8e7dc81a06c50389ef41b6891b133)
- chore: update changelog [`373f517`](https://github.com/nocobase/nocobase/commit/373f51773b772886cc8db3cb50184562113c62eb)
## [v1.3.10-beta](https://github.com/nocobase/nocobase/compare/v1.3.9-beta...v1.3.10-beta) - 2024-08-31
### Merged
- fix: issue with association select data scope linkage in sub-table [`#5160`](https://github.com/nocobase/nocobase/pull/5160)
- fix: issue in data selector other block should display Markdown, not 'Add Text' [`#5161`](https://github.com/nocobase/nocobase/pull/5161)
- fix(data-vi): issue of parsing variables in filter block [`#5157`](https://github.com/nocobase/nocobase/pull/5157)
- fix(data-vi): transform the values of decimal fields from type string to number [`#5155`](https://github.com/nocobase/nocobase/pull/5155)
### Commits
- chore(versions): 😊 publish v1.3.10-beta [`5afac9c`](https://github.com/nocobase/nocobase/commit/5afac9cf82c78db4a7ee8ddb01a60597939ac82d)
- chore: update changelog [`6fceac1`](https://github.com/nocobase/nocobase/commit/6fceac15827a10b6fba65e98314c37f3f9e697ba)
- chore: update comment [`6e45780`](https://github.com/nocobase/nocobase/commit/6e4578056556c1c60ac721ff990a81ed37339074)
## [v1.3.9-beta](https://github.com/nocobase/nocobase/compare/v1.3.8-beta...v1.3.9-beta) - 2024-08-29
### Merged
- fix(mobile): should not force redirect to mobile page [`#5152`](https://github.com/nocobase/nocobase/pull/5152)
- chore: support year data type in mysql [`#5123`](https://github.com/nocobase/nocobase/pull/5123)
### Commits
- chore(versions): 😊 publish v1.3.9-beta [`bf5011f`](https://github.com/nocobase/nocobase/commit/bf5011f75a7a9b26db7fef7aa4be28d7e4e077b4)
- chore: update changelog [`b2fc646`](https://github.com/nocobase/nocobase/commit/b2fc646e5aa64d2ade03ce6fca78753cfddc26ec)
## [v1.3.8-beta](https://github.com/nocobase/nocobase/compare/v1.3.7-beta...v1.3.8-beta) - 2024-08-29
### Commits
- chore(versions): 😊 publish v1.3.8-beta [`39d021a`](https://github.com/nocobase/nocobase/commit/39d021a9aa29bef9cf15d4af546060fc4b1dbd10)
- chore: update changelog [`9f66c14`](https://github.com/nocobase/nocobase/commit/9f66c14968639d90b399d087eefac7a0c4ea4383)
## [v1.3.7-beta](https://github.com/nocobase/nocobase/compare/v1.3.6-beta...v1.3.7-beta) - 2024-08-29
### Merged
- fix: add text support handlebars [`#5150`](https://github.com/nocobase/nocobase/pull/5150)
### Commits
- chore(versions): 😊 publish v1.3.7-beta [`f429d13`](https://github.com/nocobase/nocobase/commit/f429d1326433e7f290e552ca91548d21b5af92e4)
- chore: update changelog [`b41e477`](https://github.com/nocobase/nocobase/commit/b41e47757ec0d1f7b0af917e25ff5b4a436042aa)
## [v1.3.6-beta](https://github.com/nocobase/nocobase/compare/v1.3.5-beta...v1.3.6-beta) - 2024-08-29
### Merged
- fix: association select data scope linkage should support edit form [`#5149`](https://github.com/nocobase/nocobase/pull/5149)
### Commits
- chore(versions): 😊 publish v1.3.6-beta [`39c7ce4`](https://github.com/nocobase/nocobase/commit/39c7ce4741801819b98970b95c1663915a8c3bff)
- chore: update changelog [`cfbc2a6`](https://github.com/nocobase/nocobase/commit/cfbc2a6c15a5dfb8c0684051df1cf01499ff30ac)
## [v1.3.5-beta](https://github.com/nocobase/nocobase/compare/v1.3.4-beta...v1.3.5-beta) - 2024-08-28
### Merged
- fix: association select data scope linkage should be supported in sub-form [`#5146`](https://github.com/nocobase/nocobase/pull/5146)
- fix(mobile): resovle redirect issue [`#5145`](https://github.com/nocobase/nocobase/pull/5145)
- feat(plugin-workflow): allow to delete execution in list [`#5135`](https://github.com/nocobase/nocobase/pull/5135)
- fix(defaultValue): ignores variable values that do not match the current field [`#5122`](https://github.com/nocobase/nocobase/pull/5122)
- chore(deps-dev): bump eslint-plugin-jest-dom from 5.1.0 to 5.4.0 [`#5138`](https://github.com/nocobase/nocobase/pull/5138)
- chore(deps): bump @ant-design/pro-layout from 7.17.16 to 7.19.12 [`#5137`](https://github.com/nocobase/nocobase/pull/5137)
- fix(template): fix error on form block submission [`#5133`](https://github.com/nocobase/nocobase/pull/5133)
- feat: add support for opening via URL [`#5098`](https://github.com/nocobase/nocobase/pull/5098)
- fix(release): decrypt token error occasionally [`#5143`](https://github.com/nocobase/nocobase/pull/5143)
### Commits
- chore(versions): 😊 publish v1.3.5-beta [`35e8f89`](https://github.com/nocobase/nocobase/commit/35e8f89c75800a612db27485c96196555f922273)
- Revert "chore(deps): bump @ant-design/pro-layout from 7.17.16 to 7.19.12 (#5137)" [`3f461ad`](https://github.com/nocobase/nocobase/commit/3f461ad8f079b4c2cf5975c1e26271f55021e08a)
- fix(release): pro image ci [`e45d450`](https://github.com/nocobase/nocobase/commit/e45d45015792138e7378741bdaf488de714b365d)
## [v1.3.4-beta](https://github.com/nocobase/nocobase/compare/v1.3.3-beta...v1.3.4-beta) - 2024-08-27
### Merged
- refactor: set remainsTheSame as the default value for field editing in bulk editing action [`#5124`](https://github.com/nocobase/nocobase/pull/5124)
### Commits
- chore(versions): 😊 publish v1.3.4-beta [`a011748`](https://github.com/nocobase/nocobase/commit/a0117480e037e48a23f59921110003047a1a174b)
- chore: update changelog [`3403e8d`](https://github.com/nocobase/nocobase/commit/3403e8d76684950d6962a6110a4440eb95856a35)
## [v1.3.3-beta](https://github.com/nocobase/nocobase/compare/v1.3.2-beta...v1.3.3-beta) - 2024-08-27 ## [v1.3.3-beta](https://github.com/nocobase/nocobase/compare/v1.3.2-beta...v1.3.3-beta) - 2024-08-27

298
CHANGELOG.zh-CN.md Normal file
View File

@ -0,0 +1,298 @@
# 更新日志
本项目的所有重要更改都将记录在此文件中。
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
## [v1.3.32-beta](https://github.com/nocobase/nocobase/compare/v1.3.31-beta...v1.3.32-beta) - 2024-10-13
### 🐛 修复
- **[client]** 关系字段设置必填,数据范围中设置变量后,选中值却报字段必填不通过 ([#5399](https://github.com/nocobase/nocobase/pull/5399)) by @katherinehhh
## [v1.3.31-beta](https://github.com/nocobase/nocobase/compare/v1.3.30-beta...v1.3.31-beta) - 2024-10-11
### 🐛 修复
- **[client]** 修复在筛选表单中使用行政区划字段无法正确筛选出值的问题 ([#5390](https://github.com/nocobase/nocobase/pull/5390)) by @zhangzhonghe
- **[操作:导入记录]** 修复导入 wps 文件报错的问题 ([#5397](https://github.com/nocobase/nocobase/pull/5397)) by @chareice
## [v1.3.30-beta](https://github.com/nocobase/nocobase/compare/v1.3.29-beta...v1.3.30-beta) - 2024-10-11
### 🐛 修复
- **[client]**
- 修复在移动端中,显示文件表关系字段时报渲染错误的问题 ([#5387](https://github.com/nocobase/nocobase/pull/5387)) by @zhangzhonghe
- 修复创建区块菜单无法加载更多数据表的问题 ([#5388](https://github.com/nocobase/nocobase/pull/5388)) by @zhangzhonghe
- **[工作流:自定义操作事件]**
- 修复 自定义工作流事件提交成功后跳转不生效 by @katherinehhh
- 自定义工作流事件提交成功后跳转不生效 by @katherinehhh
## [v1.3.29-beta](https://github.com/nocobase/nocobase/compare/v1.3.28-beta...v1.3.29-beta) - 2024-10-10
### 🚀 优化
- **[client]** 创建表单中也不禁用日期变量 ([#5376](https://github.com/nocobase/nocobase/pull/5376)) by @zhangzhonghe
### 🐛 修复
- **[工作流SQL 节点]** 修复在 SQL 节点中调用存储过程没有返回结果时导致错误的问题 ([#5385](https://github.com/nocobase/nocobase/pull/5385)) by @mytharcher
- **[工作流]** 修复基于时间字段的定时任务导致报错的问题,并支持其他数据库数据源 ([#5364](https://github.com/nocobase/nocobase/pull/5364)) by @mytharcher
## [v1.3.28-beta](https://github.com/nocobase/nocobase/compare/v1.3.27-beta...v1.3.28-beta) - 2024-10-09
### 🚀 优化
- **[client]** 将 cdn 链接保存为本地资源,以防止在内网部署时请求外部资源 ([#5375](https://github.com/nocobase/nocobase/pull/5375)) by @zhangzhonghe
### 🐛 修复
- **[client]**
- 修复在“用户和权限”配置页打开的弹窗被其它弹窗遮挡的问题 ([#5373](https://github.com/nocobase/nocobase/pull/5373)) by @zhangzhonghe
- 修复在子页面中删除 tab 页后,再次打开后未生效的问题 ([#5362](https://github.com/nocobase/nocobase/pull/5362)) by @zhangzhonghe
- 修复继承表关系字段无法正常使用变量的问题 ([#5346](https://github.com/nocobase/nocobase/pull/5346)) by @zhangzhonghe
- 修复字段配置中当前数据表字段与关系表字段互相影响缺陷 ([#5343](https://github.com/nocobase/nocobase/pull/5343)) by @katherinehhh
- **[操作:导入记录]** 修复导入大日期结果不正确的问题 ([#5356](https://github.com/nocobase/nocobase/pull/5356)) by @chareice
- **[工作流]** 修复新增、更新节点中配置关系字段赋值时切换组件导致的页面崩溃 ([#5366](https://github.com/nocobase/nocobase/pull/5366)) by @mytharcher
- **[区块:甘特图]** 修复在甘特图中打开弹窗,然后再关闭,导致子页面也被关闭的问题 ([#5370](https://github.com/nocobase/nocobase/pull/5370)) by @zhangzhonghe
## [v1.3.27-beta](https://github.com/nocobase/nocobase/compare/v1.3.26-beta...v1.3.27-beta) - 2024-09-30
### 🐛 修复
- **[client]** 修复变量“表格中选中的记录” ([#5337](https://github.com/nocobase/nocobase/pull/5337)) by @zhangzhonghe
- **[工作流:自定义操作事件]** 修复自定义操作事件在关系区块中不触发的问题 by @mytharcher
## [v1.3.26-beta](https://github.com/nocobase/nocobase/compare/v1.3.25-beta...v1.3.26-beta) - 2024-09-29
### 🚀 优化
- **[client]** 隐藏移动端的滚动条 ([#5339](https://github.com/nocobase/nocobase/pull/5339)) by @zhangzhonghe
### 🐛 修复
- **[client]**
- 修复在嵌入页面中无法打开子页面的问题 ([#5335](https://github.com/nocobase/nocobase/pull/5335)) by @zhangzhonghe
- 修复弹窗被遮挡的问题 ([#5338](https://github.com/nocobase/nocobase/pull/5338)) by @zhangzhonghe
- 修复移动端子页面中,使用数据模板创建区块时,样式异常的问题 ([#5340](https://github.com/nocobase/nocobase/pull/5340)) by @zhangzhonghe
- 修复通过页面菜单关闭子页面时,不刷新页面区块数据的问题 ([#5331](https://github.com/nocobase/nocobase/pull/5331)) by @zhangzhonghe
- **[操作:导出记录]** 修复 decimal 类型字段的导出格式 ([#5316](https://github.com/nocobase/nocobase/pull/5316)) by @chareice
- **[区块:看板]** 修复在嵌入页面中,点击看板卡片后,无法打开弹窗的问题 ([#5326](https://github.com/nocobase/nocobase/pull/5326)) by @zhangzhonghe
## [v1.3.25-beta](https://github.com/nocobase/nocobase/compare/v1.3.24-beta...v1.3.25-beta) - 2024-09-25
### 🚀 优化
- **[client]** 增加日语本地化翻译 ([#5292](https://github.com/nocobase/nocobase/pull/5292)) by @Albert-mah
- **[工作流]** 增加对未注册的节点类型导致错误的跟踪报错 ([#5319](https://github.com/nocobase/nocobase/pull/5319)) by @mytharcher
### 🐛 修复
- **[client]** 修复变量中没有显示完整字段的问题 ([#5310](https://github.com/nocobase/nocobase/pull/5310)) by @zhangzhonghe
- **[工作流]** 修复数据表事件中发生改变的字段被删除后报错的问题 ([#5318](https://github.com/nocobase/nocobase/pull/5318)) by @mytharcher
- **[操作:导出记录]** 修复导出操作时关联表中的字段未执行interface渲染逻辑 ([#5296](https://github.com/nocobase/nocobase/pull/5296)) by @gchust
## [v1.3.24-beta](https://github.com/nocobase/nocobase/compare/v1.3.23-beta...v1.3.24-beta) - 2024-09-23
### 🐛 修复
- **[client]**
- markdown 的handlebars 模板使用#each 渲染数组数据时数据没有正常显示 ([#5305](https://github.com/nocobase/nocobase/pull/5305)) by @katherinehhh
- 外部数据库数据源表格列头不支持排序的问题 ([#5293](https://github.com/nocobase/nocobase/pull/5293)) by @katherinehhh
- **[数据可视化]** 修复图表区块在暗黑主题下的样式问题 ([#5302](https://github.com/nocobase/nocobase/pull/5302)) by @2013xile
## [v1.3.23-beta](https://github.com/nocobase/nocobase/compare/v1.3.22-beta...v1.3.23-beta) - 2024-09-19
### 🚀 优化
- **[用户]** 优化用户管理表格的渲染速度 ([#5276](https://github.com/nocobase/nocobase/pull/5276)) by @2013xile
- **[部门]** 优化部门管理中的用户表格的渲染速度 by @2013xile
### 🐛 修复
- **[client]**
- 修复用户和权限设置页面中`通用操作权限表格``rowKey`不正确问题 ([#5287](https://github.com/nocobase/nocobase/pull/5287)) by @gchust
- 修复在筛选表单中,为日期字段设置日期变量后,导致的筛选结果不正确的问题 ([#5257](https://github.com/nocobase/nocobase/pull/5257)) by @zhangzhonghe
- 表格没有数据时且设置了区块高度时无法设置列宽 ([#5256](https://github.com/nocobase/nocobase/pull/5256)) by @katherinehhh
- 修复表格区块在一开始出现空白行的问题 ([#5284](https://github.com/nocobase/nocobase/pull/5284)) by @zhangzhonghe
- **[create-nocobase-app]** 修复在新增自动编码字段时,配置编码规则的弹窗缺少提交按钮的问题 ([#5281](https://github.com/nocobase/nocobase/pull/5281)) by @zhangzhonghe
- **[database]** 导入支持勾选字段 ([#4992](https://github.com/nocobase/nocobase/pull/4992)) by @chareice
- **[evaluators]** 修复 Math.js 计算输出矩阵类型导致的问题 ([#5270](https://github.com/nocobase/nocobase/pull/5270)) by @mytharcher
- **[日历]** 删除日程弹窗选项不能选择 ([#5274](https://github.com/nocobase/nocobase/pull/5274)) by @katherinehhh
- **[操作:导出记录]** 修复在导出操作中,生成数据表格时,缺少上下文的问题 ([#5286](https://github.com/nocobase/nocobase/pull/5286)) by @gchust
## [v1.3.22-beta](https://github.com/nocobase/nocobase/compare/v1.3.21-beta...v1.3.22-beta) - 2024-09-12
### 🎉 新特性
- **[操作:自定义请求]** 自定义请求按钮的配置中,支持使用 API token 变量 ([#5263](https://github.com/nocobase/nocobase/pull/5263)) by @zhangzhonghe
参考文档:[自定义请求-变量](https://docs-cn.nocobase.com/handbook/action-custom-request#%E5%8F%98%E9%87%8F)
### 🚀 优化
- **[数据表字段Markdown(Vditor)]** 在外部数据源中选字段 UI 的时支持 Vidtor ([#5246](https://github.com/nocobase/nocobase/pull/5246)) by @katherinehhh
### 🐛 修复
- **[日历]** 日历区块结束日期跨月时无法正确显示的问题 ([#5239](https://github.com/nocobase/nocobase/pull/5239)) by @katherinehhh
## [v1.3.21-beta](https://github.com/nocobase/nocobase/compare/v1.3.20-beta...v1.3.21-beta) - 2024-09-10
### 🐛 修复
- **[client]** 修复在使用联动规则时报错的问题(通过 create-nocobase-app 安装的 NocoBase ([#5249](https://github.com/nocobase/nocobase/pull/5249)) by @zhangzhonghe
## [v1.3.20-beta](https://github.com/nocobase/nocobase/compare/v1.3.19-beta...v1.3.20-beta) - 2024-09-10
### 🚀 优化
- **[client]** 数据区块中支持显示更深层级的关系字段 ([#5243](https://github.com/nocobase/nocobase/pull/5243)) by @zhangzhonghe
### 🐛 修复
- **[client]**
- 修改菜单标题时没有实时生效 ([#5207](https://github.com/nocobase/nocobase/pull/5207)) by @katherinehhh
- 支持 Handlebars 模板中关系字段的预加载 ([#5236](https://github.com/nocobase/nocobase/pull/5236)) by @katherinehhh
- **[数据可视化]** 修复存在多个数据源时,图表的数据源上下文不正确的问题 ([#5237](https://github.com/nocobase/nocobase/pull/5237)) by @2013xile
## [v1.3.19-beta](https://github.com/nocobase/nocobase/compare/v1.3.18-beta...v1.3.19-beta) - 2024-09-08
### 🐛 修复
- **[client]** 修复因弹窗与 Link 按钮一起使用,所导致的 URL 异常的问题 ([#5219](https://github.com/nocobase/nocobase/pull/5219)) by @zhangzhonghe
## [v1.3.18-beta](https://github.com/nocobase/nocobase/compare/v1.3.17-beta...v1.3.18-beta) - 2024-09-08
### 🐛 修复
- **[数据表字段:多对多 (数组)]** 修复删除包含多对多(数组)字段的数据表时出现的错误 ([#5231](https://github.com/nocobase/nocobase/pull/5231)) by @2013xile
## [v1.3.17-beta](https://github.com/nocobase/nocobase/compare/v1.3.16-beta...v1.3.17-beta) - 2024-09-07
### 🎉 新特性
- **[client]** 支持在子表单和子表格中配置联动规则。 ([#5159](https://github.com/nocobase/nocobase/pull/5159)) by @zhangzhonghe
### 🚀 优化
- **[client]**
- 显示时间时默认时间为 00:00:00 ([#5226](https://github.com/nocobase/nocobase/pull/5226)) by @chenos
- 插件依赖版本不一致时也可以激活插件 ([#5225](https://github.com/nocobase/nocobase/pull/5225)) by @chenos
- **[server]** 提供更友好的应用级错误提示 ([#5220](https://github.com/nocobase/nocobase/pull/5220)) by @chenos
### 🐛 修复
- **[client]** 修复在详情区块中出现的 “Maximum call stack size exceeded” 错误 ([#5228](https://github.com/nocobase/nocobase/pull/5228)) by @zhangzhonghe
- **[数据表字段:多对多 (数组)]** 修复将 `uid` 类型的字段设置为多对多(数组)字段的目标键时出现的报错 ([#5229](https://github.com/nocobase/nocobase/pull/5229)) by @2013xile
- **[UI schema 存储服务]** 修复 member 角色点击按钮报无权限的问题 ([#5206](https://github.com/nocobase/nocobase/pull/5206)) by @zhangzhonghe
- **[工作流]** 修复创建工作流后类型列展示错误文字的问题 ([#5222](https://github.com/nocobase/nocobase/pull/5222)) by @mytharcher
- **[用户]** 移除在用户管理中编辑用户资料时的手机号格式验证 ([#5221](https://github.com/nocobase/nocobase/pull/5221)) by @2013xile
## [v1.3.16-beta](https://github.com/nocobase/nocobase/compare/v1.3.15-beta...v1.3.16-beta) - 2024-09-06
### 🚀 优化
- **[client]**
- 有UI配置权限但没有数据表查看权限时添加占位 ([#5208](https://github.com/nocobase/nocobase/pull/5208)) by @katherinehhh
- 当缺少 logo 时,显示系统标题。 ([#5175](https://github.com/nocobase/nocobase/pull/5175)) by @maoyutofu
- **[用户认证]** 系统标题支持换行 ([#5211](https://github.com/nocobase/nocobase/pull/5211)) by @chenos
- **[工作流SQL 节点]** 将 SQL 操作节点的结果数据结构调整为仅包含数据部分。 ([#5189](https://github.com/nocobase/nocobase/pull/5189)) by @mytharcher
Reference: [SQL 操作](https://docs-cn.nocobase.com/handbook/workflow/nodes/sql)
- **[权限控制]** 使 `Users & Permissions` 配置页的 `Permissions` Tab 面板可扩展。 ([#5216](https://github.com/nocobase/nocobase/pull/5216)) by @zhangzhonghe
Reference: [开发指南](https://docs-cn.nocobase.com/handbook/acl#%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97)
- **[操作:批量编辑]** 批量更新、批量编辑的 文案 ,“所有” 改成 “全表” ([#5200](https://github.com/nocobase/nocobase/pull/5200)) by @katherinehhh
### 🐛 修复
- **[client]**
- 修复联动规则中切换赋值类型时组件显示错误 ([#5180](https://github.com/nocobase/nocobase/pull/5180)) by @katherinehhh
- 修复数据范围中使用变量报错的问题。 ([#5195](https://github.com/nocobase/nocobase/pull/5195)) by @zhangzhonghe
- 自定义请求按钮的请求后刷新数据设置不生效 ([#5188](https://github.com/nocobase/nocobase/pull/5188)) by @katherinehhh
- **[数据可视化]** 修复聚合选项字段时,获取结果不正确的问题 ([#5214](https://github.com/nocobase/nocobase/pull/5214)) by @2013xile
- **[数据源管理]** 修复`用户和权限`设置页面中数据源表格`rowKey`不正确问题 ([#5215](https://github.com/nocobase/nocobase/pull/5215)) by @gchust
- **[工作流HTTP 请求节点]** 修复请求节点参数使用非字符串变量时的问题。 ([#5204](https://github.com/nocobase/nocobase/pull/5204)) by @mytharcher
- **[数据表字段:公式]** 修复公式字段时间类型测试用例 ([#5197](https://github.com/nocobase/nocobase/pull/5197)) by @katherinehhh
- **[应用的备份与还原(废弃)]** 修复测试用例报错 ([#5201](https://github.com/nocobase/nocobase/pull/5201)) by @chenos
- **[数据源REST API]**
- rest api 数据表 标识不可编辑 by @katherinehhh
- Rest api 多语言调整 by @katherinehhh
## [v1.3.15-beta](https://github.com/nocobase/nocobase/compare/v1.3.14-beta...v1.3.15-beta) - 2024-09-04
### 🐛 修复
- **[工作流]** 修复工作流变量中缺少部分字段可选的问题。 ([#5187](https://github.com/nocobase/nocobase/pull/5187)) by @mytharcher
- **[数据表字段Markdown(Vditor)]** 修复 markdown(Vditor) 字段没有正确显数据(缓存) ([#5176](https://github.com/nocobase/nocobase/pull/5176)) by @katherinehhh
## [v1.3.14-beta](https://github.com/nocobase/nocobase/compare/v1.3.13-beta...v1.3.14-beta) - 2024-09-03
### 🎉 新特性
- **[client]** 支持在筛选表单中配置对多关系目标表中的字段。 ([#5178](https://github.com/nocobase/nocobase/pull/5178)) by @zhangzhonghe
### 🚀 优化
- **[操作:自定义请求]** 去掉添加数据表单自定义请求按钮的联动规则 ([#5179](https://github.com/nocobase/nocobase/pull/5179)) by @katherinehhh
### 🐛 修复
- **[数据表字段:公式]** 公式字段使用日期字段时页面报错 ([#5168](https://github.com/nocobase/nocobase/pull/5168)) by @katherinehhh
## [v1.3.13-beta](https://github.com/nocobase/nocobase/compare/v1.3.12-beta...v1.3.13-beta) - 2024-09-03
### 🐛 修复
- **[操作:导出记录]** 修复导出关系数据不正确的问题 ([#5170](https://github.com/nocobase/nocobase/pull/5170)) by @chareice

View File

@ -6,7 +6,7 @@ ARG BEFORE_PACK_NOCOBASE="ls -l"
ARG PLUGINS_DIRS ARG PLUGINS_DIRS
ENV PLUGINS_DIRS=${PLUGINS_DIRS} ENV PLUGINS_DIRS=${PLUGINS_DIRS}
ENV COMMIT_HASH=${COMMIT_HASH}
RUN npx npm-cli-adduser --username test --password test -e test@nocobase.com -r $VERDACCIO_URL RUN npx npm-cli-adduser --username test --password test -e test@nocobase.com -r $VERDACCIO_URL
@ -40,17 +40,18 @@ RUN cd /app \
&& rm -rf nocobase.tar.gz \ && rm -rf nocobase.tar.gz \
&& tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app . && tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app .
RUN echo "${COMMIT_HASH}" > /tmp/commit_hash.txt
FROM node:20.13-bullseye-slim FROM node:20.13-bullseye-slim
RUN apt-get update && apt-get install -y nginx RUN apt-get update && apt-get install -y nginx libaio1
RUN rm -rf /etc/nginx/sites-enabled/default RUN rm -rf /etc/nginx/sites-enabled/default
COPY ./docker/nocobase/nocobase.conf /etc/nginx/sites-enabled/nocobase.conf COPY ./docker/nocobase/nocobase.conf /etc/nginx/sites-enabled/nocobase.conf
COPY --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz COPY --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz
COPY --from=builder /tmp/commit_hash.txt /app/commit_hash.txt
WORKDIR /app/nocobase WORKDIR /app/nocobase
RUN mkdir -p /app/nocobase/storage/uploads/ && echo "$COMMIT_HASH" >> /app/nocobase/storage/uploads/COMMIT_HASH
COPY ./docker/nocobase/docker-entrypoint.sh /app/ COPY ./docker/nocobase/docker-entrypoint.sh /app/
CMD ["/app/docker-entrypoint.sh"] CMD ["/app/docker-entrypoint.sh"]

65
Dockerfile.pro Normal file
View File

@ -0,0 +1,65 @@
FROM node:20.13-bullseye as builder
ARG VERDACCIO_URL=http://host.docker.internal:10104/
ARG COMMIT_HASH
ARG APPEND_PRESET_LOCAL_PLUGINS
ARG BEFORE_PACK_NOCOBASE="ls -l"
ARG PLUGINS_DIRS
ENV PLUGINS_DIRS=${PLUGINS_DIRS}
RUN npx npm-cli-adduser --username test --password test -e test@nocobase.com -r $VERDACCIO_URL
RUN apt-get update && apt-get install -y jq
WORKDIR /tmp
COPY . /tmp
RUN yarn install && yarn build --no-dts
RUN cd /tmp && \
NEWVERSION="$(cat lerna.json | jq '.version' | tr -d '"').$(date +'%Y%m%d%H%M%S')" \
&& git checkout -b release-$(date +'%Y%m%d%H%M%S') \
&& yarn lerna version ${NEWVERSION} -y --no-git-tag-version
RUN git config user.email "test@mail.com" \
&& git config user.name "test" && git add . \
&& git commit -m "chore(versions): test publish packages"
RUN yarn release:force --registry $VERDACCIO_URL
RUN yarn config set registry $VERDACCIO_URL
WORKDIR /app
RUN cd /app \
&& yarn config set network-timeout 600000 -g \
&& yarn create nocobase-app my-nocobase-app -a -e APP_ENV=production -e APPEND_PRESET_LOCAL_PLUGINS=$APPEND_PRESET_LOCAL_PLUGINS \
&& cd /app/my-nocobase-app \
&& yarn install --production
WORKDIR /app/my-nocobase-app
RUN $BEFORE_PACK_NOCOBASE
RUN cd /app \
&& rm -rf my-nocobase-app/packages/app/client/src/.umi \
&& rm -rf nocobase.tar.gz \
&& tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app .
FROM node:20.13-bullseye-slim
RUN apt-get update && apt-get install -y nginx
RUN rm -rf /etc/nginx/sites-enabled/default
COPY ./docker/nocobase/nocobase.conf /etc/nginx/sites-enabled/nocobase.conf
COPY --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz
WORKDIR /app/nocobase
RUN mkdir -p /app/nocobase/storage/uploads/ && echo "$COMMIT_HASH" >> /app/nocobase/storage/uploads/COMMIT_HASH
# install postgresql-client and mysql-client
RUN apt update && apt install -y wget postgresql-common gnupg \
&& /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y \
&& apt install -y postgresql-client-16 \
&& wget https://downloads.mysql.com/archives/get/p/23/file/mysql-community-client-core_8.1.0-1debian11_amd64.deb \
&& dpkg -x mysql-community-client-core_8.1.0-1debian11_amd64.deb /tmp/mysql-client \
&& cp /tmp/mysql-client/usr/bin/mysqldump /usr/bin/ \
&& cp /tmp/mysql-client/usr/bin/mysql /usr/bin/
COPY ./docker/nocobase/docker-entrypoint.sh /app/
CMD ["/app/docker-entrypoint.sh"]

View File

@ -1,4 +1,4 @@
Updated Date: August 15, 2024 Updated Date: September 17, 2024
NocoBase License Agreement NocoBase License Agreement
@ -92,21 +92,23 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr
7.1 It is not allowed to remove or change all intellectual property statements about NocoBase in the code. 7.1 It is not allowed to remove or change all intellectual property statements about NocoBase in the code.
7.2 It is not allowed to sell, transfer, lease, share, or give away the Commercial License. 7.2 It is not allowed to sell, transfer, lease, share, or distribute the Commercial License.
7.3 It is not allowed to provide any form of no-code, zero-code, low-code platform SaaS products to the public using the original or modified Software. 7.3 It is not allowed to sell, transfer, lease, share, or distribute any form of no-code, zero-code, low-code platform, or developer tools developed based on Software.
7.4 It is not allowed for Users with a standard license to sell Upper Layer Application to clients without a commercial license. 7.4 It is not allowed to provide any form of no-code, zero-code, low-code platform SaaS products to the public using the original or modified Software.
7.5 It is not allowed to use reverse engineering, decompilation, and other means to try to discover the source code of Commercial Plugins that have not obtained source code license. 7.5 It is not allowed for Users with a standard license to sell Upper Layer Application to clients without a commercial license.
7.6 It is not allowed to disclose the source code of Commercial Plugins to any third party. 7.6 It is not allowed to use reverse engineering, decompilation, and other means to try to discover the source code of Commercial Plugins that have not obtained source code license.
7.7 It is not allowed to publicly sell plugins developed for Software outside of the Marketplace. 7.7 It is not allowed to disclose the source code of Commercial Plugins to any third party.
7.8 If there is a violation of the above obligations or the terms of this Agreement, the rights owned by the User will be immediately terminated, the paid fees will not be refunded, and the Company reserves the right to pursue the User's legal responsibility. 7.8 It is not allowed to publicly sell plugins developed for Software outside of the Marketplace.
7.9 If there are other agreements in the contract for the above obligations, the contract agreement shall prevail. 7.9 If there is a violation of the above obligations or the terms of this Agreement, the rights owned by the User will be immediately terminated, the paid fees will not be refunded, and the Company reserves the right to pursue the User's legal responsibility.
7.10 If there are other agreements in the contract for the above obligations, the contract agreement shall prevail.
============================================================= =============================================================
8. Legal Jurisdiction, Interpretation, and Dispute Resolution 8. Legal Jurisdiction, Interpretation, and Dispute Resolution

View File

@ -8,18 +8,20 @@ https://github.com/user-attachments/assets/b11cbb68-76bc-4e8b-a2aa-2a1feed0ab77
<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&#0045;first&#0044;&#0032;open&#0045;source&#0032;no&#0045;code&#0032;platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></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&#0045;first&#0044;&#0032;open&#0045;source&#0032;no&#0045;code&#0032;platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
## 最近の重要なリリース ## 最近の重要なリリース
- [v1.0.1-alpha.1:ブロックの高さ設定をサポート - 2024/06/07](https://docs-cn.nocobase.com/welcome/changelog/20240607)
- [v1.0.0-alpha.15:新しいプラグインの追加、「設定操作」インターフェースの改善 - 2024/05/19](https://docs-cn.nocobase.com/welcome/changelog/20240519) - [v1.3REST API データソース、モバイル版 V2 などの新機能 - 2024/08/29](https://www.nocobase.com/en/blog/nocobase-1-3)
- [v1.0:新しいマイルストーン - 2024/04/28](https://docs-cn.nocobase.com/welcome/release/v1001-changelog) - [v1.0.1-alpha.1:ブロックの高さ設定をサポート - 2024/06/07](https://www.nocobase.com/en/blog/release-v101-alpha1)
- [v0.21:ブロックのパフォーマンスの最適化 - 2024/03/29](https://docs-cn.nocobase.com/welcome/release/v0210-changelog) - [v1.0.0-alpha.15:新しいプラグインの追加、「設定操作」インターフェースの改善 - 2024/05/19](https://www.nocobase.com/en/blog/release-v100-alpha15)
- [v0.20:複数のデータソースをサポート - 2024/03/03](https://docs-cn.nocobase.com/welcome/release/v0200-changelog) - [v1.0:新しいマイルストーン - 2024/04/28](https://www.nocobase.com/en/blog/release-v10)
- [v0.19:アプリケーションフローの最適化 - 2024/01/08](https://blog-cn.nocobase.com/posts/release-v019/) - [v0.21:ブロックのパフォーマンスの最適化 - 2024/03/29](https://www.nocobase.com/en/blog/release-v021)
- [v0.18:完全なテストシステムの確立 - 2023/12/21](https://blog-cn.nocobase.com/posts/release-v018/) - [v0.20:複数のデータソースをサポート - 2024/03/03](https://www.nocobase.com/en/blog/release-v020)
- [v0.17新しいSchemaInitializerおよびSchemaSettings - 2023/12/11](https://blog-cn.nocobase.com/posts/release-v017/) - [v0.19:アプリケーションフローの最適化 - 2024/01/08](https://www.nocobase.com/en/blog/release-v019)
- [v0.16:新しいキャッシュモジュール - 2023/11/20](https://blog-cn.nocobase.com/posts/release-v016/) - [v0.18:完全なテストシステムの確立 - 2023/12/21](https://www.nocobase.com/en/blog/release-v018)
- [v0.15:新しいプラグイン設定センター - 2023/11/13](https://blog-cn.nocobase.com/posts/release-v015/) - [v0.17新しいSchemaInitializerおよびSchemaSettings - 2023/12/11](https://www.nocobase.com/en/blog/release-v017)
- [v0.14:新しいプラグインマネージャー、インターフェースを通じたプラグインの追加をサポート - 2023/09/11](https://blog-cn.nocobase.com/posts/release-v014/) - [v0.16:新しいキャッシュモジュール - 2023/11/20](https://www.nocobase.com/en/blog/release-v016)
- [v0.13: 新しいアプリケーションステートフロー - 2023/08/24](https://blog-cn.nocobase.com/posts/release-v013/) - [v0.15:新しいプラグイン設定センター - 2023/11/13](https://www.nocobase.com/en/blog/release-v015)
- [v0.14:新しいプラグインマネージャー、インターフェースを通じたプラグインの追加をサポート - 2023/09/11](https://www.nocobase.com/en/blog/release-v014)
- [v0.13: 新しいアプリケーションステートフロー - 2023/08/24](https://www.nocobase.com/en/blog/release-v013)
## NocoBaseはなに ## NocoBaseはなに

View File

@ -10,18 +10,19 @@ https://github.com/nocobase/nocobase/assets/1267426/1d6a3979-d1eb-4e50-b726-2f90
## Recent major updates ## Recent major updates
- [v1.0.1-alpha.1: Blocks support height settings - 2024/06/07](https://docs.nocobase.com/welcome/changelog/20240607) - [v1.3: REST API data source, mobile v2, and more features - 2024/08/29](https://www.nocobase.com/en/blog/nocobase-1-3)
- [v1.0.0-alpha.15: New Plugins and Improved “Configure actions” Interaction - 2024/05/19](https://docs.nocobase.com/welcome/changelog/20240519) - [v1.0.1-alpha.1: Blocks support height settings - 2024/06/07](https://www.nocobase.com/en/blog/release-v101-alpha1)
- [v1.0: Significant Milestone - 2024/04/28](https://docs.nocobase.com/welcome/release/v1001-changelog) - [v1.0.0-alpha.15: New Plugins and Improved “Configure actions” Interaction - 2024/05/19](https://www.nocobase.com/en/blog/release-v100-alpha15)
- [v0.21: Block performance optimization - 2024/03/29](https://docs.nocobase.com/welcome/release/v0210-changelog) - [v1.0: Significant Milestone - 2024/04/28](https://www.nocobase.com/en/blog/release-v10)
- [v0.20: Support for multiple data sources - 2024/03/03](https://docs.nocobase.com/welcome/release/v0200-changelog) - [v0.21: Block performance optimization - 2024/03/29](https://www.nocobase.com/en/blog/release-v021)
- [v0.19: Application process optimization - 2024/01/08](https://docs.nocobase.com/welcome/release/v0190-changelog) - [v0.20: Support for multiple data sources - 2024/03/03](https://www.nocobase.com/en/blog/release-v020)
- [v0.18: Establish a sound testing system - 2023/12/21](https://docs.nocobase.com/welcome/release/v0180-changelog) - [v0.19: Application process optimization - 2024/01/08](https://www.nocobase.com/en/blog/release-v019)
- [v0.17: New SchemaInitializer and SchemaSettings - 2023/12/11](https://docs.nocobase.com/welcome/release/v0170-changelog) - [v0.18: Establish a sound testing system - 2023/12/21](https://www.nocobase.com/en/blog/release-v018)
- [v0.16: New cache manager - 2023/11/20](https://docs.nocobase.com/welcome/release/v0160-changelog) - [v0.17: New SchemaInitializer and SchemaSettings - 2023/12/11](https://www.nocobase.com/en/blog/release-v017)
- [v0.15: New plugin settings manager - 2023/11/13](https://docs.nocobase.com/welcome/release/v0150-changelog) - [v0.16: New cache manager - 2023/11/20](https://www.nocobase.com/en/blog/release-v016)
- [v0.14: New plugin manager, supports adding plugins through UI - 2023/09/11](https://docs.nocobase.com/welcome/release/v0140-changelog) - [v0.15: New plugin settings manager - 2023/11/13](https://www.nocobase.com/en/blog/release-v015)
- [v0.13: New application status flow - 2023/08/24](https://docs.nocobase.com/welcome/release/v0130-changelog) - [v0.14: New plugin manager, supports adding plugins through UI - 2023/09/11](https://www.nocobase.com/en/blog/release-v014)
- [v0.13: New application status flow - 2023/08/24](https://www.nocobase.com/en/blog/release-v013)
## What is NocoBase ## What is NocoBase

View File

@ -11,18 +11,19 @@ https://github.com/nocobase/nocobase/assets/1267426/29623e45-9a48-4598-bb9e-9dd1
我们正在招聘远程 **全栈开发工程师****测试工程师****技术培训与文档专家**。 欢迎对 NocoBase 有强烈兴趣的伙伴加入。[查看详情](https://www.nocobase.com/cn/recruitment) 我们正在招聘远程 **全栈开发工程师****测试工程师****技术培训与文档专家**。 欢迎对 NocoBase 有强烈兴趣的伙伴加入。[查看详情](https://www.nocobase.com/cn/recruitment)
## 最近重要更新 ## 最近重要更新
- [v1.0.1-alpha.1:区块支持高度设置 - 2024/06/07](https://docs-cn.nocobase.com/welcome/changelog/20240607) - [v1.3REST API 数据源、移动端 V2 和更多功能 - 2024/08/29](https://www.nocobase.com/cn/blog/nocobase-1-3)
- [v1.0.0-alpha.15:新增插件、改进「配置操作」交互 - 2024/05/19](https://docs-cn.nocobase.com/welcome/changelog/20240519) - [v1.0.1-alpha.1:区块支持高度设置 - 2024/06/07](https://www.nocobase.com/cn/blog/release-v101-alpha1)
- [v1.0:新的里程碑 - 2024/04/28](https://docs-cn.nocobase.com/welcome/release/v1001-changelog) - [v1.0.0-alpha.15:新增插件、改进「配置操作」交互 - 2024/05/19](https://www.nocobase.com/cn/blog/release-v100-alpha15)
- [v0.21:优化区块性能 - 2024/03/29](https://docs-cn.nocobase.com/welcome/release/v0210-changelog) - [v1.0:新的里程碑 - 2024/04/28](https://www.nocobase.com/cn/blog/release-v10)
- [v0.20:支持多数据源 - 2024/03/03](https://docs-cn.nocobase.com/welcome/release/v0200-changelog) - [v0.21:优化区块性能 - 2024/03/29](https://www.nocobase.com/cn/blog/release-v021)
- [v0.19:应用流程优化 - 2024/01/08](https://blog-cn.nocobase.com/posts/release-v019/) - [v0.20:支持多数据源 - 2024/03/03](https://www.nocobase.com/cn/blog/release-v020)
- [v0.18:建立健全的测试体系 - 2023/12/21](https://blog-cn.nocobase.com/posts/release-v018/) - [v0.19:应用流程优化 - 2024/01/08](https://www.nocobase.com/cn/blog/release-v019)
- [v0.17:全新的 SchemaInitializer 和 SchemaSettings - 2023/12/11](https://blog-cn.nocobase.com/posts/release-v017/) - [v0.18:建立健全的测试体系 - 2023/12/21](https://www.nocobase.com/cn/blog/release-v018)
- [v0.16:全新的缓存模块 - 2023/11/20](https://blog-cn.nocobase.com/posts/release-v016/) - [v0.17:全新的 SchemaInitializer 和 SchemaSettings - 2023/12/11](https://www.nocobase.com/cn/blog/release-v017)
- [v0.15:全新的插件设置中心 - 2023/11/13](https://blog-cn.nocobase.com/posts/release-v015/) - [v0.16:全新的缓存模块 - 2023/11/20](https://www.nocobase.com/cn/blog/release-v016)
- [v0.14:全新的插件管理器,支持通过界面添加插件 - 2023/09/11](https://blog-cn.nocobase.com/posts/release-v014/) - [v0.15:全新的插件设置中心 - 2023/11/13](https://www.nocobase.com/cn/blog/release-v015)
- [v0.13: 全新的应用状态流转 - 2023/08/24](https://blog-cn.nocobase.com/posts/release-v013/) - [v0.14:全新的插件管理器,支持通过界面添加插件 - 2023/09/11](https://www.nocobase.com/cn/blog/release-v014)
- [v0.13: 全新的应用状态流转 - 2023/08/24](https://www.nocobase.com/cn/blog/release-v013)
## NocoBase 是什么 ## NocoBase 是什么

View File

@ -1,6 +1,8 @@
#!/bin/sh #!/bin/sh
set -e set -e
echo "COMMIT_HASH: $(cat /app/commit_hash.txt)"
if [ ! -d "/app/nocobase" ]; then if [ ! -d "/app/nocobase" ]; then
mkdir nocobase mkdir nocobase
fi fi
@ -18,6 +20,14 @@ ln -s /app/nocobase/storage/nocobase.conf /etc/nginx/sites-enabled/nocobase.conf
nginx nginx
echo 'nginx started'; echo 'nginx started';
# run scripts in storage/scripts
if [ -d "/app/nocobase/storage/scripts" ]; then
for f in /app/nocobase/storage/scripts/*.sh; do
echo "Running $f"
sh "$f"
done
fi
cd /app/nocobase && yarn start --quickstart cd /app/nocobase && yarn start --quickstart
# Run command with node if the first argument contains a "-" or is not a system command. The last # Run command with node if the first argument contains a "-" or is not a system command. The last

View File

@ -3,9 +3,14 @@
FILES=$(find packages/pro-plugins/@nocobase -name .npmignore) FILES=$(find packages/pro-plugins/@nocobase -name .npmignore)
if [ "$1" == "ignore-src" ]; then if [ "$1" == "ignore-src" ]; then
CONTENT="/node_modules" CONTENT="/node_modules
/docker
/docs
"
else else
CONTENT="/node_modules CONTENT="/node_modules
/docker
/docs
/src" /src"
fi fi

View File

@ -146,4 +146,52 @@ describe('list action', () => {
expect(response.status).toEqual(200); expect(response.status).toEqual(200);
expect(response.body.count).toEqual(0); expect(response.body.count).toEqual(0);
}); });
it('should list with simple paginate', async () => {
const Item = app.collection({
name: 'items',
simplePaginate: true,
fields: [{ type: 'string', name: 'name' }],
});
await app.db.sync();
await Item.repository.create({
values: [
{
name: 'item1',
},
{
name: 'item2',
},
{
name: 'item3',
},
],
});
const response = await app
.agent()
.resource('items')
.list({
fields: ['id'],
pageSize: 1,
page: 2,
});
const body = response.body;
expect(body.hasNext).toBeTruthy();
const lastPageResponse = await app
.agent()
.resource('items')
.list({
fields: ['id'],
pageSize: 1,
page: 3,
});
const lastPageBody = lastPageResponse.body;
expect(lastPageBody.hasNext).toBeFalsy();
});
}); });

View File

@ -7,7 +7,6 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { assign, isValidFilter } from '@nocobase/utils';
import { Context } from '..'; import { Context } from '..';
import { getRepositoryFromParams, pageArgsToLimitArgs } from '../utils'; import { getRepositoryFromParams, pageArgsToLimitArgs } from '../utils';
import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '../constants'; import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '../constants';
@ -49,9 +48,13 @@ async function listWithPagination(ctx: Context) {
}); });
if (simplePaginate) { if (simplePaginate) {
options.limit = options.limit + 1;
const rows = await repository.find(options); const rows = await repository.find(options);
ctx.body = { ctx.body = {
rows, rows: rows.slice(0, pageSize),
hasNext: rows.length > pageSize,
page: Number(page), page: Number(page),
pageSize: Number(pageSize), pageSize: Number(pageSize),
}; };

View File

@ -1,4 +1,4 @@
import { getUmiConfig, IndexGenerator } from '@nocobase/devtools/umiConfig'; import { generatePlugins, getUmiConfig } from '@nocobase/devtools/umiConfig';
import path from 'path'; import path from 'path';
import { defineConfig } from 'umi'; import { defineConfig } from 'umi';
@ -8,17 +8,11 @@ process.env.MFSU_AD = 'none';
process.env.DID_YOU_KNOW = 'none'; process.env.DID_YOU_KNOW = 'none';
const pluginPrefix = (process.env.PLUGIN_PACKAGE_PREFIX || '').split(',').filter((item) => !item.includes('preset')); // 因为现在 preset 是直接引入的,所以不能忽略,如果以后 preset 也是动态插件的形式引入,那么这里可以去掉 const pluginPrefix = (process.env.PLUGIN_PACKAGE_PREFIX || '').split(',').filter((item) => !item.includes('preset')); // 因为现在 preset 是直接引入的,所以不能忽略,如果以后 preset 也是动态插件的形式引入,那么这里可以去掉
const pluginDirs = (process.env.PLUGIN_PATH || 'packages/plugins/,packages/samples/,packages/pro-plugins/')
.split(',').map(item => path.join(process.cwd(), item));
const outputPluginPath = path.join(__dirname, 'src', '.plugins');
const indexGenerator = new IndexGenerator(outputPluginPath, pluginDirs);
indexGenerator.generate();
const isDevCmd = !!process.env.IS_DEV_CMD; const isDevCmd = !!process.env.IS_DEV_CMD;
const appPublicPath = isDevCmd ? '/' : '{{env.APP_PUBLIC_PATH}}'; const appPublicPath = isDevCmd ? '/' : '{{env.APP_PUBLIC_PATH}}';
generatePlugins();
export default defineConfig({ export default defineConfig({
title: 'Loading...', title: 'Loading...',
devtool: process.env.NODE_ENV === 'development' ? 'source-map' : false, devtool: process.env.NODE_ENV === 'development' ? 'source-map' : false,

Binary file not shown.

After

Width:  |  Height:  |  Size: 979 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1017 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 981 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

View File

@ -70,7 +70,7 @@ describe('middleware', () => {
hasFn.mockImplementation(() => true); hasFn.mockImplementation(() => true);
const res = await agent.resource('auth').check(); const res = await agent.resource('auth').check();
expect(res.status).toBe(401); expect(res.status).toBe(401);
expect(res.text).toContain('token is not available'); expect(res.text).toContain('Token is invalid');
}); });
}); });
}); });

View File

@ -109,7 +109,10 @@ export class AuthManager {
return async (ctx: Context & { auth: Auth }, next: Next) => { return async (ctx: Context & { auth: Auth }, next: Next) => {
const token = ctx.getBearerToken(); const token = ctx.getBearerToken();
if (token && (await ctx.app.authManager.jwt.blacklist?.has(token))) { if (token && (await ctx.app.authManager.jwt.blacklist?.has(token))) {
return ctx.throw(401, ctx.t('token is not available')); return ctx.throw(401, {
code: 'TOKEN_INVALID',
message: ctx.t('Token is invalid'),
});
} }
const name = ctx.get(this.options.authKey) || this.options.default; const name = ctx.get(this.options.authKey) || this.options.default;

View File

@ -69,14 +69,14 @@ export class BaseAuth extends Auth {
return null; return null;
} }
try { try {
const { userId, roleName } = await this.jwt.decode(token); const { userId, roleName, iat, temp } = await this.jwt.decode(token);
if (roleName) { if (roleName) {
this.ctx.headers['x-role'] = roleName; this.ctx.headers['x-role'] = roleName;
} }
const cache = this.ctx.cache as Cache; const cache = this.ctx.cache as Cache;
return await cache.wrap(this.getCacheKey(userId), () => const user = await cache.wrap(this.getCacheKey(userId), () =>
this.userRepository.findOne({ this.userRepository.findOne({
filter: { filter: {
id: userId, id: userId,
@ -84,6 +84,10 @@ export class BaseAuth extends Auth {
raw: true, raw: true,
}), }),
); );
if (temp && user.passwordChangeTz && iat * 1000 < user.passwordChangeTz) {
throw new Error('Token is invalid');
}
return user;
} catch (err) { } catch (err) {
this.ctx.logger.error(err, { method: 'check' }); this.ctx.logger.error(err, { method: 'check' });
return null; return null;
@ -106,6 +110,7 @@ export class BaseAuth extends Auth {
} }
const token = this.jwt.sign({ const token = this.jwt.sign({
userId: user.id, userId: user.id,
temp: true,
}); });
return { return {
user, user,
@ -119,7 +124,7 @@ export class BaseAuth extends Auth {
return; return;
} }
const { userId } = await this.jwt.decode(token); const { userId } = await this.jwt.decode(token);
await this.ctx.app.emitAsync('beforeSignOut', { userId }); await this.ctx.app.emitAsync('cache:del:roles', { userId });
await this.ctx.cache.del(this.getCacheKey(userId)); await this.ctx.cache.del(this.getCacheKey(userId));
return await this.jwt.block(token); return await this.jwt.block(token);
} }

View File

@ -11,7 +11,7 @@
"dependencies": { "dependencies": {
"@babel/core": "7.25.2", "@babel/core": "7.25.2",
"@babel/plugin-transform-modules-amd": "7.24.7", "@babel/plugin-transform-modules-amd": "7.24.7",
"@babel/preset-env": "7.25.3", "@babel/preset-env": "7.25.4",
"@hapi/topo": "^6.0.0", "@hapi/topo": "^6.0.0",
"@lerna/project": "4.0.0", "@lerna/project": "4.0.0",
"@types/gulp": "^4.0.13", "@types/gulp": "^4.0.13",

View File

@ -22,7 +22,7 @@
"portfinder": "^1.0.28", "portfinder": "^1.0.28",
"serve": "^13.0.2", "serve": "^13.0.2",
"tree-kill": "^1.2.2", "tree-kill": "^1.2.2",
"tsx": "^4.6.2" "tsx": "^4.19.0"
}, },
"devDependencies": { "devDependencies": {
"@nocobase/devtools": "1.4.0-alpha" "@nocobase/devtools": "1.4.0-alpha"

View File

@ -9,8 +9,12 @@
const chalk = require('chalk'); const chalk = require('chalk');
const { Command } = require('commander'); const { Command } = require('commander');
const { runAppCommand, runInstall, run, postCheck, nodeCheck, promptForTs } = require('../util'); const { generatePlugins, run, postCheck, nodeCheck, promptForTs } = require('../util');
const { getPortPromise } = require('portfinder'); const { getPortPromise } = require('portfinder');
const chokidar = require('chokidar');
const { uid } = require('@formily/shared');
const path = require('path');
const fs = require('fs');
/** /**
* *
@ -27,6 +31,25 @@ module.exports = (cli) => {
.option('--inspect [port]') .option('--inspect [port]')
.allowUnknownOption() .allowUnknownOption()
.action(async (opts) => { .action(async (opts) => {
const watcher = chokidar.watch('./storage/plugins/**/*', {
cwd: process.cwd(),
ignored: /(^|[\/\\])\../, // 忽略隐藏文件
persistent: true,
depth: 1, // 只监听第一层目录
});
watcher
.on('addDir', async (pathname) => {
generatePlugins();
const file = path.resolve(process.cwd(), 'storage/app.watch.ts');
await fs.promises.writeFile(file, `export const watchId = '${uid()}';`, 'utf-8');
})
.on('unlinkDir', async (pathname) => {
generatePlugins();
const file = path.resolve(process.cwd(), 'storage/app.watch.ts');
await fs.promises.writeFile(file, `export const watchId = '${uid()}';`, 'utf-8');
});
promptForTs(); promptForTs();
const { SERVER_TSCONFIG_PATH } = process.env; const { SERVER_TSCONFIG_PATH } = process.env;
process.env.IS_DEV_CMD = true; process.env.IS_DEV_CMD = true;

View File

@ -8,7 +8,7 @@
*/ */
const { Command } = require('commander'); const { Command } = require('commander');
const { run, isDev, isProd, promptForTs } = require('../util'); const { run, isDev, isProd, promptForTs, downloadPro } = require('../util');
/** /**
* *
@ -20,10 +20,14 @@ module.exports = (cli) => {
.allowUnknownOption() .allowUnknownOption()
.option('-h, --help') .option('-h, --help')
.option('--ts-node-dev') .option('--ts-node-dev')
.action((options) => { .action(async (options) => {
const cmd = process.argv.slice(2)?.[0];
if (cmd === 'install') {
await downloadPro();
}
if (isDev()) { if (isDev()) {
promptForTs(); promptForTs();
run('tsx', [ await run('tsx', [
'--tsconfig', '--tsconfig',
SERVER_TSCONFIG_PATH, SERVER_TSCONFIG_PATH,
'-r', '-r',
@ -32,7 +36,7 @@ module.exports = (cli) => {
...process.argv.slice(2), ...process.argv.slice(2),
]); ]);
} else if (isProd()) { } else if (isProd()) {
run('node', [`${APP_PACKAGE_ROOT}/lib/index.js`, ...process.argv.slice(2)]); await run('node', [`${APP_PACKAGE_ROOT}/lib/index.js`, ...process.argv.slice(2)]);
} }
}); });
}; };

View File

@ -31,6 +31,7 @@ module.exports = (cli) => {
require('./umi')(cli); require('./umi')(cli);
require('./upgrade')(cli); require('./upgrade')(cli);
require('./postinstall')(cli); require('./postinstall')(cli);
require('./pkg')(cli);
if (isPackageValid('@umijs/utils')) { if (isPackageValid('@umijs/utils')) {
require('./create-plugin')(cli); require('./create-plugin')(cli);
} }

View File

@ -0,0 +1,218 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
const { Command } = require('commander');
const axios = require('axios');
const fs = require('fs-extra');
const zlib = require('zlib');
const tar = require('tar');
const path = require('path');
const { createStoragePluginsSymlink } = require('@nocobase/utils/plugin-symlink');
const chalk = require('chalk');
class Package {
data;
constructor(packageName, packageManager) {
this.packageName = packageName;
this.packageManager = packageManager;
this.outputDir = path.resolve(process.cwd(), `storage/plugins/${this.packageName}`);
}
get token() {
return this.packageManager.getToken();
}
url(path) {
return this.packageManager.url(path);
}
async mkdir() {
if (await fs.exists(this.outputDir)) {
await fs.rm(this.outputDir, { recursive: true, force: true });
}
await fs.mkdirp(this.outputDir);
}
async getInfo() {
try {
const res = await axios.get(this.url(this.packageName), {
headers: {
Authorization: `Bearer ${this.token}`,
},
responseType: 'json',
});
this.data = res.data;
} catch (error) {
return;
}
}
getTarball(version = 'latest') {
if (this.data.versions[version]) {
return [version, this.data.versions[version].dist.tarball];
}
if (version.includes('beta')) {
version = version.split('beta')[0] + 'beta';
} else if (version.includes('alpha')) {
const prefix = (version = version.split('alpha')[0]);
version = Object.keys(this.data.versions)
.filter((ver) => ver.startsWith(`${prefix}alpha`))
.sort()
.pop();
}
if (version === 'latest') {
version = this.data['dist-tags']['latest'];
} else if (version === 'next') {
version = this.data['dist-tags']['next'];
}
if (!this.data.versions[version]) {
console.log(chalk.redBright(`Download failed: ${this.packageName}@${version} package does not exist`));
}
return [version, this.data.versions[version].dist.tarball];
}
async isDevPackage() {
let file = path.resolve(process.cwd(), 'packages/plugins', this.packageName, 'package.json');
if (await fs.exists(file)) {
return true;
}
file = path.resolve(process.cwd(), 'packages/pro-plugins', this.packageName, 'package.json');
if (await fs.exists(file)) {
return true;
}
return false;
}
async download(options = {}) {
if (await this.isDevPackage()) {
console.log(chalk.yellowBright(`Skipped: ${this.packageName} is dev package`));
return;
}
await this.getInfo();
if (!this.data) {
console.log(chalk.redBright(`Download failed: ${this.packageName} package does not exist`));
return;
}
try {
const [version, url] = this.getTarball(options.version);
const response = await axios({
url,
responseType: 'stream',
method: 'GET',
headers: {
Authorization: `Bearer ${this.token}`,
},
});
await this.mkdir();
await new Promise((resolve, reject) => {
response.data
.pipe(zlib.createGunzip()) // 解压 gzip
.pipe(tar.extract({ cwd: this.outputDir, strip: 1 })) // 解压 tar
.on('finish', resolve)
.on('error', reject);
});
console.log(chalk.greenBright(`Download success: ${this.packageName}@${version}`));
} catch (error) {
console.log(chalk.redBright(`Download failed: ${this.packageName}`));
}
}
}
class PackageManager {
token;
baseURL;
constructor({ baseURL }) {
this.baseURL = baseURL;
}
getToken() {
return this.token;
}
getBaseURL() {
return this.baseURL;
}
url(path) {
return this.baseURL + path;
}
async login(credentials) {
try {
const res1 = await axios.post(`${this.baseURL}-/verdaccio/sec/login`, credentials, {
responseType: 'json',
});
this.token = res1.data.token;
} catch (error) {
console.error(chalk.redBright(`Login failed: ${this.baseURL}`));
}
}
getPackage(packageName) {
return new Package(packageName, this);
}
async getProPackages() {
const res = await axios.get(this.url('pro-packages'), {
headers: {
Authorization: `Bearer ${this.token}`,
},
responseType: 'json',
});
return res.data.data;
}
async getPackages() {
const pkgs = await this.getProPackages();
return pkgs;
}
async download(options = {}) {
const { version } = options;
if (!this.token) {
return;
}
const pkgs = await this.getPackages();
for (const pkg of pkgs) {
await this.getPackage(pkg).download({ version });
}
}
}
/**
*
* @param {Command} cli
*/
module.exports = (cli) => {
const pkg = cli.command('pkg');
pkg
.command('download-pro')
.option('-V, --version [version]')
.action(async () => {
const { NOCOBASE_PKG_URL, NOCOBASE_PKG_USERNAME, NOCOBASE_PKG_PASSWORD } = process.env;
if (!(NOCOBASE_PKG_URL && NOCOBASE_PKG_USERNAME && NOCOBASE_PKG_PASSWORD)) {
return;
}
const credentials = { username: NOCOBASE_PKG_USERNAME, password: NOCOBASE_PKG_PASSWORD };
const pm = new PackageManager({ baseURL: NOCOBASE_PKG_URL });
await pm.login(credentials);
const file = path.resolve(__dirname, '../../package.json');
const json = await fs.readJson(file);
await pm.download({ version: json.version });
await createStoragePluginsSymlink();
});
pkg.command('export-all').action(async () => {
console.log('Todo...');
});
};

View File

@ -8,7 +8,7 @@
*/ */
const { Command } = require('commander'); const { Command } = require('commander');
const { run, isDev, isPackageValid, generatePlaywrightPath } = require('../util'); const { run, isDev, isPackageValid, generatePlaywrightPath, generatePlugins } = require('../util');
const { dirname, resolve } = require('path'); const { dirname, resolve } = require('path');
const { existsSync, mkdirSync, readFileSync, appendFileSync } = require('fs'); const { existsSync, mkdirSync, readFileSync, appendFileSync } = require('fs');
const { readFile, writeFile } = require('fs').promises; const { readFile, writeFile } = require('fs').promises;
@ -41,7 +41,7 @@ module.exports = (cli) => {
.option('--skip-umi') .option('--skip-umi')
.action(async (options) => { .action(async (options) => {
writeToExclude(); writeToExclude();
generatePlugins();
generatePlaywrightPath(true); generatePlaywrightPath(true);
await createStoragePluginsSymlink(); await createStoragePluginsSymlink();
if (!isDev()) { if (!isDev()) {

View File

@ -8,10 +8,11 @@
*/ */
const { Command } = require('commander'); const { Command } = require('commander');
const { isDev, run, postCheck, runInstall, promptForTs } = require('../util'); const { isDev, run, postCheck, downloadPro, promptForTs } = require('../util');
const { existsSync, rmSync } = require('fs'); const { existsSync, rmSync } = require('fs');
const { resolve } = require('path'); const { resolve } = require('path');
const chalk = require('chalk'); const chalk = require('chalk');
const chokidar = require('chokidar');
function deleteSockFiles() { function deleteSockFiles() {
const { SOCKET_PATH, PM2_HOME } = process.env; const { SOCKET_PATH, PM2_HOME } = process.env;
@ -38,6 +39,23 @@ module.exports = (cli) => {
.option('--quickstart') .option('--quickstart')
.allowUnknownOption() .allowUnknownOption()
.action(async (opts) => { .action(async (opts) => {
if (opts.quickstart) {
await downloadPro();
}
const watcher = chokidar.watch('./storage/plugins/**/*', {
cwd: process.cwd(),
ignoreInitial: true,
ignored: /(^|[\/\\])\../, // 忽略隐藏文件
persistent: true,
depth: 1, // 只监听第一层目录
});
watcher.on('addDir', async (pathname) => {
console.log('pathname', pathname);
await run('yarn', ['nocobase', 'pm2-restart']);
});
if (opts.port) { if (opts.port) {
process.env.APP_PORT = opts.port; process.env.APP_PORT = opts.port;
} }

View File

@ -10,7 +10,7 @@
const chalk = require('chalk'); const chalk = require('chalk');
const { Command } = require('commander'); const { Command } = require('commander');
const { resolve } = require('path'); const { resolve } = require('path');
const { run, promptForTs, runAppCommand, hasCorePackages, updateJsonFile, hasTsNode } = require('../util'); const { run, promptForTs, runAppCommand, hasCorePackages, downloadPro, hasTsNode } = require('../util');
const { existsSync, rmSync } = require('fs'); const { existsSync, rmSync } = require('fs');
/** /**
@ -29,15 +29,18 @@ module.exports = (cli) => {
if (hasTsNode()) promptForTs(); if (hasTsNode()) promptForTs();
if (hasCorePackages()) { if (hasCorePackages()) {
// await run('yarn', ['install']); // await run('yarn', ['install']);
await downloadPro();
await runAppCommand('upgrade'); await runAppCommand('upgrade');
return; return;
} }
if (options.skipCodeUpdate) { if (options.skipCodeUpdate) {
await downloadPro();
await runAppCommand('upgrade'); await runAppCommand('upgrade');
return; return;
} }
// await runAppCommand('upgrade'); // await runAppCommand('upgrade');
if (!hasTsNode()) { if (!hasTsNode()) {
await downloadPro();
await runAppCommand('upgrade'); await runAppCommand('upgrade');
return; return;
} }
@ -54,8 +57,9 @@ module.exports = (cli) => {
stdio: 'pipe', stdio: 'pipe',
}); });
if (pkg.version === stdout) { if (pkg.version === stdout) {
await downloadPro();
await runAppCommand('upgrade'); await runAppCommand('upgrade');
rmAppDir(); await rmAppDir();
return; return;
} }
const currentY = 1 * pkg.version.split('.')[1]; const currentY = 1 * pkg.version.split('.')[1];
@ -66,7 +70,8 @@ module.exports = (cli) => {
await run('yarn', ['add', '@nocobase/cli', '@nocobase/devtools', '-W']); await run('yarn', ['add', '@nocobase/cli', '@nocobase/devtools', '-W']);
} }
await run('yarn', ['install']); await run('yarn', ['install']);
await downloadPro();
await runAppCommand('upgrade'); await runAppCommand('upgrade');
rmAppDir(); await rmAppDir();
}); });
}; };

View File

@ -72,7 +72,7 @@ class PluginGenerator extends Generator {
}); });
this.log(''); this.log('');
genTsConfigPaths(); genTsConfigPaths();
execa.sync('yarn', ['postinstall', '--skip-umi'], { shell: true, stdio: 'inherit' }); execa.sync('yarn', ['postinstall'], { shell: true, stdio: 'inherit' });
this.log(`The plugin folder is in ${chalk.green(`packages/plugins/${name}`)}`); this.log(`The plugin folder is in ${chalk.green(`packages/plugins/${name}`)}`);
} }
} }

View File

@ -163,6 +163,10 @@ exports.promptForTs = () => {
console.log(chalk.green('WAIT: ') + 'TypeScript compiling...'); console.log(chalk.green('WAIT: ') + 'TypeScript compiling...');
}; };
exports.downloadPro = async () => {
await exports.run('yarn', ['nocobase', 'pkg', 'download-pro']);
};
exports.updateJsonFile = async (target, fn) => { exports.updateJsonFile = async (target, fn) => {
const content = await readFile(target, 'utf-8'); const content = await readFile(target, 'utf-8');
const json = JSON.parse(content); const json = JSON.parse(content);
@ -416,3 +420,13 @@ exports.initEnv = function initEnv() {
); );
} }
}; };
exports.generatePlugins = function () {
try {
require.resolve('@nocobase/devtools/umiConfig');
const { generatePlugins } = require('@nocobase/devtools/umiConfig');
generatePlugins();
} catch (error) {
return;
}
};

View File

@ -11,6 +11,7 @@
"@ant-design/icons": "^5.1.4", "@ant-design/icons": "^5.1.4",
"@ant-design/pro-layout": "^7.16.11", "@ant-design/pro-layout": "^7.16.11",
"@antv/g2plot": "^2.4.18", "@antv/g2plot": "^2.4.18",
"@budibase/handlebars-helpers": "^0.13.2",
"@ctrl/tinycolor": "^3.6.0", "@ctrl/tinycolor": "^3.6.0",
"@dnd-kit/core": "^5.0.1", "@dnd-kit/core": "^5.0.1",
"@dnd-kit/modifiers": "^6.0.0", "@dnd-kit/modifiers": "^6.0.0",
@ -30,7 +31,7 @@
"@nocobase/sdk": "1.4.0-alpha", "@nocobase/sdk": "1.4.0-alpha",
"@nocobase/utils": "1.4.0-alpha", "@nocobase/utils": "1.4.0-alpha",
"ahooks": "^3.7.2", "ahooks": "^3.7.2",
"antd": "^5.12.8", "antd": "5.12.8",
"antd-style": "3.4.5", "antd-style": "3.4.5",
"axios": "^0.26.1", "axios": "^0.26.1",
"bignumber.js": "^9.1.2", "bignumber.js": "^9.1.2",
@ -48,6 +49,7 @@
"markdown-it-highlightjs": "3.3.1", "markdown-it-highlightjs": "3.3.1",
"mathjs": "^10.6.0", "mathjs": "^10.6.0",
"mermaid": "9.4.3", "mermaid": "9.4.3",
"mime": "^4.0.4",
"mime-match": "^1.0.2", "mime-match": "^1.0.2",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-drag-listview": "^0.1.9", "react-drag-listview": "^0.1.9",
@ -57,7 +59,7 @@
"react-i18next": "^11.15.1", "react-i18next": "^11.15.1",
"react-iframe": "~1.8.5", "react-iframe": "~1.8.5",
"react-image-lightbox": "^5.1.4", "react-image-lightbox": "^5.1.4",
"react-intersection-observer": "9.8.1", "react-intersection-observer": "9.13.0",
"react-js-cron": "^3.1.0", "react-js-cron": "^3.1.0",
"react-quill": "^2.0.0", "react-quill": "^2.0.0",
"react-router-dom": "^6.11.2", "react-router-dom": "^6.11.2",

View File

@ -20,6 +20,7 @@ import { useResourceActionContext } from '../collection-manager/ResourceActionPr
import { useDataSourceKey } from '../data-source/data-source/DataSourceProvider'; import { useDataSourceKey } from '../data-source/data-source/DataSourceProvider';
import { useRecord } from '../record-provider'; import { useRecord } from '../record-provider';
import { SchemaComponentOptions, useDesignable } from '../schema-component'; import { SchemaComponentOptions, useDesignable } from '../schema-component';
import { CollectionNotAllowViewPlaceholder } from '../data-source';
import { useApp } from '../application'; import { useApp } from '../application';
@ -102,6 +103,13 @@ export const useACLContext = () => {
export const ACLActionParamsContext = createContext<any>({}); export const ACLActionParamsContext = createContext<any>({});
ACLActionParamsContext.displayName = 'ACLActionParamsContext'; ACLActionParamsContext.displayName = 'ACLActionParamsContext';
export const ACLCustomContext = createContext<any>({});
ACLCustomContext.displayName = 'ACLCustomContext';
const useACLCustomContext = () => {
return useContext(ACLCustomContext);
};
export const useACLRolesCheck = () => { export const useACLRolesCheck = () => {
const ctx = useContext(ACLContext); const ctx = useContext(ACLContext);
const dataSourceName = useDataSourceKey(); const dataSourceName = useDataSourceKey();
@ -204,11 +212,23 @@ export function useACLRoleContext() {
}; };
} }
/**
* Used to get whether the current user has permission to configure UI
* @returns {allowConfigUI: boolean}
*/
export function useUIConfigurationPermissions(): { allowConfigUI: boolean } {
const { allowAll, snippets } = useACLRoleContext();
return {
allowConfigUI: allowAll || snippets.includes('ui.*'),
};
}
export const ACLCollectionProvider = (props) => { export const ACLCollectionProvider = (props) => {
const { allowAll, parseAction } = useACLRoleContext(); const { allowAll, parseAction } = useACLRoleContext();
const { allowAll: customAllowAll } = useACLCustomContext();
const app = useApp(); const app = useApp();
const schema = useFieldSchema(); const schema = useFieldSchema();
if (allowAll || app.disableAcl) { if (allowAll || app.disableAcl || customAllowAll) {
return props.children; return props.children;
} }
let actionPath = schema?.['x-acl-action'] || props.actionPath; let actionPath = schema?.['x-acl-action'] || props.actionPath;
@ -222,7 +242,7 @@ export const ACLCollectionProvider = (props) => {
} }
const params = parseAction(actionPath, { schema }); const params = parseAction(actionPath, { schema });
if (!params) { if (!params) {
return null; return <CollectionNotAllowViewPlaceholder />;
} }
const [_, actionName] = actionPath.split(':'); const [_, actionName] = actionPath.split(':');
params.actionName = actionName; params.actionName = actionName;

View File

@ -54,6 +54,7 @@ export const StrategyActions = connect((props) => {
<Table <Table
size={'small'} size={'small'}
pagination={false} pagination={false}
rowKey={'name'}
columns={[ columns={[
{ {
dataIndex: 'displayName', dataIndex: 'displayName',

View File

@ -92,17 +92,31 @@ export class APIClient extends APIClientSDK {
return response; return response;
}, },
(error) => { (error) => {
const errs = error?.response?.data?.errors || [{ message: 'Server error' }]; const errs = this.toErrMessages(error);
// Hard code here temporarily // Hard code here temporarily
// TODO(yangqia): improve error code and message // TODO(yangqia): improve error code and message
if (errs.find((error: { code?: string }) => error.code === 'ROLE_NOT_FOUND_ERR')) { if (errs.find((error: { code?: string }) => error.code === 'ROLE_NOT_FOUND_ERR')) {
this.auth.setRole(null); this.auth.setRole(null);
} }
if (errs.find((error: { code?: string }) => error.code === 'TOKEN_INVALID')) {
this.auth.setToken(null);
}
throw error; throw error;
}, },
); );
} }
toErrMessages(error) {
if (typeof error?.response?.data === 'string') {
return [{ message: error?.response?.data }];
}
return (
error?.response?.data?.errors ||
error?.response?.data?.messages ||
error?.response?.error || [{ message: error.message || 'Server error' }]
);
}
useNotificationMiddleware() { useNotificationMiddleware() {
this.axios.interceptors.response.use( this.axios.interceptors.response.use(
(response) => { (response) => {
@ -119,9 +133,11 @@ export class APIClient extends APIClientSDK {
} }
return response; return response;
}, },
(error) => { async (error) => {
if (this.silence) { if (this.silence) {
throw error; console.error(error);
return;
// throw error;
} }
const redirectTo = error?.response?.data?.redirectTo; const redirectTo = error?.response?.data?.redirectTo;
if (redirectTo) { if (redirectTo) {
@ -143,7 +159,7 @@ export class APIClient extends APIClientSDK {
} else if (this.app.error) { } else if (this.app.error) {
this.app.error = null; this.app.error = null;
} }
let errs = error?.response?.data?.errors || error?.response?.data?.messages || [{ message: 'Server error' }]; let errs = this.toErrMessages(error);
errs = errs.filter((error) => { errs = errs.filter((error) => {
const lastTime = errorCache.get(error.message); const lastTime = errorCache.get(error.message);
if (lastTime && new Date().getTime() - lastTime < 500) { if (lastTime && new Date().getTime() - lastTime < 500) {

View File

@ -34,17 +34,17 @@ import { compose, normalizeContainer } from './utils';
import { defineGlobalDeps } from './utils/globalDeps'; import { defineGlobalDeps } from './utils/globalDeps';
import { getRequireJs } from './utils/requirejs'; import { getRequireJs } from './utils/requirejs';
import { CollectionFieldInterfaceComponentOption } from '../data-source/collection-field-interface/CollectionFieldInterface';
import { CollectionField } from '../data-source/collection-field/CollectionField'; import { CollectionField } from '../data-source/collection-field/CollectionField';
import { DataSourceApplicationProvider } from '../data-source/components/DataSourceApplicationProvider'; import { DataSourceApplicationProvider } from '../data-source/components/DataSourceApplicationProvider';
import { DataBlockProvider } from '../data-source/data-block/DataBlockProvider'; import { DataBlockProvider } from '../data-source/data-block/DataBlockProvider';
import { DataSourceManager, type DataSourceManagerOptions } from '../data-source/data-source/DataSourceManager'; import { DataSourceManager, type DataSourceManagerOptions } from '../data-source/data-source/DataSourceManager';
import { CollectionFieldInterfaceComponentOption } from '../data-source/collection-field-interface/CollectionFieldInterface';
import type { CollectionFieldInterfaceFactory } from '../data-source';
import { OpenModeProvider } from '../modules/popup/OpenModeProvider'; import { OpenModeProvider } from '../modules/popup/OpenModeProvider';
import { AppSchemaComponentProvider } from './AppSchemaComponentProvider'; import { AppSchemaComponentProvider } from './AppSchemaComponentProvider';
import type { Plugin } from './Plugin'; import type { Plugin } from './Plugin';
import type { RequireJS } from './utils/requirejs'; import type { RequireJS } from './utils/requirejs';
import type { CollectionFieldInterfaceFactory } from '../data-source';
declare global { declare global {
interface Window { interface Window {
@ -282,10 +282,21 @@ export class Application {
}); });
} }
loadFailed = true; loadFailed = true;
const others = error?.response?.data?.error || error?.response?.data?.errors?.[0] || { message: error?.message }; const toError = (error) => {
if (typeof error?.response?.data === 'string') {
return { message: error?.response?.data };
}
if (error?.response?.data?.error) {
return error?.response?.data?.error;
}
if (error?.response?.data?.errors?.[0]) {
return error?.response?.data?.errors?.[0];
}
return { message: error?.message };
};
this.error = { this.error = {
code: 'LOAD_ERROR', code: 'LOAD_ERROR',
...others, ...toError(error),
}; };
console.error(error, this.error); console.error(error, this.error);
} }

View File

@ -267,7 +267,7 @@ describe('remotePlugins', () => {
expect(requirejs.requirejs.config).toBeCalledTimes(0); expect(requirejs.requirejs.config).toBeCalledTimes(0);
}); });
it('If there is devDynamicImport and devDynamicImport returns partial, remote API will be requested', async () => { it.skip('If there is devDynamicImport and devDynamicImport returns partial, remote API will be requested', async () => {
const remoteFn = vi.fn(); const remoteFn = vi.fn();
const mockPluginsModules = (pluginData, resolve) => { const mockPluginsModules = (pluginData, resolve) => {
remoteFn(); remoteFn();

View File

@ -48,7 +48,11 @@ export const SchemaInitializerMenu: FC<MenuProps> = (props) => {
const { items, ...others } = props; const { items, ...others } = props;
const { token } = theme.useToken(); const { token } = theme.useToken();
const itemsWithPopupClass = useMemo( const itemsWithPopupClass = useMemo(
() => items.map((item) => ({ ...item, popupClassName: `${hashId} ${componentCls}-menu-sub` })), () =>
items.map((item) => ({
...item,
popupClassName: `${hashId} ${componentCls}-menu-sub`,
})),
[componentCls, hashId, items], [componentCls, hashId, items],
); );
// selectedKeys 为了不让有选中效果 // selectedKeys 为了不让有选中效果
@ -62,9 +66,23 @@ export const SchemaInitializerMenu: FC<MenuProps> = (props) => {
border-inline-end: 0 !important; border-inline-end: 0 !important;
.ant-menu-sub { .ant-menu-sub {
max-height: 50vh !important; max-height: 50vh !important;
padding: ${token.paddingXXS}px !important;
} }
.ant-menu-item { .ant-menu-item {
margin-block: 0; margin-inline: ${token.marginXXS}px !important;
margin-block: 0 !important;
width: auto !important;
padding: 0 ${token.paddingSM}px 0 ${token.padding}px !important;
}
.ant-menu-item-group-title {
padding: 0 ${token.padding}px;
margin-inline: 0;
line-height: 32px;
}
.ant-menu-submenu-title {
margin: 0 ${token.marginXXS}px !important;
padding-left: ${token.padding}px !important;
width: auto !important;
} }
.ant-menu-root { .ant-menu-root {
margin: 0 -${token.margin}px; margin: 0 -${token.margin}px;

View File

@ -51,11 +51,6 @@ export const useSchemaInitializerStyles = genStyleHook('nb-schema-initializer',
}, },
}, },
}, },
[`${componentCls}-menu-sub`]: {
ul: {
maxHeight: '50vh !important',
},
},
[`${componentCls}-item-content`]: { [`${componentCls}-item-content`]: {
marginLeft: token.marginXS, marginLeft: token.marginXS,
}, },

View File

@ -12,10 +12,10 @@ import React, { FC, useMemo } from 'react';
import { useApp } from '../../hooks'; import { useApp } from '../../hooks';
import { SchemaInitializerItems } from '../components'; import { SchemaInitializerItems } from '../components';
import { SchemaInitializerButton } from '../components/SchemaInitializerButton'; import { SchemaInitializerButton } from '../components/SchemaInitializerButton';
import { withInitializer } from '../withInitializer';
import { SchemaInitializerOptions } from '../types';
import { SchemaInitializer } from '../SchemaInitializer'; import { SchemaInitializer } from '../SchemaInitializer';
import { SchemaInitializerOptions } from '../types';
import { withInitializer } from '../withInitializer';
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
const InitializerComponent: FC<SchemaInitializerOptions<any, any>> = React.memo((options) => { const InitializerComponent: FC<SchemaInitializerOptions<any, any>> = React.memo((options) => {
const Component: any = options.Component || SchemaInitializerButton; const Component: any = options.Component || SchemaInitializerButton;
@ -38,6 +38,18 @@ export function useSchemaInitializerRender<P1 = ButtonProps, P2 = {}>(
options?: Omit<SchemaInitializerOptions<P1, P2>, 'name'>, options?: Omit<SchemaInitializerOptions<P1, P2>, 'name'>,
) { ) {
const app = useApp(); const app = useApp();
const { isMobile } = useOpenModeContext() || {};
// compatible with mobile
// TODO: delete this code
if (
name === 'popup:common:addBlock' &&
app.schemaInitializerManager.has('mobile:popup:common:addBlock') &&
isMobile
) {
name = 'mobile:popup:common:addBlock';
}
const initializer = useMemo( const initializer = useMemo(
() => (typeof name === 'object' ? name : app.schemaInitializerManager.get<P1, P2>(name)), () => (typeof name === 'object' ? name : app.schemaInitializerManager.get<P1, P2>(name)),
[app.schemaInitializerManager, name], [app.schemaInitializerManager, name],

View File

@ -7,10 +7,10 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import type { DevDynamicImport } from '../Application';
import type { Plugin } from '../Plugin'; import type { Plugin } from '../Plugin';
import type { PluginData } from '../PluginManager'; import type { PluginData } from '../PluginManager';
import type { RequireJS } from './requirejs'; import type { RequireJS } from './requirejs';
import type { DevDynamicImport } from '../Application';
/** /**
* @internal * @internal
@ -131,7 +131,10 @@ export async function getPlugins(options: GetPluginsOption): Promise<Array<[stri
return res; return res;
} }
const remotePluginList = await getRemotePlugins(requirejs, remotePlugins); if (res.length === 0) {
res.push(...remotePluginList); const remotePluginList = await getRemotePlugins(requirejs, remotePlugins);
res.push(...remotePluginList);
}
return res; return res;
} }

View File

@ -11,6 +11,7 @@ import { Field, GeneralField } from '@formily/core';
import { RecursionField, useField, useFieldSchema } from '@formily/react'; import { RecursionField, useField, useFieldSchema } from '@formily/react';
import { Col, Row } from 'antd'; import { Col, Row } from 'antd';
import merge from 'deepmerge'; import merge from 'deepmerge';
import { isArray } from 'lodash';
import template from 'lodash/template'; import template from 'lodash/template';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -307,7 +308,15 @@ export const useFilterByTk = () => {
const association = getCollectionField(assoc); const association = getCollectionField(assoc);
return recordData?.[association.targetKey || 'id']; return recordData?.[association.targetKey || 'id'];
} }
return recordData?.[collection.filterTargetKey || 'id']; if (isArray(collection.filterTargetKey)) {
const filterByTk = {};
for (const key of collection.filterTargetKey) {
filterByTk[key] = recordData?.[key];
}
return filterByTk;
} else {
return recordData?.[collection.filterTargetKey || 'id'];
}
}; };
/** /**

View File

@ -141,7 +141,8 @@ export const useDetailsBlockProps = () => {
ctx.form ctx.form
.reset() .reset()
.then(() => { .then(() => {
ctx.form.setValues(data || {}); ctx.form.setInitialValues(data || {});
// Using `ctx.form.setValues(data || {});` here may cause an internal infinite loop in Formily
}) })
.catch(console.error); .catch(console.error);
} }

View File

@ -87,7 +87,7 @@ const InternalFormBlockProvider = (props) => {
updateAssociationValues, updateAssociationValues,
]); ]);
if (service.loading && Object.keys(form?.initialValues)?.length === 0 && action) { if (service.loading && Object.keys(form?.initialValues || {})?.length === 0 && action) {
return <Spin />; return <Spin />;
} }
@ -151,9 +151,10 @@ export const useFormBlockContext = () => {
/** /**
* @internal * @internal
*/ */
export const useFormBlockProps = (shouldClearInitialValues = false) => { export const useFormBlockProps = () => {
const ctx = useFormBlockContext(); const ctx = useFormBlockContext();
const treeParentRecord = useTreeParentRecord(); const treeParentRecord = useTreeParentRecord();
useEffect(() => { useEffect(() => {
if (treeParentRecord) { if (treeParentRecord) {
ctx.form?.query('parent').take((field) => { ctx.form?.query('parent').take((field) => {
@ -164,19 +165,14 @@ export const useFormBlockProps = (shouldClearInitialValues = false) => {
}); });
useEffect(() => { useEffect(() => {
if (!ctx?.service?.loading) { const form: Form = ctx.form;
const form: Form = ctx.form;
if (form) { if (!form || ctx.service?.loading) {
// form 字段中可能一开始就存在一些默认值(比如设置了字段默认值的模板区块)。在编辑表单中, return;
// 这些默认值是不需要的需要清除掉不然会导致一些问题。比如https://github.com/nocobase/nocobase/issues/4868
if (shouldClearInitialValues === true) {
form.initialValues = {};
form.reset();
}
form.setInitialValues(ctx.service?.data?.data);
}
} }
}, [ctx?.service?.loading]);
form.setInitialValues(ctx.service?.data?.data);
}, [ctx.form, ctx.service?.data?.data, ctx.service?.loading]);
return { return {
form: ctx.form, form: ctx.form,
}; };

View File

@ -12,7 +12,7 @@ import { FormContext, useField, useFieldSchema } from '@formily/react';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useCollectionManager_deprecated } from '../collection-manager'; import { useCollectionManager_deprecated } from '../collection-manager';
import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps'; import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps';
import { useTableBlockParams } from '../modules/blocks/data-blocks/table'; import { useTableBlockParams } from '../modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps';
import { FixedBlockWrapper, SchemaComponentOptions } from '../schema-component'; import { FixedBlockWrapper, SchemaComponentOptions } from '../schema-component';
import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { BlockProvider, useBlockRequestContext } from './BlockProvider';
import { useBlockHeightProps } from './hooks'; import { useBlockHeightProps } from './hooks';

View File

@ -183,9 +183,10 @@ export const useTableFieldProps = () => {
rowKey: (record: any) => { rowKey: (record: any) => {
return field.value?.indexOf?.(record); return field.value?.indexOf?.(record);
}, },
onRowSelectionChange(selectedRowKeys) { onRowSelectionChange(selectedRowKeys, selectedRowData) {
ctx.field.data = ctx?.field?.data || {}; ctx.field.data = ctx?.field?.data || {};
ctx.field.data.selectedRowKeys = selectedRowKeys; ctx.field.data.selectedRowKeys = selectedRowKeys;
ctx.field.data.selectedRowData = selectedRowData;
}, },
onChange({ current, pageSize }) { onChange({ current, pageSize }) {
ctx.service.run({ page: current, pageSize }); ctx.service.run({ page: current, pageSize });

View File

@ -319,9 +319,10 @@ export const useTableSelectorProps = () => {
dragSort: false, dragSort: false,
rowKey: ctx.rowKey || 'id', rowKey: ctx.rowKey || 'id',
pagination: fieldSchema?.['x-component-props']?.pagination === false ? false : field.componentProps.pagination, pagination: fieldSchema?.['x-component-props']?.pagination === false ? false : field.componentProps.pagination,
onRowSelectionChange(selectedRowKeys, selectedRows) { onRowSelectionChange(selectedRowKeys, selectedRowData) {
ctx.field.data = ctx?.field?.data || {}; ctx.field.data = ctx?.field?.data || {};
ctx.field.data.selectedRowKeys = selectedRowKeys; ctx.field.data.selectedRowKeys = selectedRowKeys;
ctx.field.data.selectedRowData = selectedRowData;
}, },
async onRowDragEnd({ from, to }) { async onRowDragEnd({ from, to }) {
await ctx.resource.move({ await ctx.resource.move({

View File

@ -24,7 +24,7 @@ describe('parseVariablesAndChangeParamsToQueryString', () => {
{ name: 'param3', value: 'value3' }, { name: 'param3', value: 'value3' },
]; ];
const variables: any = { const variables: any = {
parseVariable: vi.fn().mockResolvedValue('parsedValue'), parseVariable: vi.fn().mockResolvedValue({ value: 'parsedValue' }),
}; };
const localVariables: any = [ const localVariables: any = [
{ name: '$var1', ctx: { value: 'localValue1' } }, { name: '$var1', ctx: { value: 'localValue1' } },

View File

@ -28,6 +28,7 @@ import {
useCollectionRecord, useCollectionRecord,
useDataSourceHeaders, useDataSourceHeaders,
useFormActiveFields, useFormActiveFields,
useParsedFilter,
useRouterBasename, useRouterBasename,
useTableBlockContext, useTableBlockContext,
} from '../..'; } from '../..';
@ -41,7 +42,7 @@ import { useTreeParentRecord } from '../../modules/blocks/data-blocks/table/Tree
import { useRecord } from '../../record-provider'; import { useRecord } from '../../record-provider';
import { removeNullCondition, useActionContext, useCompile } from '../../schema-component'; import { removeNullCondition, useActionContext, useCompile } from '../../schema-component';
import { isSubMode } from '../../schema-component/antd/association-field/util'; import { isSubMode } from '../../schema-component/antd/association-field/util';
import { replaceVariables } from '../../schema-component/antd/form-v2/utils'; import { replaceVariables } from '../../schema-settings/LinkageRules/bindLinkageRulesToFiled';
import { useCurrentUserContext } from '../../user'; import { useCurrentUserContext } from '../../user';
import { useLocalVariables, useVariables } from '../../variables'; import { useLocalVariables, useVariables } from '../../variables';
import { VariableOption, VariablesContextType } from '../../variables/types'; import { VariableOption, VariablesContextType } from '../../variables/types';
@ -163,9 +164,9 @@ export function useCollectValuesToSubmit() {
} }
if (isVariable(value)) { if (isVariable(value)) {
const result = await variables?.parseVariable(value, localVariables); const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
if (result) { if (parsedValue) {
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField }); assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
} }
} else if (value != null && value !== '') { } else if (value != null && value !== '') {
assignedValues[key] = value; assignedValues[key] = value;
@ -223,7 +224,7 @@ export const useCreateActionProps = () => {
return { return {
async onClick() { async onClick() {
const { onSuccess, skipValidator, triggerWorkflows } = actionSchema?.['x-action-settings'] ?? {}; const { onSuccess, skipValidator, triggerWorkflows } = actionSchema?.['x-action-settings'] ?? {};
const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {};
if (!skipValidator) { if (!skipValidator) {
await form.submit(); await form.submit();
} }
@ -241,39 +242,49 @@ export const useCreateActionProps = () => {
: undefined, : undefined,
updateAssociationValues, updateAssociationValues,
}); });
setVisible?.(false); if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) {
setVisible?.(false);
}
setSubmitted?.(true); setSubmitted?.(true);
setFormValueChanged?.(false); setFormValueChanged?.(false);
actionField.data.loading = false; actionField.data.loading = false;
actionField.data.data = data; actionField.data.data = data;
// __parent?.service?.refresh?.(); // __parent?.service?.refresh?.();
if (!onSuccess?.successMessage) { if (!successMessage) {
message.success(t('Saved successfully')); message.success(t('Saved successfully'));
await resetFormCorrectly(form); await resetFormCorrectly(form);
if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) {
if (isURL(redirectTo)) {
window.location.href = redirectTo;
} else {
navigate(redirectTo);
}
}
return; return;
} }
if (onSuccess?.manualClose) { if (manualClose) {
modal.success({ modal.success({
title: compile(onSuccess?.successMessage), title: compile(successMessage),
onOk: async () => { onOk: async () => {
await resetFormCorrectly(form); await resetFormCorrectly(form);
if (onSuccess?.redirecting && onSuccess?.redirectTo) { if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) {
if (isURL(onSuccess.redirectTo)) { if (isURL(redirectTo)) {
window.location.href = onSuccess.redirectTo; window.location.href = redirectTo;
} else { } else {
navigate(onSuccess.redirectTo); navigate(redirectTo);
} }
} }
}, },
}); });
} else { } else {
message.success(compile(onSuccess?.successMessage)); message.success(compile(successMessage));
await resetFormCorrectly(form); await resetFormCorrectly(form);
if (onSuccess?.redirecting && onSuccess?.redirectTo) { if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) {
if (isURL(onSuccess.redirectTo)) { if (isURL(redirectTo)) {
window.location.href = onSuccess.redirectTo; window.location.href = redirectTo;
} else { } else {
navigate(onSuccess.redirectTo); navigate(redirectTo);
} }
} }
} }
@ -311,7 +322,7 @@ export const useAssociationCreateActionProps = () => {
triggerWorkflows, triggerWorkflows,
} = actionSchema?.['x-action-settings'] ?? {}; } = actionSchema?.['x-action-settings'] ?? {};
const addChild = fieldSchema?.['x-component-props']?.addChild; const addChild = fieldSchema?.['x-component-props']?.addChild;
const { successMessage } = onSuccess || {};
const assignedValues = {}; const assignedValues = {};
const waitList = Object.keys(originalAssignedValues).map(async (key) => { const waitList = Object.keys(originalAssignedValues).map(async (key) => {
const value = originalAssignedValues[key]; const value = originalAssignedValues[key];
@ -324,9 +335,9 @@ export const useAssociationCreateActionProps = () => {
} }
if (isVariable(value)) { if (isVariable(value)) {
const result = await variables?.parseVariable(value, localVariables); const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
if (result) { if (parsedValue) {
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField }); assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
} }
} else if (value != null && value !== '') { } else if (value != null && value !== '') {
assignedValues[key] = value; assignedValues[key] = value;
@ -371,10 +382,10 @@ export const useAssociationCreateActionProps = () => {
__parent?.service?.refresh?.(); __parent?.service?.refresh?.();
setVisible?.(false); setVisible?.(false);
setSubmitted?.(true); setSubmitted?.(true);
if (!onSuccess?.successMessage) { if (!successMessage) {
return; return;
} }
message.success(compile(onSuccess?.successMessage)); message.success(compile(successMessage));
} catch (error) { } catch (error) {
actionField.data.data = null; actionField.data.data = null;
actionField.data.loading = false; actionField.data.loading = false;
@ -560,6 +571,7 @@ export const useCustomizeUpdateActionProps = () => {
const variables = useVariables(); const variables = useVariables();
const localVariables = useLocalVariables({ currentForm: form }); const localVariables = useLocalVariables({ currentForm: form });
const { name, getField } = useCollection_deprecated(); const { name, getField } = useCollection_deprecated();
const { setVisible } = useActionContext();
return { return {
async onClick(e?, callBack?) { async onClick(e?, callBack?) {
@ -569,7 +581,7 @@ export const useCustomizeUpdateActionProps = () => {
skipValidator, skipValidator,
triggerWorkflows, triggerWorkflows,
} = actionSchema?.['x-action-settings'] ?? {}; } = actionSchema?.['x-action-settings'] ?? {};
const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {};
const assignedValues = {}; const assignedValues = {};
const waitList = Object.keys(originalAssignedValues).map(async (key) => { const waitList = Object.keys(originalAssignedValues).map(async (key) => {
const value = originalAssignedValues[key]; const value = originalAssignedValues[key];
@ -582,9 +594,9 @@ export const useCustomizeUpdateActionProps = () => {
} }
if (isVariable(value)) { if (isVariable(value)) {
const result = await variables?.parseVariable(value, localVariables); const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
if (result) { if (parsedValue) {
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField }); assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
} }
} else if (value != null && value !== '') { } else if (value != null && value !== '') {
assignedValues[key] = value; assignedValues[key] = value;
@ -603,6 +615,9 @@ export const useCustomizeUpdateActionProps = () => {
? triggerWorkflows.map((row) => [row.workflowKey, row.context].filter(Boolean).join('!')).join(',') ? triggerWorkflows.map((row) => [row.workflowKey, row.context].filter(Boolean).join('!')).join(',')
: undefined, : undefined,
}); });
if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) {
setVisible?.(false);
}
// service?.refresh?.(); // service?.refresh?.();
if (callBack) { if (callBack) {
callBack?.(); callBack?.();
@ -610,29 +625,29 @@ export const useCustomizeUpdateActionProps = () => {
if (!(resource instanceof TableFieldResource)) { if (!(resource instanceof TableFieldResource)) {
__parent?.service?.refresh?.(); __parent?.service?.refresh?.();
} }
if (!onSuccess?.successMessage) { if (!successMessage) {
return; return;
} }
if (onSuccess?.manualClose) { if (manualClose) {
modal.success({ modal.success({
title: compile(onSuccess?.successMessage), title: compile(successMessage),
onOk: async () => { onOk: async () => {
if (onSuccess?.redirecting && onSuccess?.redirectTo) { if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) {
if (isURL(onSuccess.redirectTo)) { if (isURL(redirectTo)) {
window.location.href = onSuccess.redirectTo; window.location.href = redirectTo;
} else { } else {
navigate(onSuccess.redirectTo); navigate(redirectTo);
} }
} }
}, },
}); });
} else { } else {
message.success(compile(onSuccess?.successMessage)); message.success(compile(successMessage));
if (onSuccess?.redirecting && onSuccess?.redirectTo) { if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) {
if (isURL(onSuccess.redirectTo)) { if (isURL(redirectTo)) {
window.location.href = onSuccess.redirectTo; window.location.href = redirectTo;
} else { } else {
navigate(onSuccess.redirectTo); navigate(redirectTo);
} }
} }
} }
@ -657,6 +672,7 @@ export const useCustomizeBulkUpdateActionProps = () => {
const record = useRecord(); const record = useRecord();
const { name, getField } = useCollection_deprecated(); const { name, getField } = useCollection_deprecated();
const localVariables = useLocalVariables(); const localVariables = useLocalVariables();
const { setVisible } = useActionContext();
return { return {
async onClick() { async onClick() {
@ -665,6 +681,7 @@ export const useCustomizeBulkUpdateActionProps = () => {
onSuccess, onSuccess,
updateMode, updateMode,
} = actionSchema?.['x-action-settings'] ?? {}; } = actionSchema?.['x-action-settings'] ?? {};
const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {};
actionField.data = field.data || {}; actionField.data = field.data || {};
actionField.data.loading = true; actionField.data.loading = true;
@ -680,16 +697,18 @@ export const useCustomizeBulkUpdateActionProps = () => {
} }
if (isVariable(value)) { if (isVariable(value)) {
const result = await variables?.parseVariable(value, localVariables); const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
if (result) { if (parsedValue) {
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField }); assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
} }
} else if (value != null && value !== '') { } else if (value != null && value !== '') {
assignedValues[key] = value; assignedValues[key] = value;
} }
}); });
await Promise.all(waitList); await Promise.all(waitList);
if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) {
setVisible?.(false);
}
modal.confirm({ modal.confirm({
title: t('Bulk update'), title: t('Bulk update'),
content: updateMode === 'selected' ? t('Update selected data?') : t('Update all data?'), content: updateMode === 'selected' ? t('Update selected data?') : t('Update all data?'),
@ -722,29 +741,29 @@ export const useCustomizeBulkUpdateActionProps = () => {
if (!(resource instanceof TableFieldResource)) { if (!(resource instanceof TableFieldResource)) {
__parent?.service?.refresh?.(); __parent?.service?.refresh?.();
} }
if (!onSuccess?.successMessage) { if (!successMessage) {
return; return;
} }
if (onSuccess?.manualClose) { if (manualClose) {
modal.success({ modal.success({
title: compile(onSuccess?.successMessage), title: compile(successMessage),
onOk: async () => { onOk: async () => {
if (onSuccess?.redirecting && onSuccess?.redirectTo) { if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) {
if (isURL(onSuccess.redirectTo)) { if (isURL(redirectTo)) {
window.location.href = onSuccess.redirectTo; window.location.href = redirectTo;
} else { } else {
navigate(onSuccess.redirectTo); navigate(redirectTo);
} }
} }
}, },
}); });
} else { } else {
message.success(compile(onSuccess?.successMessage)); message.success(compile(successMessage));
if (onSuccess?.redirecting && onSuccess?.redirectTo) { if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) {
if (isURL(onSuccess.redirectTo)) { if (isURL(redirectTo)) {
window.location.href = onSuccess.redirectTo; window.location.href = redirectTo;
} else { } else {
navigate(onSuccess.redirectTo); navigate(redirectTo);
} }
} }
} }
@ -770,6 +789,7 @@ export const useCustomizeRequestActionProps = () => {
const currentUserContext = useCurrentUserContext(); const currentUserContext = useCurrentUserContext();
const currentUser = currentUserContext?.data?.data; const currentUser = currentUserContext?.data?.data;
const actionField = useField(); const actionField = useField();
const { t } = useTranslation();
const { setVisible } = useActionContext(); const { setVisible } = useActionContext();
const { modal } = App.useApp(); const { modal } = App.useApp();
const { getActiveFieldsName } = useFormActiveFields() || {}; const { getActiveFieldsName } = useFormActiveFields() || {};
@ -777,6 +797,7 @@ export const useCustomizeRequestActionProps = () => {
return { return {
async onClick() { async onClick() {
const { skipValidator, onSuccess, requestSettings } = actionSchema?.['x-action-settings'] ?? {}; const { skipValidator, onSuccess, requestSettings } = actionSchema?.['x-action-settings'] ?? {};
const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {};
const xAction = actionSchema?.['x-action']; const xAction = actionSchema?.['x-action'];
if (!requestSettings['url']) { if (!requestSettings['url']) {
return; return;
@ -821,26 +842,36 @@ export const useCustomizeRequestActionProps = () => {
} }
service?.refresh?.(); service?.refresh?.();
if (xAction === 'customize:form:request') { if (xAction === 'customize:form:request') {
setVisible?.(false); if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) {
setVisible?.(false);
}
} }
if (!onSuccess?.successMessage) { if (!successMessage) {
return; message.success(t('Saved successfully'));
await resetFormCorrectly(form);
if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) {
if (isURL(redirectTo)) {
window.location.href = redirectTo;
} else {
navigate(redirectTo);
}
}
} }
if (onSuccess?.manualClose) { if (manualClose) {
modal.success({ modal.success({
title: compile(onSuccess?.successMessage), title: compile(successMessage),
onOk: async () => { onOk: async () => {
if (onSuccess?.redirecting && onSuccess?.redirectTo) { if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) {
if (isURL(onSuccess.redirectTo)) { if (isURL(redirectTo)) {
window.location.href = onSuccess.redirectTo; window.location.href = redirectTo;
} else { } else {
navigate(onSuccess.redirectTo); navigate(redirectTo);
} }
} }
}, },
}); });
} else { } else {
message.success(compile(onSuccess?.successMessage)); message.success(compile(successMessage));
} }
} finally { } finally {
actionField.data.loading = false; actionField.data.loading = false;
@ -853,7 +884,7 @@ export const useUpdateActionProps = () => {
const form = useForm(); const form = useForm();
const filterByTk = useFilterByTk(); const filterByTk = useFilterByTk();
const { field, resource, __parent } = useBlockRequestContext(); const { field, resource, __parent } = useBlockRequestContext();
const { setVisible, setSubmitted, setFormValueChanged } = useActionContext(); const { setVisible, setFormValueChanged } = useActionContext();
const actionSchema = useFieldSchema(); const actionSchema = useFieldSchema();
const navigate = useNavigateNoUpdate(); const navigate = useNavigateNoUpdate();
const { fields, getField, name } = useCollection_deprecated(); const { fields, getField, name } = useCollection_deprecated();
@ -867,7 +898,7 @@ export const useUpdateActionProps = () => {
const { getActiveFieldsName } = useFormActiveFields() || {}; const { getActiveFieldsName } = useFormActiveFields() || {};
return { return {
async onClick() { async onClick(e?, callBack?) {
const { const {
assignedValues: originalAssignedValues = {}, assignedValues: originalAssignedValues = {},
onSuccess, onSuccess,
@ -875,7 +906,7 @@ export const useUpdateActionProps = () => {
skipValidator, skipValidator,
triggerWorkflows, triggerWorkflows,
} = actionSchema?.['x-action-settings'] ?? {}; } = actionSchema?.['x-action-settings'] ?? {};
const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {};
const assignedValues = {}; const assignedValues = {};
const waitList = Object.keys(originalAssignedValues).map(async (key) => { const waitList = Object.keys(originalAssignedValues).map(async (key) => {
const value = originalAssignedValues[key]; const value = originalAssignedValues[key];
@ -888,9 +919,9 @@ export const useUpdateActionProps = () => {
} }
if (isVariable(value)) { if (isVariable(value)) {
const result = await variables?.parseVariable(value, localVariables); const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
if (result) { if (parsedValue) {
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField }); assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
} }
} else if (value != null && value !== '') { } else if (value != null && value !== '') {
assignedValues[key] = value; assignedValues[key] = value;
@ -930,33 +961,42 @@ export const useUpdateActionProps = () => {
}); });
actionField.data.loading = false; actionField.data.loading = false;
// __parent?.service?.refresh?.(); // __parent?.service?.refresh?.();
setVisible?.(false); if (callBack) {
setSubmitted?.(true); callBack?.();
}
if (actionAfterSuccess === 'previous' || (!actionAfterSuccess && redirecting !== true)) {
setVisible?.(false);
}
setFormValueChanged?.(false); setFormValueChanged?.(false);
if (!onSuccess?.successMessage) { if (!successMessage) {
return; return;
} }
if (onSuccess?.manualClose) { if (manualClose) {
modal.success({ modal.success({
title: compile(onSuccess?.successMessage), title: compile(successMessage),
onOk: async () => { onOk: async () => {
await form.reset(); await form.reset();
if (onSuccess?.redirecting && onSuccess?.redirectTo) { if (((redirecting && !actionAfterSuccess) || actionAfterSuccess === 'redirect') && redirectTo) {
if (isURL(onSuccess.redirectTo)) { if (isURL(redirectTo)) {
window.location.href = onSuccess.redirectTo; window.location.href = redirectTo;
} else { } else {
navigate(onSuccess.redirectTo); navigate(redirectTo);
} }
} }
}, },
}); });
} else { } else {
message.success(compile(onSuccess?.successMessage)); message.success(compile(successMessage));
if (onSuccess?.redirecting && onSuccess?.redirectTo) { if (
if (isURL(onSuccess.redirectTo)) { ((redirecting && !actionAfterSuccess) ||
window.location.href = onSuccess.redirectTo; actionAfterSuccess === 'redirect' ||
actionAfterSuccess === 'redirect') &&
redirectTo
) {
if (isURL(redirectTo)) {
window.location.href = redirectTo;
} else { } else {
navigate(onSuccess.redirectTo); navigate(redirectTo);
} }
} }
} }
@ -1077,13 +1117,25 @@ export const useBulkDestroyActionProps = () => {
const { field } = useBlockRequestContext(); const { field } = useBlockRequestContext();
const { resource, service } = useBlockRequestContext(); const { resource, service } = useBlockRequestContext();
const { setSubmitted } = useActionContext(); const { setSubmitted } = useActionContext();
const collection = useCollection_deprecated();
const { filterTargetKey } = collection;
return { return {
async onClick(e?, callBack?) { async onClick(e?, callBack?) {
let filterByTk = field.data?.selectedRowKeys;
if (Array.isArray(filterTargetKey)) {
filterByTk = field.data.selectedRowData.map((v) => {
const obj = {};
filterTargetKey.map((j) => {
obj[j] = v[j];
});
return obj;
});
}
if (!field?.data?.selectedRowKeys?.length) { if (!field?.data?.selectedRowKeys?.length) {
return; return;
} }
await resource.destroy({ await resource.destroy({
filterByTk: field.data?.selectedRowKeys, filterByTk,
}); });
field.data.selectedRowKeys = []; field.data.selectedRowKeys = [];
const currentPage = service.params[0]?.page; const currentPage = service.params[0]?.page;
@ -1095,7 +1147,7 @@ export const useBulkDestroyActionProps = () => {
callBack?.(); callBack?.();
} }
setSubmitted?.(true); setSubmitted?.(true);
// service?.refresh?.(); service?.refresh?.();
}, },
}; };
}; };
@ -1245,6 +1297,7 @@ export const useAssociationFilterBlockProps = () => {
const { props: blockProps } = useBlockRequestContext(); const { props: blockProps } = useBlockRequestContext();
const headers = useDataSourceHeaders(blockProps?.dataSource); const headers = useDataSourceHeaders(blockProps?.dataSource);
const cm = useCollectionManager_deprecated(); const cm = useCollectionManager_deprecated();
const { filter, parseVariableLoading } = useParsedFilter({ filterOption: field.componentProps?.params?.filter });
let list, handleSearchInput, params, run, data, valueKey, labelKey, filterKey; let list, handleSearchInput, params, run, data, valueKey, labelKey, filterKey;
@ -1264,6 +1317,7 @@ export const useAssociationFilterBlockProps = () => {
pageSize: 200, pageSize: 200,
page: 1, page: 1,
...field.componentProps?.params, ...field.componentProps?.params,
filter,
}, },
}, },
{ {
@ -1275,12 +1329,64 @@ export const useAssociationFilterBlockProps = () => {
useEffect(() => { useEffect(() => {
// 由于选项字段不需要触发当前请求,所以请求单独在关系字段的时候触发 // 由于选项字段不需要触发当前请求,所以请求单独在关系字段的时候触发
if (!isOptionalField(collectionField)) { if (!isOptionalField(collectionField) && parseVariableLoading === false) {
run(); run();
} }
// do not format the dependencies // do not format the dependencies
}, [collectionField, labelKey, run, valueKey, field.componentProps?.params, field.componentProps?.params?.sort]); }, [
collectionField,
labelKey,
run,
valueKey,
field.componentProps?.params,
field.componentProps?.params?.sort,
parseVariableLoading,
]);
const onSelected = useCallback(
(value) => {
const { targets, uid } = findFilterTargets(fieldSchema);
getDataBlocks().forEach((block) => {
const target = targets.find((target) => target.uid === block.uid);
if (!target) return;
const key = `${uid}${fieldSchema.name}`;
const param = block.service.params?.[0] || {};
if (!block.service.params?.[1]?.filters) {
_.set(block.service.params, '[1].filters', {});
}
// 保留原有的 filter
const storedFilter = block.service.params[1].filters;
if (value.length) {
storedFilter[key] = {
[filterKey]: value,
};
} else {
if (block.dataLoadingMode === 'manual') {
return block.clearData();
}
delete storedFilter[key];
}
const mergedFilter = mergeFilter([...Object.values(storedFilter), block.defaultFilter]);
return block.doFilter(
{
...param,
page: 1,
filter: mergedFilter,
},
{ filters: storedFilter },
);
});
},
[fieldSchema, filterKey, getDataBlocks],
);
if (!collectionField) { if (!collectionField) {
return {}; return {};
@ -1323,41 +1429,6 @@ export const useAssociationFilterBlockProps = () => {
}; };
} }
const onSelected = (value) => {
const { targets, uid } = findFilterTargets(fieldSchema);
getDataBlocks().forEach((block) => {
const target = targets.find((target) => target.uid === block.uid);
if (!target) return;
const key = `${uid}${fieldSchema.name}`;
const param = block.service.params?.[0] || {};
// 保留原有的 filter
const storedFilter = block.service.params?.[1]?.filters || {};
if (value.length) {
storedFilter[key] = {
[filterKey]: value,
};
} else {
if (block.dataLoadingMode === 'manual') {
return block.clearData();
}
delete storedFilter[key];
}
const mergedFilter = mergeFilter([...Object.values(storedFilter), block.defaultFilter]);
return block.doFilter(
{
...param,
page: 1,
filter: mergedFilter,
},
{ filters: storedFilter },
);
});
};
return { return {
/** 渲染 Collapse 的列表数据 */ /** 渲染 Collapse 的列表数据 */
list, list,
@ -1438,7 +1509,8 @@ export const useAssociationNames = (dataSource?: string) => {
['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'belongsToArray'].includes(collectionField.type); ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'belongsToArray'].includes(collectionField.type);
// 根据联动规则中条件的字段获取一些 appends // 根据联动规则中条件的字段获取一些 appends
if (s['x-linkage-rules']) { // 需要排除掉子表格和子表单中的联动规则
if (s['x-linkage-rules'] && !isSubMode(s)) {
const collectAppends = (obj) => { const collectAppends = (obj) => {
const type = Object.keys(obj)[0] || '$and'; const type = Object.keys(obj)[0] || '$and';
const list = obj[type]; const list = obj[type];
@ -1661,8 +1733,8 @@ export async function parseVariablesAndChangeParamsToQueryString({
searchParams.map(async ({ name, value }) => { searchParams.map(async ({ name, value }) => {
if (typeof value === 'string') { if (typeof value === 'string') {
if (isVariable(value)) { if (isVariable(value)) {
const result = await variables.parseVariable(value, localVariables); const { value: parsedValue } = (await variables.parseVariable(value, localVariables)) || {};
return { name, value: result }; return { name, value: parsedValue };
} }
const result = await replaceVariableValue(value, variables, localVariables); const result = await replaceVariableValue(value, variables, localVariables);
return { name, value: result }; return { name, value: result };

View File

@ -16,64 +16,23 @@ import { cloneDeep } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRequest } from '../../api-client'; import { useRequest } from '../../api-client';
import { CollectionFieldInterface } from '../../data-source';
import { RecordProvider, useRecord } from '../../record-provider'; import { RecordProvider, useRecord } from '../../record-provider';
import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import { useCancelAction } from '../action-hooks'; import { useCancelAction } from '../action-hooks';
import { useCollectionManager_deprecated } from '../hooks'; import { useCollectionManager_deprecated } from '../hooks';
import useDialect from '../hooks/useDialect'; import useDialect from '../hooks/useDialect';
import { IField } from '../interfaces/types';
import * as components from './components'; import * as components from './components';
import { useFieldInterfaceOptions } from './interfaces'; import { useFieldInterfaceOptions } from './interfaces';
const getSchema = (schema: IField, record: any, compile) => { const getSchema = (schema: CollectionFieldInterface, record: any, compile) => {
if (!schema) { if (!schema) {
return; return;
} }
const properties = cloneDeep(schema.properties) as any; const properties = schema.getConfigureFormProperties();
if (schema.hasDefaultValue === true) {
properties['defaultValue'] = cloneDeep(schema?.default?.uiSchema);
properties.defaultValue.required = false;
properties['defaultValue']['title'] = compile('{{ t("Default value") }}');
properties['defaultValue']['x-decorator'] = 'FormItem';
properties['defaultValue']['x-reactions'] = [
{
dependencies: [
'uiSchema.x-component-props.gmt',
'uiSchema.x-component-props.showTime',
'uiSchema.x-component-props.dateFormat',
'uiSchema.x-component-props.timeFormat',
],
fulfill: {
state: {
componentProps: {
gmt: '{{$deps[0]}}',
showTime: '{{$deps[1]}}',
dateFormat: '{{$deps[2]}}',
timeFormat: '{{$deps[3]}}',
},
},
},
},
{
dependencies: ['primaryKey', 'unique', 'autoIncrement'],
when: '{{$deps[0]||$deps[1]||$deps[2]}}',
fulfill: {
state: {
hidden: true,
value: null,
},
},
otherwise: {
state: {
hidden: false,
},
},
},
];
}
const initialValue: any = { const initialValue: any = {
name: `f_${uid()}`, name: `f_${uid()}`,
...cloneDeep(schema.default), ...cloneDeep(schema.default),

View File

@ -64,6 +64,9 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema
description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`, description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Select', 'x-component': 'Select',
'x-component-props': {
multiple: true,
},
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'], 'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],
}, },
footer: { footer: {

View File

@ -16,6 +16,7 @@ import set from 'lodash/set';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAPIClient, useRequest } from '../../api-client'; import { useAPIClient, useRequest } from '../../api-client';
import { CollectionFieldInterface } from '../../data-source';
import { useCollectionParentRecordData } from '../../data-source/collection-record/CollectionRecordProvider'; import { useCollectionParentRecordData } from '../../data-source/collection-record/CollectionRecordProvider';
import { RecordProvider, useRecord } from '../../record-provider'; import { RecordProvider, useRecord } from '../../record-provider';
import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
@ -23,60 +24,23 @@ import { useResourceActionContext, useResourceContext } from '../ResourceActionP
import { useCancelAction } from '../action-hooks'; import { useCancelAction } from '../action-hooks';
import { useCollectionManager_deprecated } from '../hooks'; import { useCollectionManager_deprecated } from '../hooks';
import useDialect from '../hooks/useDialect'; import useDialect from '../hooks/useDialect';
import { IField } from '../interfaces/types';
import * as components from './components'; import * as components from './components';
const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => { const getSchema = (
schema: CollectionFieldInterface,
defaultValues: any,
record: any,
compile,
getContainer,
): ISchema => {
if (!schema) { if (!schema) {
return; return;
} }
const properties = cloneDeep(schema.properties) as any; const properties = schema.getConfigureFormProperties();
if (properties?.name) { if (properties?.name) {
properties.name['x-disabled'] = true; properties.name['x-disabled'] = true;
} }
if (schema.hasDefaultValue === true) {
properties['defaultValue'] = cloneDeep(schema.default.uiSchema) || {};
properties.defaultValue.required = false;
properties['defaultValue']['title'] = compile('{{ t("Default value") }}');
properties['defaultValue']['x-decorator'] = 'FormItem';
properties['defaultValue']['x-reactions'] = [
{
dependencies: [
'uiSchema.x-component-props.gmt',
'uiSchema.x-component-props.showTime',
'uiSchema.x-component-props.dateFormat',
'uiSchema.x-component-props.timeFormat',
],
fulfill: {
state: {
componentProps: {
gmt: '{{$deps[0]}}',
showTime: '{{$deps[1]}}',
dateFormat: '{{$deps[2]}}',
timeFormat: '{{$deps[3]}}',
},
},
},
},
{
dependencies: ['primaryKey', 'unique', 'autoIncrement'],
when: '{{$deps[0]||$deps[1]||$deps[2]}}',
fulfill: {
state: {
hidden: true,
value: undefined,
},
},
otherwise: {
state: {
hidden: false,
},
},
},
];
}
return { return {
type: 'object', type: 'object',
properties: { properties: {
@ -92,7 +56,7 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema
return useRequest( return useRequest(
() => () =>
Promise.resolve({ Promise.resolve({
data: cloneDeep(omit(schema.default, ['uiSchema.rawTitle'])), data: cloneDeep(omit(defaultValues, ['uiSchema.rawTitle'])),
}), }),
options, options,
); );
@ -230,15 +194,7 @@ export const EditFieldAction = (props) => {
set(defaultValues.reverseField, 'name', `f_${uid()}`); set(defaultValues.reverseField, 'name', `f_${uid()}`);
set(defaultValues.reverseField, 'uiSchema.title', record.__parent?.title); set(defaultValues.reverseField, 'uiSchema.title', record.__parent?.title);
} }
const schema = getSchema( const schema = getSchema(interfaceConf, defaultValues, record, compile, getContainer);
{
...interfaceConf,
default: defaultValues,
},
record,
compile,
getContainer,
);
setSchema(schema); setSchema(schema);
setVisible(true); setVisible(true);
}} }}

View File

@ -136,6 +136,6 @@ export const useResourceContext = () => {
resource, resource,
collection, collection,
association, association,
targetKey: association?.targetKey || collection?.targetKey || 'id', targetKey: association?.targetKey || collection?.filterTargetKey || collection?.targetKey || 'id',
}; };
}; };

View File

@ -51,6 +51,8 @@ import {
UUIDFieldInterface, UUIDFieldInterface,
NanoidFieldInterface, NanoidFieldInterface,
UnixTimestampFieldInterface, UnixTimestampFieldInterface,
DateFieldInterface,
DatetimeNoTzFieldInterface,
} from './interfaces'; } from './interfaces';
import { import {
GeneralCollectionTemplate, GeneralCollectionTemplate,
@ -171,6 +173,8 @@ export class CollectionPlugin extends Plugin {
UUIDFieldInterface, UUIDFieldInterface,
NanoidFieldInterface, NanoidFieldInterface,
UnixTimestampFieldInterface, UnixTimestampFieldInterface,
DateFieldInterface,
DatetimeNoTzFieldInterface,
]); ]);
} }

View File

@ -28,7 +28,7 @@ export class CreatedAtFieldInterface extends CollectionFieldInterface {
'x-read-pretty': true, 'x-read-pretty': true,
}, },
}; };
availableTypes = ['date']; availableTypes = [];
properties = { properties = {
...defaultProps, ...defaultProps,
...dateTimeProps, ...dateTimeProps,

View File

@ -0,0 +1,46 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { dateTimeProps, defaultProps, operators } from './properties';
export class DateFieldInterface extends CollectionFieldInterface {
name = 'date';
type = 'object';
group = 'datetime';
order = 3;
title = '{{t("DateOnly")}}';
sortable = true;
default = {
type: 'dateOnly',
uiSchema: {
type: 'string',
'x-component': 'DatePicker',
'x-component-props': {
dateOnly: true,
},
},
};
availableTypes = ['dateOnly'];
hasDefaultValue = true;
properties = {
...defaultProps,
...dateTimeProps,
'uiSchema.x-component-props.showTime': {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-visible': false,
},
};
filterable = {
operators: operators.datetime,
};
titleUsable = true;
}

View File

@ -15,23 +15,39 @@ export class DatetimeFieldInterface extends CollectionFieldInterface {
type = 'object'; type = 'object';
group = 'datetime'; group = 'datetime';
order = 1; order = 1;
title = '{{t("Datetime")}}'; title = '{{t("Datetime (with time zone)")}}';
sortable = true; sortable = true;
default = { default = {
type: 'date', type: 'date',
defaultToCurrentTime: false,
onUpdateToCurrentTime: false,
timezone: true,
uiSchema: { uiSchema: {
type: 'string', type: 'string',
'x-component': 'DatePicker', 'x-component': 'DatePicker',
'x-component-props': { 'x-component-props': {
showTime: false, showTime: false,
utc: true,
}, },
}, },
}; };
availableTypes = ['date', 'dateOnly', 'string']; availableTypes = ['date', 'string', 'datetime', 'datetimeTz'];
hasDefaultValue = true; hasDefaultValue = true;
properties = { properties = {
...defaultProps, ...defaultProps,
...dateTimeProps, ...dateTimeProps,
defaultToCurrentTime: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Default value to current time")}}',
},
onUpdateToCurrentTime: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Automatically update timestamp on update")}}',
},
'uiSchema.x-component-props.gmt': { 'uiSchema.x-component-props.gmt': {
type: 'boolean', type: 'boolean',
title: '{{t("GMT")}}', title: '{{t("GMT")}}',

View File

@ -0,0 +1,65 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { dateTimeProps, defaultProps, operators } from './properties';
export class DatetimeNoTzFieldInterface extends CollectionFieldInterface {
name = 'datetimeNoTz';
type = 'object';
group = 'datetime';
order = 2;
title = '{{t("Datetime (without time zone)")}}';
sortable = true;
default = {
type: 'datetimeNoTz',
defaultToCurrentTime: false,
onUpdateToCurrentTime: false,
timezone: false,
uiSchema: {
type: 'string',
'x-component': 'DatePicker',
'x-component-props': {
showTime: false,
utc: false,
},
},
};
availableTypes = ['string', 'datetimeNoTz'];
hasDefaultValue = true;
properties = {
...defaultProps,
...dateTimeProps,
defaultToCurrentTime: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Default value to current server time")}}',
},
onUpdateToCurrentTime: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Automatically update timestamp to the current server time on update")}}',
},
'uiSchema.x-component-props.gmt': {
type: 'boolean',
title: '{{t("GMT")}}',
'x-hidden': true,
'x-component': 'Checkbox',
'x-content': '{{t("Use the same time zone (GMT) for all users")}}',
'x-decorator': 'FormItem',
default: false,
},
};
filterable = {
operators: operators.datetime,
};
titleUsable = true;
}

View File

@ -45,3 +45,5 @@ export * from './url';
export * from './uuid'; export * from './uuid';
export * from './nanoid'; export * from './nanoid';
export * from './unixTimestamp'; export * from './unixTimestamp';
export * from './dateOnly';
export * from './datetimeNoTz';

View File

@ -10,7 +10,7 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { getUniqueKeyFromCollection } from './o2m'; import { getUniqueKeyFromCollection } from './utils';
import { defaultProps, relationshipType, reverseFieldProperties } from './properties'; import { defaultProps, relationshipType, reverseFieldProperties } from './properties';
export class M2MFieldInterface extends CollectionFieldInterface { export class M2MFieldInterface extends CollectionFieldInterface {

View File

@ -9,7 +9,7 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { getUniqueKeyFromCollection } from './o2m'; import { getUniqueKeyFromCollection } from './utils';
import { constraintsProps, relationshipType, reverseFieldProperties } from './properties'; import { constraintsProps, relationshipType, reverseFieldProperties } from './properties';
export class M2OFieldInterface extends CollectionFieldInterface { export class M2OFieldInterface extends CollectionFieldInterface {

View File

@ -8,10 +8,9 @@
*/ */
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { Collection } from '../../data-source';
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { constraintsProps, relationshipType, reverseFieldProperties } from './properties'; import { constraintsProps, relationshipType, reverseFieldProperties } from './properties';
import { getUniqueKeyFromCollection } from './utils';
export class O2MFieldInterface extends CollectionFieldInterface { export class O2MFieldInterface extends CollectionFieldInterface {
name = 'o2m'; name = 'o2m';
type = 'object'; type = 'object';
@ -215,7 +214,3 @@ export class O2MFieldInterface extends CollectionFieldInterface {
], ],
}; };
} }
export function getUniqueKeyFromCollection(collection: Collection) {
return collection?.filterTargetKey || collection?.getPrimaryKey() || 'id';
}

View File

@ -9,7 +9,7 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { getUniqueKeyFromCollection } from './o2m'; import { getUniqueKeyFromCollection } from './utils';
import { constraintsProps, relationshipType, reverseFieldProperties } from './properties'; import { constraintsProps, relationshipType, reverseFieldProperties } from './properties';
export class O2OFieldInterface extends CollectionFieldInterface { export class O2OFieldInterface extends CollectionFieldInterface {

View File

@ -10,6 +10,8 @@
import { Field } from '@formily/core'; import { Field } from '@formily/core';
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { css } from '@emotion/css';
import { DateFormatCom } from '../../../schema-component/antd/expiresRadio';
export * as operators from './operators'; export * as operators from './operators';
export const type: ISchema = { export const type: ISchema = {
@ -225,26 +227,88 @@ export const reverseFieldProperties: Record<string, ISchema> = {
}; };
export const dateTimeProps: { [key: string]: ISchema } = { export const dateTimeProps: { [key: string]: ISchema } = {
'uiSchema.x-component-props.picker': {
type: 'string',
title: '{{t("Picker")}}',
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
default: 'date',
enum: [
{
label: '{{t("Date")}}',
value: 'date',
},
// {
// label: '{{t("Week")}}',
// value: 'week',
// },
{
label: '{{t("Month")}}',
value: 'month',
},
{
label: '{{t("Quarter")}}',
value: 'quarter',
},
{
label: '{{t("Year")}}',
value: 'year',
},
],
},
'uiSchema.x-component-props.dateFormat': { 'uiSchema.x-component-props.dateFormat': {
type: 'string', type: 'string',
title: '{{t("Date format")}}', title: '{{t("Date format")}}',
'x-component': 'Radio.Group',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'ExpiresRadio',
'x-decorator-props': {},
'x-component-props': {
className: css`
.ant-radio-wrapper {
display: flex;
margin: 5px 0px;
}
`,
defaultValue: 'dddd',
formats: ['MMMM Do YYYY', 'YYYY-MM-DD', 'MM/DD/YY', 'YYYY/MM/DD', 'DD/MM/YYYY'],
},
default: 'YYYY-MM-DD', default: 'YYYY-MM-DD',
enum: [ enum: [
{ {
label: '{{t("Year/Month/Day")}}', label: DateFormatCom({ format: 'MMMM Do YYYY' }),
value: 'YYYY/MM/DD', value: 'MMMM Do YYYY',
}, },
{ {
label: '{{t("Year-Month-Day")}}', label: DateFormatCom({ format: 'YYYY-MM-DD' }),
value: 'YYYY-MM-DD', value: 'YYYY-MM-DD',
}, },
{ {
label: '{{t("Day/Month/Year")}}', label: DateFormatCom({ format: 'MM/DD/YY' }),
value: 'MM/DD/YY',
},
{
label: DateFormatCom({ format: 'YYYY/MM/DD' }),
value: 'YYYY/MM/DD',
},
{
label: DateFormatCom({ format: 'DD/MM/YYYY' }),
value: 'DD/MM/YYYY', value: 'DD/MM/YYYY',
}, },
{
label: 'custom',
value: 'custom',
},
], ],
'x-reactions': {
dependencies: ['uiSchema.x-component-props.picker'],
fulfill: {
state: {
value: `{{ getPickerFormat($deps[0])}}`,
componentProps: { picker: `{{$deps[0]}}` },
},
},
},
}, },
'uiSchema.x-component-props.showTime': { 'uiSchema.x-component-props.showTime': {
type: 'boolean', type: 'boolean',
@ -253,10 +317,26 @@ export const dateTimeProps: { [key: string]: ISchema } = {
'x-content': '{{t("Show time")}}', 'x-content': '{{t("Show time")}}',
'x-reactions': [ 'x-reactions': [
`{{(field) => { `{{(field) => {
field.query('..[].timeFormat').take(f => { field.query('..[].timeFormat').take(f => {
f.display = field.value ? 'visible' : 'none'; f.display = field.value ? 'visible' : 'none';
}); f.value='HH:mm:ss'
}}}`, });
}}}`,
{
dependencies: ['uiSchema.x-component-props.picker'],
when: '{{$deps[0]==="date"}}',
fulfill: {
state: {
hidden: false,
},
},
otherwise: {
state: {
hidden: true,
value: false,
},
},
},
], ],
}, },
'uiSchema.x-component-props.timeFormat': { 'uiSchema.x-component-props.timeFormat': {
@ -275,6 +355,14 @@ export const dateTimeProps: { [key: string]: ISchema } = {
value: 'HH:mm:ss', value: 'HH:mm:ss',
}, },
], ],
'x-reactions': {
dependencies: ['uiSchema.x-component-props.showTime'],
fulfill: {
state: {
hidden: `{{ !$deps[0] }}`,
},
},
},
}, },
}; };

View File

@ -60,13 +60,48 @@ export const object = [
]; ];
export const datetime = [ export const datetime = [
{ label: "{{ t('is') }}", value: '$dateOn', selected: true }, {
{ label: "{{ t('is not') }}", value: '$dateNotOn' }, label: "{{ t('is') }}",
{ label: "{{ t('is before') }}", value: '$dateBefore' }, value: '$dateOn',
{ label: "{{ t('is after') }}", value: '$dateAfter' }, selected: true,
{ label: "{{ t('is on or after') }}", value: '$dateNotBefore' }, schema: { 'x-component': 'DatePicker.FilterWithPicker' },
{ label: "{{ t('is on or before') }}", value: '$dateNotAfter' }, onlyFilterAction: true, //schema 仅在Filter.Action生效筛选表单中不生效
{ label: "{{ t('is between') }}", value: '$dateBetween', schema: { 'x-component': 'DatePicker.RangePicker' } }, },
{
label: "{{ t('is not') }}",
value: '$dateNotOn',
schema: { 'x-component': 'DatePicker.FilterWithPicker' },
onlyFilterAction: true,
},
{
label: "{{ t('is before') }}",
value: '$dateBefore',
schema: { 'x-component': 'DatePicker.FilterWithPicker' },
onlyFilterAction: true,
},
{
label: "{{ t('is after') }}",
value: '$dateAfter',
schema: { 'x-component': 'DatePicker.FilterWithPicker' },
onlyFilterAction: true,
},
{
label: "{{ t('is on or after') }}",
value: '$dateNotBefore',
schema: { 'x-component': 'DatePicker.FilterWithPicker' },
onlyFilterAction: true,
},
{
label: "{{ t('is on or before') }}",
value: '$dateNotAfter',
schema: { 'x-component': 'DatePicker.FilterWithPicker' },
onlyFilterAction: true,
},
{
label: "{{ t('is between') }}",
value: '$dateBetween',
schema: { 'x-component': 'DatePicker.RangePicker' },
},
{ label: "{{ t('is empty') }}", value: '$empty', noValue: true }, { label: "{{ t('is empty') }}", value: '$empty', noValue: true },
{ label: "{{ t('is not empty') }}", value: '$notEmpty', noValue: true }, { label: "{{ t('is not empty') }}", value: '$notEmpty', noValue: true },
]; ];
@ -157,18 +192,18 @@ export const collection = [
label: '{{t("is")}}', label: '{{t("is")}}',
value: '$eq', value: '$eq',
selected: true, selected: true,
schema: { 'x-component': 'CollectionSelect' }, schema: { 'x-component': 'DataSourceCollectionCascader' },
}, },
{ {
label: '{{t("is not")}}', label: '{{t("is not")}}',
value: '$ne', value: '$ne',
schema: { 'x-component': 'CollectionSelect' }, schema: { 'x-component': 'DataSourceCollectionCascader' },
}, },
{ {
label: '{{t("is any of")}}', label: '{{t("is any of")}}',
value: '$in', value: '$in',
schema: { schema: {
'x-component': 'CollectionSelect', 'x-component': 'DataSourceCollectionCascader',
'x-component-props': { mode: 'tags' }, 'x-component-props': { mode: 'tags' },
}, },
}, },
@ -176,7 +211,7 @@ export const collection = [
label: '{{t("is none of")}}', label: '{{t("is none of")}}',
value: '$notIn', value: '$notIn',
schema: { schema: {
'x-component': 'CollectionSelect', 'x-component': 'DataSourceCollectionCascader',
'x-component-props': { mode: 'tags' }, 'x-component-props': { mode: 'tags' },
}, },
}, },

View File

@ -14,7 +14,7 @@ export class TimeFieldInterface extends CollectionFieldInterface {
name = 'time'; name = 'time';
type = 'object'; type = 'object';
group = 'datetime'; group = 'datetime';
order = 2; order = 4;
title = '{{t("Time")}}'; title = '{{t("Time")}}';
sortable = true; sortable = true;
default = { default = {

View File

@ -8,31 +8,34 @@
*/ */
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { dateTimeProps, defaultProps, operators } from './properties'; import { defaultProps, operators, dateTimeProps } from './properties';
export class UnixTimestampFieldInterface extends CollectionFieldInterface { export class UnixTimestampFieldInterface extends CollectionFieldInterface {
name = 'unixTimestamp'; name = 'unixTimestamp';
type = 'object'; type = 'object';
group = 'datetime'; group = 'datetime';
order = 1; order = 4;
title = '{{t("Unix Timestamp")}}'; title = '{{t("Unix Timestamp")}}';
sortable = true; sortable = true;
default = { default = {
type: 'bigInt', type: 'unixTimestamp',
accuracy: 'second',
timezone: true,
defaultToCurrentTime: false,
onUpdateToCurrentTime: false,
uiSchema: { uiSchema: {
type: 'number', type: 'number',
'x-component': 'UnixTimestamp', 'x-component': 'UnixTimestamp',
'x-component-props': { 'x-component-props': {
accuracy: 'second',
showTime: true, showTime: true,
}, },
}, },
}; };
availableTypes = ['integer', 'bigInt']; availableTypes = ['unixTimestamp'];
hasDefaultValue = true; hasDefaultValue = false;
properties = { properties = {
...defaultProps, ...defaultProps,
'uiSchema.x-component-props.accuracy': { ...dateTimeProps,
accuracy: {
type: 'string', type: 'string',
title: '{{t("Accuracy")}}', title: '{{t("Accuracy")}}',
'x-component': 'Radio.Group', 'x-component': 'Radio.Group',
@ -43,9 +46,21 @@ export class UnixTimestampFieldInterface extends CollectionFieldInterface {
{ value: 'second', label: '{{t("Second")}}' }, { value: 'second', label: '{{t("Second")}}' },
], ],
}, },
defaultToCurrentTime: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Default value to current time")}}',
},
onUpdateToCurrentTime: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Automatically update timestamp on update")}}',
},
}; };
filterable = { filterable = {
operators: operators.number, operators: operators.datetime,
}; };
titleUsable = true; titleUsable = true;
} }

View File

@ -28,7 +28,7 @@ export class UpdatedAtFieldInterface extends CollectionFieldInterface {
'x-read-pretty': true, 'x-read-pretty': true,
}, },
}; };
availableTypes = ['date']; availableTypes = [];
properties = { properties = {
...defaultProps, ...defaultProps,
...dateTimeProps, ...dateTimeProps,

View File

@ -0,0 +1,20 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Collection } from '../../../data-source';
export function getUniqueKeyFromCollection(collection: Collection) {
if (collection?.filterTargetKey) {
if (Array.isArray(collection.filterTargetKey)) {
return collection?.filterTargetKey?.[0];
}
return collection?.filterTargetKey;
}
return collection?.getPrimaryKey() || 'id';
}

View File

@ -26,7 +26,7 @@ export class UUIDFieldInterface extends CollectionFieldInterface {
'x-validator': 'uuid', 'x-validator': 'uuid',
}, },
}; };
availableTypes = ['string', 'uid', 'uuid']; availableTypes = ['string', 'uuid'];
properties = { properties = {
'uiSchema.title': { 'uiSchema.title': {
type: 'string', type: 'string',

View File

@ -90,6 +90,9 @@ export class SqlCollectionTemplate extends CollectionTemplate {
description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`, description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Select', 'x-component': 'Select',
'x-component-props': {
multiple: true,
},
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'], 'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],
}, },
}; };

View File

@ -153,6 +153,9 @@ export class ViewCollectionTemplate extends CollectionTemplate {
description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`, description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Select', 'x-component': 'Select',
'x-component-props': {
multiple: true,
},
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'], 'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],
}, },
...getConfigurableProperties('category', 'description'), ...getConfigurableProperties('category', 'description'),

Some files were not shown because too many files have changed in this diff Show More