Merge branch 'next' into T-4910

This commit is contained in:
mytharcher 2024-11-15 10:59:04 +08:00
commit 5824f1fb15
1063 changed files with 27739 additions and 8842 deletions

View File

@ -1,10 +1,14 @@
name: Auto merge main -> next name: Auto merge main -> next
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.repository }}
cancel-in-progress: true cancel-in-progress: true
on: on:
workflow_dispatch:
inputs:
repository:
description: 'Please enter a repository name'
push: push:
branches: branches:
- 'main' - 'main'
@ -18,7 +22,7 @@ jobs:
with: with:
app-id: ${{ vars.NOCOBASE_APP_ID }} app-id: ${{ vars.NOCOBASE_APP_ID }}
private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }} repositories: nocobase,pro-plugins,plugin-pro-tpl,${{ join(fromJSON(vars.NEXT_PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }}
skip-token-revoke: true skip-token-revoke: true
- name: Get GitHub App User ID - name: Get GitHub App User ID
id: get-user-id id: get-user-id
@ -28,11 +32,11 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: nocobase/nocobase repository: nocobase/${{ inputs.repository || 'nocobase' }}
token: ${{ steps.app-token.outputs.token }} 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(${{ inputs.repository || 'nocobase' }})
run: | run: |
git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>' git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>'
@ -41,11 +45,11 @@ jobs:
git checkout next git checkout next
git merge main git merge main
git push origin next git push origin next
- name: push nocobase(next) - name: push ${{ inputs.repository || 'nocobase' }}(next)
uses: ad-m/github-push-action@master uses: ad-m/github-push-action@master
with: with:
branch: next branch: next
github_token: ${{ steps.app-token.outputs.token }} github_token: ${{ steps.app-token.outputs.token }}
repository: nocobase/nocobase repository: nocobase/${{ inputs.repository || 'nocobase' }}
tags: true tags: true
atomic: true atomic: true

View File

@ -63,28 +63,28 @@ jobs:
username: ${{ secrets.ALI_DOCKER_USERNAME }} username: ${{ secrets.ALI_DOCKER_USERNAME }}
password: ${{ secrets.ALI_DOCKER_PASSWORD }} password: ${{ secrets.ALI_DOCKER_PASSWORD }}
- name: Login to Aliyun Container Registry (Public) # - name: Login to Aliyun Container Registry (Public)
uses: docker/login-action@v2 # uses: docker/login-action@v2
with: # with:
registry: ${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }} # registry: ${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}
username: ${{ secrets.ALI_DOCKER_USERNAME }} # username: ${{ secrets.ALI_DOCKER_USERNAME }}
password: ${{ secrets.ALI_DOCKER_PASSWORD }} # password: ${{ secrets.ALI_DOCKER_PASSWORD }}
#
- name: Login to Docker Hub # - name: Login to Docker Hub
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next' # if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next'
uses: docker/login-action@v2 # uses: docker/login-action@v2
with: # with:
username: ${{ secrets.DOCKERHUB_USERNAME }} # username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} # password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set tags - name: Set tags
id: set-tags id: set-tags
run: | run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]] || [[ "${{ github.ref }}" == "refs/heads/next" ]]; then # if [[ "${{ github.ref }}" == "refs/heads/main" ]] || [[ "${{ github.ref }}" == "refs/heads/next" ]]; then
echo "::set-output name=tags::${{ steps.meta.outputs.tags }},${{ secrets.ALI_DOCKER_REGISTRY }}/${{ steps.meta.outputs.tags }},${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/${{ steps.meta.outputs.tags }}" # echo "::set-output name=tags::${{ steps.meta.outputs.tags }},${{ secrets.ALI_DOCKER_REGISTRY }}/${{ steps.meta.outputs.tags }},${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/${{ steps.meta.outputs.tags }}"
else # else
echo "::set-output name=tags::${{ secrets.ALI_DOCKER_REGISTRY }}/${{ steps.meta.outputs.tags }}" echo "::set-output name=tags::${{ secrets.ALI_DOCKER_REGISTRY }}/${{ steps.meta.outputs.tags }}"
fi # fi
- name: Build and push - name: Build and push
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3

View File

@ -13,6 +13,7 @@ on:
paths: paths:
- 'packages/**' - 'packages/**'
- 'Dockerfile.pro' - 'Dockerfile.pro'
- 'package.json'
- '.github/workflows/build-pro-image.yml' - '.github/workflows/build-pro-image.yml'
jobs: jobs:

View File

@ -16,18 +16,31 @@ on:
default: beta default: beta
push: push:
tags: tags:
- 'v*-beta' - 'v*'
jobs: jobs:
write-changelog-and-release: write-changelog-and-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Get info
id: get-info
shell: bash
run: |
if [[ "${{ inputs.version }}" == "alpha" || ${{ github.ref_name }} =~ "alpha" ]]; then
echo "branch=$(echo 'next')" >> $GITHUB_OUTPUT
echo "version=$(echo 'alpha')" >> $GITHUB_OUTPUT
echo "proRepos=$(echo '${{ vars.NEXT_PRO_PLUGIN_REPOS }}')" >> $GITHUB_OUTPUT
else
echo "branch=$(echo 'main')" >> $GITHUB_OUTPUT
echo "version=$(echo 'beta')" >> $GITHUB_OUTPUT
echo "proRepos=$(echo '${{ vars.PRO_PLUGIN_REPOS }}')" >> $GITHUB_OUTPUT
fi
- uses: actions/create-github-app-token@v1 - uses: actions/create-github-app-token@v1
id: app-token id: app-token
with: with:
app-id: ${{ vars.NOCOBASE_APP_ID }} app-id: ${{ vars.NOCOBASE_APP_ID }}
private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }} repositories: nocobase,pro-plugins,${{ join(fromJSON(steps.get-info.outputs.proRepos), ',') }}
skip-token-revoke: true skip-token-revoke: true
- name: Get GitHub App User ID - name: Get GitHub App User ID
id: get-user-id id: get-user-id
@ -38,7 +51,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: nocobase/nocobase repository: nocobase/nocobase
ref: main ref: ${{ steps.get-info.outputs.branch }}
token: ${{ steps.app-token.outputs.token }} token: ${{ steps.app-token.outputs.token }}
persist-credentials: true persist-credentials: true
fetch-depth: 0 fetch-depth: 0
@ -47,15 +60,16 @@ jobs:
with: with:
repository: nocobase/pro-plugins repository: nocobase/pro-plugins
path: packages/pro-plugins path: packages/pro-plugins
ref: ${{ steps.get-info.outputs.branch }}
fetch-depth: 0 fetch-depth: 0
token: ${{ steps.app-token.outputs.token }} token: ${{ steps.app-token.outputs.token }}
persist-credentials: true persist-credentials: true
- 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-info.outputs.proRepos), ' ') }}
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 ${{ steps.get-info.outputs.branch }} https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
done done
- name: Set user - name: Set user
run: | run: |
@ -70,11 +84,12 @@ jobs:
- name: Run script - name: Run script
shell: bash shell: bash
run: | run: |
node scripts/release/changelogAndRelease.js --ver ${{ inputs.version }} --cmsURL ${{ secrets.CMS_URL }} --cmsToken ${{ secrets.CMS_TOKEN }} node scripts/release/changelogAndRelease.js --ver ${{ steps.get-info.outputs.version }} --cmsURL ${{ secrets.CMS_URL }} --cmsToken ${{ secrets.CMS_TOKEN }}
env: env:
PRO_PLUGIN_REPOS: ${{ vars.PRO_PLUGIN_REPOS }} PRO_PLUGIN_REPOS: ${{ steps.get-info.outputs.proRepos }}
GH_TOKEN: ${{ steps.app-token.outputs.token }} GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Commit and push - name: Commit and push
if: ${{ steps.get-info.outputs.version == 'beta' }}
run: | run: |
git pull origin main git pull origin main
git add . git add .

View File

@ -44,7 +44,7 @@ jobs:
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.7
with: with:
host: ${{ secrets.CN_CLIENT_HOST }} host: ${{ secrets.CN_CLIENT_HOST }}
username: ${{ secrets.CN_CLIENT_USERNAME }} username: ${{ secrets.CN_CLIENT_USERNAME }}

View File

@ -44,7 +44,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

@ -1,19 +1,18 @@
name: Manual build pro image name: Manual build pro image
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.pr_number }}-${{ inputs.nocobase_pr_number }}
cancel-in-progress: true cancel-in-progress: true
run-name: Build pro image ${{ github.ref }}-${{ inputs.pr_number }}-${{ inputs.nocobase_pr_number }}
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
base_branch:
description: 'Please enter a base branch for main repo'
required: true
default: 'main'
pr_number: pr_number:
description: 'Please enter a pull request number' description: 'Please enter the pr number of pro-plugins'
required: true nocobase_pr_number:
description: 'Please enter the pr number of nocobase/nocobase repository'
jobs: jobs:
build-and-push: build-and-push:
@ -43,22 +42,32 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: ${{ github.event.inputs.base_branch }} ref: ${{ github.head_ref || github.ref_name }}
token: ${{ steps.app-token.outputs.token }} token: ${{ steps.app-token.outputs.token }}
submodules: true submodules: true
- name: Set PR branch - name: Checkout nocobase/nocobase pr
id: set_pro_pr_branch if: ${{ inputs.nocobase_pr_number != '' }}
if: inputs.pr_number != 'main' shell: bash
run: echo "pr_branch=refs/pull/${{ github.event.inputs.pr_number }}/head" >> $GITHUB_OUTPUT run: |
- name: Echo PR branch gh pr checkout ${{ inputs.nocobase_pr_number }}
run: echo "${{ steps.set_pro_pr_branch.outputs.pr_branch }}" env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Checkout pro-plugins - name: Checkout pro-plugins
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: nocobase/pro-plugins repository: nocobase/pro-plugins
path: packages/pro-plugins path: packages/pro-plugins
ref: ${{ steps.set_pro_pr_branch.outputs.pr_branch || 'main' }} ref: ${{ github.head_ref || github.ref_name }}
token: ${{ steps.app-token.outputs.token }} token: ${{ steps.app-token.outputs.token }}
- name: Checkout pr
if: ${{ inputs.pr_number != '' }}
shell: bash
run: |
cd ./packages/pro-plugins/
gh pr checkout ${{ inputs.pr_number }}
cd ../../
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Clone pro repos - name: Clone pro repos
shell: bash shell: bash
run: | run: |
@ -81,23 +90,24 @@ jobs:
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
with: with:
driver-opts: network=host driver-opts: network=host
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
nocobase/nocobase
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Login to Aliyun Container Registry - name: Login to Aliyun Container Registry
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
registry: ${{ secrets.ALI_DOCKER_REGISTRY }} registry: ${{ secrets.ALI_DOCKER_REGISTRY }}
username: ${{ secrets.ALI_DOCKER_USERNAME }} username: ${{ secrets.ALI_DOCKER_USERNAME }}
password: ${{ secrets.ALI_DOCKER_PASSWORD }} 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 }}-pro"
- name: Set variables - name: Set variables
run: | run: |
target_directory="./packages/pro-plugins/@nocobase" target_directory="./packages/pro-plugins/@nocobase"
@ -110,11 +120,11 @@ jobs:
echo "var1=$BEFORE_PACK_NOCOBASE" >> $GITHUB_OUTPUT echo "var1=$BEFORE_PACK_NOCOBASE" >> $GITHUB_OUTPUT
echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT
id: vars id: vars
- name: Build and push - pr-${{ inputs.pr_number }}-pro - name: Build and push - ${{ steps.get-tag.outputs.tag }}-pro
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}
@ -122,12 +132,12 @@ jobs:
BEFORE_PACK_NOCOBASE=${{ steps.vars.outputs.var1 }} BEFORE_PACK_NOCOBASE=${{ steps.vars.outputs.var1 }}
APPEND_PRESET_LOCAL_PLUGINS=${{ steps.vars.outputs.var2 }} APPEND_PRESET_LOCAL_PLUGINS=${{ steps.vars.outputs.var2 }}
push: true push: true
tags: ${{ secrets.ALI_DOCKER_REGISTRY }}/nocobase/nocobase:pr-${{ inputs.pr_number }}-pro tags: ${{ steps.set-tags.outputs.tags }}
- name: Deploy NocoBase - name: Deploy NocoBase
run: | run: |
curl --retry 2 --location --request POST "${{secrets.NOCOBASE_DEPLOY_HOST}}pr-${{ inputs.pr_number }}-pro" \ curl --retry 2 --location --request POST "${{secrets.NOCOBASE_DEPLOY_HOST}}${{ steps.get-tag.outputs.tag }}-pro" \
--header 'Content-Type: application/json' \ --header 'Content-Type: application/json' \
-d "{ -d "{
\"tag\": \"pr-${{ inputs.pr_number }}-pro\", \"tag\": \"${{ steps.get-tag.outputs.tag }}-pro\",
\"dialect\": \"postgres\" \"dialect\": \"postgres\"
}" }"

View File

@ -1,9 +1,11 @@
name: Build pro plugin docker image name: Build pro plugin docker image
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.pro_plugin }}-${{ inputs.pr_number }}-${{ inputs.nocobase_pr_number }}
cancel-in-progress: true cancel-in-progress: true
run-name: Build pro plugin image ${{ github.ref }}-${{ inputs.pro_plugin }}-${{ inputs.pr_number }}-${{ inputs.nocobase_pr_number }}
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
@ -107,7 +109,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,86 @@
name: Manual release next
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
jobs:
update-version:
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.NEXT_PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }}
skip-token-revoke: true
- name: Get GitHub App User ID
id: get-user-id
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Checkout
uses: actions/checkout@v4
with:
repository: nocobase/nocobase
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true
fetch-depth: 0
ref: next
- name: Checkout pro-plugins
uses: actions/checkout@v4
with:
repository: nocobase/pro-plugins
path: packages/pro-plugins
fetch-depth: 0
ref: next
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true
- name: Clone pro repos
shell: bash
run: |
for repo in ${{ join(fromJSON(vars.NEXT_PRO_PLUGIN_REPOS), ' ') }} ${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ' ') }}
do
git clone -b next https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
done
- name: Set Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Lerna
run: npm install -g lerna@4 auto-changelog@2
- name: Run release.sh
shell: bash
run: |
cd ./packages/pro-plugins
git checkout next
for repo in ${{ join(fromJSON(vars.NEXT_PRO_PLUGIN_REPOS), ' ') }} ${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ' ') }}
do
echo "@nocobase/$repo" >> .git/info/exclude
done
echo "$(<.git/info/exclude )"
cd ./../..
git checkout next
git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>'
echo "packages/pro-plugins" >> .git/info/exclude
bash release.sh
env:
PRO_PLUGIN_REPOS: ${{ vars.NEXT_PRO_PLUGIN_REPOS }}
CUSTOM_PRO_PLUGIN_REPOS: ${{ vars.CUSTOM_PRO_PLUGIN_REPOS }}
- name: Push
run: |
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 push origin next --atomic --tags
cd ../../../../
done
cd ./packages/pro-plugins
git push origin next --atomic --tags
cd ../../
git push origin next --tags --atomic

View File

@ -6,10 +6,6 @@ concurrency:
on: on:
workflow_dispatch: workflow_dispatch:
inputs:
is_feat:
description: 'is feat'
type: boolean
jobs: jobs:
pre-merge-main-into-next: pre-merge-main-into-next:
@ -110,9 +106,8 @@ jobs:
git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>' git 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
env: env:
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 }} CUSTOM_PRO_PLUGIN_REPOS: ${{ vars.CUSTOM_PRO_PLUGIN_REPOS }}
- name: Push and merge into next - name: Push and merge into next

View File

@ -1,154 +0,0 @@
name: Release next
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
jobs:
publish-npm:
runs-on: ubuntu-latest
container: node:18
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.NEXT_PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }}
skip-token-revoke: true
- name: Get GitHub App User ID
id: get-user-id
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Checkout
uses: actions/checkout@v4
with:
ref: next
fetch-depth: 0
- name: Send curl request and parse response
env:
PKG_USERNAME: ${{ secrets.PKG_USERNAME }}
PKG_PASSWORD: ${{ secrets.PKG_PASSWORD }}
run: |
mkdir git-ci-cache
apt-get update && apt-get install -y jq gh
response1=$(curl -s 'https://pkg.nocobase.com/-/verdaccio/sec/login' \
-H 'content-type: application/json' \
--data-raw '{"username":"'$PKG_USERNAME'","password":"'$PKG_PASSWORD'"}')
token1=$(echo $response1 | jq -r '.token')
response2=$(curl -s 'https://pkg-src.nocobase.com/-/verdaccio/sec/login' \
-H 'content-type: application/json' \
--data-raw '{"username":"'$PKG_USERNAME'","password":"'$PKG_PASSWORD'"}')
token2=$(echo $response2 | jq -r '.token')
echo "PKG_NOCOBASE_TOKEN=$token1" >> $GITHUB_ENV
echo "PKG_SRC_NOCOBASE_TOKEN=$token2" >> $GITHUB_ENV
- name: restore cache
id: cache
uses: actions/cache@v3
with:
path: ./git-ci-cache
key: new-next-version-${{ github.run_id }}
- name: Set NEWVERSION variable
id: set_version
run: |
cd ./git-ci-cache
if [ -f newversion.txt ]; then
NEWVERSION=$(cat newversion.txt)
else
NEWVERSION=$(cat ../lerna.json | jq -r '.version').$(date +'%Y%m%d%H%M%S')
echo "$NEWVERSION" > newversion.txt
fi
echo "NEWVERSION=$NEWVERSION" >> $GITHUB_ENV
- name: Print NEWVERSION
run: echo "The new version is ${{ env.NEWVERSION }}"
- name: Save NEWVERSION to cache
run: echo "NEWVERSION=$NEWVERSION" >> ./git-ci-cache/newversion.txt
- name: save cache
id: save-cache
uses: actions/cache/save@v3
if: steps.cache.outputs.cache-hit != 'true'
with:
path: ./git-ci-cache
key: new-next-version-${{ github.run_id }}
- name: publish npmjs.org
continue-on-error: true
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>'
git config --global --add safe.directory /__w/nocobase/nocobase
npm config set access public
npm config set registry https://registry.npmjs.org/
npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
yarn config set access public
yarn config set registry https://registry.npmjs.org/
yarn config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
yarn install
yarn lerna version ${{ env.NEWVERSION }} -y --no-git-tag-version
yarn build
echo "# test" >> Release.md
git add .
git commit -m "chore(versions): test publish packages xxx"
cat lerna.json
yarn release:force --no-verify-access --no-git-reset --registry https://registry.npmjs.org/ --dist-tag=next
- name: Checkout pro-plugins
uses: actions/checkout@v3
with:
repository: nocobase/pro-plugins
path: packages/pro-plugins
ref: next
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Clone pro repos
shell: bash
run: |
for repo in ${{ join(fromJSON(vars.NEXT_PRO_PLUGIN_REPOS), ' ') }} ${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ' ') }}
do
git clone -b next https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
done
- name: Build Pro plugins
run: |
yarn config set registry https://registry.npmjs.org/
yarn install
yarn lerna version ${{ env.NEWVERSION }} -y --no-git-tag-version
yarn build packages/pro-plugins
- name: publish pkg.nocobase.com
run: |
git reset --hard
npm config set //pkg.nocobase.com/:_authToken=${{ env.PKG_NOCOBASE_TOKEN }}
yarn release:force --no-verify-access --no-git-reset --registry https://pkg.nocobase.com --dist-tag=next
- name: publish pkg-src.nocobase.com
run: |
git reset --hard
bash generate-npmignore.sh ignore-src
npm config set //pkg-src.nocobase.com/:_authToken=${{ env.PKG_SRC_NOCOBASE_TOKEN }}
yarn release:force --no-verify-access --no-git-reset --registry https://pkg-src.nocobase.com --dist-tag=next
- name: Tag
run: |
git reset --hard HEAD~
git tag v${{ env.NEWVERSION }}
git push origin v${{ env.NEWVERSION }}
cd ./packages/pro-plugins
git reset --hard
git tag v${{ env.NEWVERSION }}
git push origin v${{ env.NEWVERSION }}
cd ../../
for repo in ${{ join(fromJSON(vars.NEXT_PRO_PLUGIN_REPOS), ' ') }} ${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ' ') }}
do
cd ./packages/pro-plugins/@nocobase/$repo
git reset --hard
git tag v${{ env.NEWVERSION }}
git push origin v${{ env.NEWVERSION }}
cd ../../../../
done
- name: Run release script
shell: bash
run: |
git fetch
node scripts/release/changelogAndRelease.js --ver alpha --cmsURL ${{ secrets.CMS_URL }} --cmsToken ${{ secrets.CMS_TOKEN }}
env:
PRO_PLUGIN_REPOS: ${{ vars.NEXT_PRO_PLUGIN_REPOS }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}

View File

@ -5,6 +5,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
on: on:
workflow_dispatch:
push: push:
tags: tags:
- 'v*' - 'v*'
@ -14,15 +15,28 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: node:18 container: node:18
steps: steps:
- name: Get info
id: get-info
shell: bash
run: |
if [[ "${{ github.ref_name }}" =~ "beta" ]]; then
echo "defaultTag=$(echo 'latest')" >> $GITHUB_OUTPUT
echo "proRepos=$(echo '${{ vars.PRO_PLUGIN_REPOS }}')" >> $GITHUB_OUTPUT
else
echo "defaultTag=$(echo 'next')" >> $GITHUB_OUTPUT
echo "proRepos=$(echo '${{ vars.NEXT_PRO_PLUGIN_REPOS }}')" >> $GITHUB_OUTPUT
fi
- uses: actions/create-github-app-token@v1 - uses: actions/create-github-app-token@v1
id: app-token id: app-token
with: with:
app-id: ${{ vars.NOCOBASE_APP_ID }} app-id: ${{ vars.NOCOBASE_APP_ID }}
private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }} repositories: nocobase,pro-plugins,${{ join(fromJSON(steps.get-info.outputs.proRepos), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }}
skip-token-revoke: true skip-token-revoke: true
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
ref: ${{ github.ref_name }}
- 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 }}
@ -61,19 +75,20 @@ jobs:
yarn config set registry https://registry.npmjs.org/ yarn config set registry https://registry.npmjs.org/
yarn config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} yarn config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
npm whoami npm whoami
yarn release:force --no-verify-access --no-git-reset --registry https://registry.npmjs.org/ yarn release:force --no-verify-access --no-git-reset --registry https://registry.npmjs.org/ --dist-tag=${{ steps.get-info.outputs.defaultTag }}
- name: Checkout pro-plugins - name: Checkout pro-plugins
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
repository: nocobase/pro-plugins repository: nocobase/pro-plugins
path: packages/pro-plugins path: packages/pro-plugins
ref: ${{ github.ref_name }}
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), ' ') }} ${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ' ') }} for repo in ${{ join(fromJSON(steps.get-info.outputs.proRepos), ' ') }} ${{ 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 ${{ 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: Build Pro plugins - name: Build Pro plugins
run: | run: |
@ -84,17 +99,26 @@ jobs:
run: | run: |
git reset --hard git reset --hard
npm config set //pkg.nocobase.com/:_authToken=${{ env.PKG_NOCOBASE_TOKEN }} npm config set //pkg.nocobase.com/:_authToken=${{ env.PKG_NOCOBASE_TOKEN }}
yarn release:force --no-verify-access --no-git-reset --registry https://pkg.nocobase.com yarn release:force --no-verify-access --no-git-reset --registry https://pkg.nocobase.com --dist-tag=${{ steps.get-info.outputs.defaultTag }}
- name: publish pkg-src.nocobase.com - name: publish pkg-src.nocobase.com
run: | run: |
git reset --hard git reset --hard
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 yarn release:force --no-verify-access --no-git-reset --registry https://pkg-src.nocobase.com --dist-tag=${{ steps.get-info.outputs.defaultTag }}
push-docker: push-docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: publish-npm needs: publish-npm
steps: steps:
- name: Get info
id: get-info
shell: bash
run: |
if [[ "${{ github.ref_name }}" =~ "beta" ]]; then
echo "branch=$(echo 'main')" >> $GITHUB_OUTPUT
else
echo "branch=$(echo 'next')" >> $GITHUB_OUTPUT
fi
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Set up QEMU - name: Set up QEMU
@ -123,10 +147,21 @@ jobs:
registry: ${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }} registry: ${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}
username: ${{ secrets.ALI_DOCKER_USERNAME }} username: ${{ secrets.ALI_DOCKER_USERNAME }}
password: ${{ secrets.ALI_DOCKER_PASSWORD }} password: ${{ secrets.ALI_DOCKER_PASSWORD }}
- name: Build and push - name: Build and push main
if: ${{ steps.get-info.outputs.branch == 'main' }}
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
with: with:
context: ./docker/nocobase context: ./docker/nocobase
file: ./docker/nocobase/Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: nocobase/nocobase:latest,${{ steps.meta.outputs.tags }},${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/nocobase/nocobase:latest,${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/${{ steps.meta.outputs.tags }} tags: nocobase/nocobase:main,nocobase/nocobase:latest,${{ steps.meta.outputs.tags }},${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/nocobase/nocobase:main,${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/nocobase/nocobase:latest,${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/${{ steps.meta.outputs.tags }}
- name: Build and push next
if: ${{ steps.get-info.outputs.branch == 'next' }}
uses: docker/build-push-action@v3
with:
context: ./docker/nocobase
file: ./docker/nocobase/Dockerfile.next
platforms: linux/amd64,linux/arm64
push: true
tags: nocobase/nocobase:next,${{ steps.meta.outputs.tags }},${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/nocobase/nocobase:next,${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/${{ steps.meta.outputs.tags }}

View File

@ -5,6 +5,216 @@ 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).
## [v1.3.50-beta](https://github.com/nocobase/nocobase/compare/v1.3.49-beta...v1.3.50-beta) - 2024-11-14
### 🐛 Bug Fixes
- **[client]** Fix issue preventing linkage rule title from being cleared during editing ([#5644](https://github.com/nocobase/nocobase/pull/5644)) by @katherinehhh
- **[Comments]** Fix data scope setting not working in comment block by @katherinehhh
## [v1.3.49-beta](https://github.com/nocobase/nocobase/compare/v1.3.48-beta...v1.3.49-beta) - 2024-11-13
### 🚀 Improvements
- **[client]** support one-to-one and many-to-many (array) field to use file collection ([#5637](https://github.com/nocobase/nocobase/pull/5637)) by @mytharcher
- **[evaluators]** use Formula.js as default evaluator in calculation node ([#5626](https://github.com/nocobase/nocobase/pull/5626)) by @mytharcher
### 🐛 Bug Fixes
- **[client]** Fix reset issue that reverts filter button title to default ([#5635](https://github.com/nocobase/nocobase/pull/5635)) by @katherinehhh
- **[Action: Import records]** Fixed the issue that many-to-many relationship data cannot be imported through the id field ([#5623](https://github.com/nocobase/nocobase/pull/5623)) by @chareice
## [v1.3.48-beta](https://github.com/nocobase/nocobase/compare/v1.3.47-beta...v1.3.48-beta) - 2024-11-11
### 🚀 Improvements
- **[client]** support hiding menu items ([#5624](https://github.com/nocobase/nocobase/pull/5624)) by @chenos
- **[server]** add DB_SQL_BENCHMARK environment variable ([#5615](https://github.com/nocobase/nocobase/pull/5615)) by @chareice
### 🐛 Bug Fixes
- **[client]** support file collection as target of one-to-many association ([#5619](https://github.com/nocobase/nocobase/pull/5619)) by @mytharcher
- **[Action: Import records]** Fixed the issue that many-to-many relationship data cannot be imported through the id field ([#5623](https://github.com/nocobase/nocobase/pull/5623)) by @chareice
## [v1.3.47-beta](https://github.com/nocobase/nocobase/compare/v1.3.46-beta...v1.3.47-beta) - 2024-11-08
### 🚀 Improvements
- **[Authentication]** Optimize error message for sign in and sign up ([#5612](https://github.com/nocobase/nocobase/pull/5612)) by @2013xile
### 🐛 Bug Fixes
- **[client]**
- Fix default value issues in subtable ([#5607](https://github.com/nocobase/nocobase/pull/5607)) by @zhangzhonghe
- Fix issue with fuzzy search support for association fields with string type title field ([#5611](https://github.com/nocobase/nocobase/pull/5611)) by @katherinehhh
- **[Authentication]** Fix the issue where users can't change password when signing in with a non-password authenticator ([#5609](https://github.com/nocobase/nocobase/pull/5609)) by @2013xile
## [v1.3.45-beta](https://github.com/nocobase/nocobase/compare/v1.3.44-beta...v1.3.45-beta) - 2024-11-06
### 🐛 Bug Fixes
- **[client]** permission for the association table field in the table is based on the permission of the corresponding association field ([#5569](https://github.com/nocobase/nocobase/pull/5569)) by @katherinehhh
- **[Action: Export records]** Fix export with i18n ([#5591](https://github.com/nocobase/nocobase/pull/5591)) by @chareice
- **[Action: Import records]** fix issue with import belongs to association ([#5417](https://github.com/nocobase/nocobase/pull/5417)) by @chareice
## [v1.3.44-beta](https://github.com/nocobase/nocobase/compare/v1.3.43-beta...v1.3.44-beta) - 2024-11-05
### 🎉 New Features
- **[Auth: OIDC]** Add an option "enable RP-initiated logout" by @2013xile
### 🐛 Bug Fixes
- **[client]** Fix filter issue when setting single-select field as title field in association select ([#5581](https://github.com/nocobase/nocobase/pull/5581)) by @katherinehhh
## [v1.3.43-beta](https://github.com/nocobase/nocobase/compare/v1.3.42-beta...v1.3.43-beta) - 2024-11-05
### 🚀 Improvements
- **[client]** numeric precision can be configured to 8 digits ([#5552](https://github.com/nocobase/nocobase/pull/5552)) by @chenos
### 🐛 Bug Fixes
- **[client]** Fix linkage style not updating in form. ([#5539](https://github.com/nocobase/nocobase/pull/5539)) by @sheldon66
- **[Auth: API keys]** Fix the URL path for API keys settings page ([#5562](https://github.com/nocobase/nocobase/pull/5562)) by @2013xile
- **[Mobile]** Fix the issue of preview images being covered by page ([#5535](https://github.com/nocobase/nocobase/pull/5535)) by @zhangzhonghe
- **[Block: Map]** resolve map rendering in sub-details and incorrect value display for empty fields ([#5526](https://github.com/nocobase/nocobase/pull/5526)) by @katherinehhh
- **[Collection: Tree]** Fix errors when updating path collection ([#5551](https://github.com/nocobase/nocobase/pull/5551)) by @2013xile
## [v1.3.42-beta](https://github.com/nocobase/nocobase/compare/v1.3.41-beta...v1.3.42-beta) - 2024-10-28
### 🐛 Bug Fixes
- **[Collection: Tree]** Fix the issue where node paths are not updated when disassociate children ([#5522](https://github.com/nocobase/nocobase/pull/5522)) by @2013xile
## [v1.3.41-beta](https://github.com/nocobase/nocobase/compare/v1.3.40-beta...v1.3.41-beta) - 2024-10-27
### 🚀 Improvements
- **[Access control]** Optimize performance for large tables in acl ([#5496](https://github.com/nocobase/nocobase/pull/5496)) by @chareice
## [v1.3.40-beta](https://github.com/nocobase/nocobase/compare/v1.3.39-beta...v1.3.40-beta) - 2024-10-25
### 🎉 New Features
- **[Auth: OIDC]** Add an option for allowing skip ssl verification by @2013xile
### 🚀 Improvements
- **[client]** show disabled unchecked checkbox for unselected fields ([#5503](https://github.com/nocobase/nocobase/pull/5503)) by @katherinehhh
### 🐛 Bug Fixes
- **[database]** Fix the issue where string operators "contains" and "does not contain do not properly handle `null` values ([#5509](https://github.com/nocobase/nocobase/pull/5509)) by @2013xile
- **[client]** Fix linkage rule to correctly evaluate URL parameter variables ([#5504](https://github.com/nocobase/nocobase/pull/5504)) by @katherinehhh
- **[Block: Map]** Fixed the issue where some maps are displayed incorrectly when multiple maps exist due to multiple calls to the `load` method of AMap ([#5490](https://github.com/nocobase/nocobase/pull/5490)) by @Cyx649312038
## [v1.3.39-beta](https://github.com/nocobase/nocobase/compare/v1.3.38-beta...v1.3.39-beta) - 2024-10-24
### 🐛 Bug Fixes
- **[client]** Fix the issue where filter blocks cannot be added in the popup ([#5502](https://github.com/nocobase/nocobase/pull/5502)) by @zhangzhonghe
## [v1.3.38-beta](https://github.com/nocobase/nocobase/compare/v1.3.37-beta...v1.3.38-beta) - 2024-10-24
### 🐛 Bug Fixes
- **[client]**
- pagination issue in list block with simple pagination collection ([#5500](https://github.com/nocobase/nocobase/pull/5500)) by @katherinehhh
- In non-config mode, display only the current collection in the edit form. ([#5499](https://github.com/nocobase/nocobase/pull/5499)) by @katherinehhh
- **[Workflow: HTTP request node]** fix special white space appears when paste content into variable textarea caused issue ([#5497](https://github.com/nocobase/nocobase/pull/5497)) by @mytharcher
- **[Departments]** Fix the issue of incorrect external data source permissions check under the department role by @2013xile
## [v1.3.37-beta](https://github.com/nocobase/nocobase/compare/v1.3.36-beta...v1.3.37-beta) - 2024-10-23
### 🚀 Improvements
- **[client]** Adjust hint in configuration panel of binding workflow to a button ([#5494](https://github.com/nocobase/nocobase/pull/5494)) by @mytharcher
### 🐛 Bug Fixes
- **[File manager]** fix upload and destroy file record within an association block ([#5493](https://github.com/nocobase/nocobase/pull/5493)) by @mytharcher
## [v1.3.36-beta](https://github.com/nocobase/nocobase/compare/v1.3.35-beta...v1.3.36-beta) - 2024-10-22
### 🐛 Bug Fixes
- **[Collection: Tree]** Fix the issue where the path collection for the inheritance tree collection is not automatically created ([#5486](https://github.com/nocobase/nocobase/pull/5486)) by @2013xile
- **[Calendar]** show pagination bar with data in the table ([#5480](https://github.com/nocobase/nocobase/pull/5480)) by @katherinehhh
- **[File manager]** fix file can not be uploaded due to rule hook. ([#5460](https://github.com/nocobase/nocobase/pull/5460)) by @mytharcher
- **[Collection field: Formula]** Fix incorrect formula calculation in nested multi-level sub-table ([#5469](https://github.com/nocobase/nocobase/pull/5469)) by @gu-zhichao
## [v1.3.35-beta](https://github.com/nocobase/nocobase/compare/v1.3.34-beta...v1.3.35-beta) - 2024-10-21
### 🚀 Improvements
- **[Workflow: mailer node]** add placeholder to mailer node ([#5470](https://github.com/nocobase/nocobase/pull/5470)) by @mytharcher
## [v1.3.34-beta](https://github.com/nocobase/nocobase/compare/v1.3.33-beta...v1.3.34-beta) - 2024-10-21
### 🎉 New Features
- **[test]** Association fields in filter forms support configuring whether multiple selection is allowed ([#5451](https://github.com/nocobase/nocobase/pull/5451)) by @zhangzhonghe
- **[client]** Add a variable named "Parent object" ([#5449](https://github.com/nocobase/nocobase/pull/5449)) by @zhangzhonghe
Reference: [Parent object](https://docs.nocobase.com/handbook/ui/variables#parent-object)
### 🐛 Bug Fixes
- **[client]**
- Fix URL search params variables not being parsed ([#5454](https://github.com/nocobase/nocobase/pull/5454)) by @zhangzhonghe
- Fix data clearing bug when selecting association data with data scope in nested sub-tables ([#5441](https://github.com/nocobase/nocobase/pull/5441)) by @katherinehhh
- fix selected background color of table row ([#5445](https://github.com/nocobase/nocobase/pull/5445)) by @mytharcher
- **[Block: Map]** remove zoom level configuration for map fields in table column ([#5457](https://github.com/nocobase/nocobase/pull/5457)) by @katherinehhh
- **[File manager]** fix calling storage rule hook on read-pretty fields ([#5447](https://github.com/nocobase/nocobase/pull/5447)) by @mytharcher
- **[Data source: Main]** fix e2e case failed due to component changed ([#5437](https://github.com/nocobase/nocobase/pull/5437)) by @mytharcher
## [v1.3.33-beta](https://github.com/nocobase/nocobase/compare/v1.3.32-beta...v1.3.33-beta) - 2024-10-16
### 🚀 Improvements
- **[Workflow]** add association field related hint to the batch mode of update node ([#5426](https://github.com/nocobase/nocobase/pull/5426)) by @mytharcher
### 🐛 Bug Fixes
- **[client]**
- fix the issue of Edit profile drawer being covered by subpage ([#5409](https://github.com/nocobase/nocobase/pull/5409)) by @zhangzhonghe
- Workflow node variables do not display inherited collection fields ([#5415](https://github.com/nocobase/nocobase/pull/5415)) by @chenos
- pagination not resetting after clearing filter data in table filtering block ([#5411](https://github.com/nocobase/nocobase/pull/5411)) by @katherinehhh
- **[File manager]** remove the 20 items limit of loading storages in file template collection configuration ([#5430](https://github.com/nocobase/nocobase/pull/5430)) by @mytharcher
- **[Action: Duplicate record]** Fix the issue where the bulk edit popup does not display content ([#5412](https://github.com/nocobase/nocobase/pull/5412)) by @zhangzhonghe
- **[Data visualization]** Fix the issue of default values not displaying in the chart filter block ([#5405](https://github.com/nocobase/nocobase/pull/5405)) by @zhangzhonghe
## [v1.3.32-beta](https://github.com/nocobase/nocobase/compare/v1.3.31-beta...v1.3.32-beta) - 2024-10-13 ## [v1.3.32-beta](https://github.com/nocobase/nocobase/compare/v1.3.31-beta...v1.3.32-beta) - 2024-10-13
### 🐛 Bug Fixes ### 🐛 Bug Fixes

View File

@ -5,6 +5,216 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。 并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
## [v1.3.50-beta](https://github.com/nocobase/nocobase/compare/v1.3.49-beta...v1.3.50-beta) - 2024-11-14
### 🐛 修复
- **[client]** 修复联动规则标题编辑时无法清空的问题 ([#5644](https://github.com/nocobase/nocobase/pull/5644)) by @katherinehhh
- **[评论]** 修复评论区块设置数据范围不生效问题 by @katherinehhh
## [v1.3.49-beta](https://github.com/nocobase/nocobase/compare/v1.3.48-beta...v1.3.49-beta) - 2024-11-13
### 🚀 优化
- **[client]** 一对一字段和多对多(数组)字段支持选择文件表 ([#5637](https://github.com/nocobase/nocobase/pull/5637)) by @mytharcher
- **[evaluators]** 将运算节点的默认计算引擎更换为 Formula.js ([#5626](https://github.com/nocobase/nocobase/pull/5626)) by @mytharcher
### 🐛 修复
- **[client]** 修复筛选按钮重置后标题恢复为默认名称的问题 ([#5635](https://github.com/nocobase/nocobase/pull/5635)) by @katherinehhh
- **[操作:导入记录]** 修复无法通过 id 字段导入多对多关联数据的问题 ([#5623](https://github.com/nocobase/nocobase/pull/5623)) by @chareice
## [v1.3.48-beta](https://github.com/nocobase/nocobase/compare/v1.3.47-beta...v1.3.48-beta) - 2024-11-11
### 🚀 优化
- **[client]** 支持隐藏菜单项 ([#5624](https://github.com/nocobase/nocobase/pull/5624)) by @chenos
- **[server]** 增加 DB_SQL_BENCHMARK 环境变量 ([#5615](https://github.com/nocobase/nocobase/pull/5615)) by @chareice
### 🐛 修复
- **[client]** 支持一对多关系使用文件表 ([#5619](https://github.com/nocobase/nocobase/pull/5619)) by @mytharcher
- **[操作:导入记录]** 修复无法通过 id 字段导入多对多关联数据的问题 ([#5623](https://github.com/nocobase/nocobase/pull/5623)) by @chareice
## [v1.3.47-beta](https://github.com/nocobase/nocobase/compare/v1.3.46-beta...v1.3.47-beta) - 2024-11-08
### 🚀 优化
- **[用户认证]** 优化登录、注册的错误提示 ([#5612](https://github.com/nocobase/nocobase/pull/5612)) by @2013xile
### 🐛 修复
- **[client]**
- 修复子表格中的默认值问题 ([#5607](https://github.com/nocobase/nocobase/pull/5607)) by @zhangzhonghe
- 修复 关系字段标题字段为string 类型时应支持模糊查询 ([#5611](https://github.com/nocobase/nocobase/pull/5611)) by @katherinehhh
- **[用户认证]** 修复用户使用非密码认证器登录时无法修改密码的问题 ([#5609](https://github.com/nocobase/nocobase/pull/5609)) by @2013xile
## [v1.3.45-beta](https://github.com/nocobase/nocobase/compare/v1.3.44-beta...v1.3.45-beta) - 2024-11-06
### 🐛 修复
- **[client]** 表格中关系表字段权限为该关系字段的权限 ([#5569](https://github.com/nocobase/nocobase/pull/5569)) by @katherinehhh
- **[操作:导出记录]** 修复导出过程中的多语言问题 ([#5591](https://github.com/nocobase/nocobase/pull/5591)) by @chareice
- **[操作:导入记录]** 修复无法导入多对一关联的问题 ([#5417](https://github.com/nocobase/nocobase/pull/5417)) by @chareice
## [v1.3.44-beta](https://github.com/nocobase/nocobase/compare/v1.3.43-beta...v1.3.44-beta) - 2024-11-05
### 🎉 新特性
- **[认证OIDC]** 添加「启用 RP-initiated logout」选项 by @2013xile
### 🐛 修复
- **[client]** 修复 关系字段下拉选项中设置单选字段为标题字段时筛选不生效的问题 ([#5581](https://github.com/nocobase/nocobase/pull/5581)) by @katherinehhh
## [v1.3.43-beta](https://github.com/nocobase/nocobase/compare/v1.3.42-beta...v1.3.43-beta) - 2024-11-05
### 🚀 优化
- **[client]** 数字精确度支持配置 8 位数 ([#5552](https://github.com/nocobase/nocobase/pull/5552)) by @chenos
### 🐛 修复
- **[client]** 修复联动样式在表单里不更新。 ([#5539](https://github.com/nocobase/nocobase/pull/5539)) by @sheldon66
- **[认证API 密钥]** 修复 API keys 设置页面的 URL 路径 ([#5562](https://github.com/nocobase/nocobase/pull/5562)) by @2013xile
- **[移动端]** 修复预览图片被页面覆盖的问题 ([#5535](https://github.com/nocobase/nocobase/pull/5535)) by @zhangzhonghe
- **[区块:地图]** 子详情中地图字段,渲染地图不正确,应该显示坐标字符/详情区块,没有值的字段,会显示上一条数据的值 ([#5526](https://github.com/nocobase/nocobase/pull/5526)) by @katherinehhh
- **[数据表:树]** 修复更新路径表时的报错 ([#5551](https://github.com/nocobase/nocobase/pull/5551)) by @2013xile
## [v1.3.42-beta](https://github.com/nocobase/nocobase/compare/v1.3.41-beta...v1.3.42-beta) - 2024-10-28
### 🐛 修复
- **[数据表:树]** 修复解除关联子节点,节点路径没有更新的问题 ([#5522](https://github.com/nocobase/nocobase/pull/5522)) by @2013xile
## [v1.3.41-beta](https://github.com/nocobase/nocobase/compare/v1.3.40-beta...v1.3.41-beta) - 2024-10-27
### 🚀 优化
- **[权限控制]** 优化权限系统中的大表查询性能 ([#5496](https://github.com/nocobase/nocobase/pull/5496)) by @chareice
## [v1.3.40-beta](https://github.com/nocobase/nocobase/compare/v1.3.39-beta...v1.3.40-beta) - 2024-10-25
### 🎉 新特性
- **[认证OIDC]** 添加“跳过 SSL 验证“选项 by @2013xile
### 🚀 优化
- **[client]** 勾选字段未勾选时显示禁用的未勾选框 ([#5503](https://github.com/nocobase/nocobase/pull/5503)) by @katherinehhh
### 🐛 修复
- **[database]** 修复字符串操作符”包含“和”不包含“没有正确处理 `null` 值的问题 ([#5509](https://github.com/nocobase/nocobase/pull/5509)) by @2013xile
- **[client]** 修复联动规则中使用「URL参数变量」作条件判断无效 ([#5504](https://github.com/nocobase/nocobase/pull/5504)) by @katherinehhh
- **[区块:地图]** 修复高德地图多次调用 `load` 方法,导致多张地图存在时,部分地图展示报错的问题 ([#5490](https://github.com/nocobase/nocobase/pull/5490)) by @Cyx649312038
## [v1.3.39-beta](https://github.com/nocobase/nocobase/compare/v1.3.38-beta...v1.3.39-beta) - 2024-10-24
### 🐛 修复
- **[client]** 修复弹窗中无法添加筛选区块的问题 ([#5502](https://github.com/nocobase/nocobase/pull/5502)) by @zhangzhonghe
## [v1.3.38-beta](https://github.com/nocobase/nocobase/compare/v1.3.37-beta...v1.3.38-beta) - 2024-10-24
### 🐛 修复
- **[client]**
- 使用简单分页的数据表在列表区块上分页异常 ([#5500](https://github.com/nocobase/nocobase/pull/5500)) by @katherinehhh
- 在非配置状态下,编辑表单应只显示本表区块 ([#5499](https://github.com/nocobase/nocobase/pull/5499)) by @katherinehhh
- **[工作流HTTP 请求节点]** 修复变量文本输入框中在粘贴时可能产生非标准空格导致服务端逻辑错误的问题 ([#5497](https://github.com/nocobase/nocobase/pull/5497)) by @mytharcher
- **[部门]** 修复在所属部门角色下外部数据源权限判断不正确的问题 by @2013xile
## [v1.3.37-beta](https://github.com/nocobase/nocobase/compare/v1.3.36-beta...v1.3.37-beta) - 2024-10-23
### 🚀 优化
- **[client]** 调整绑定工作流配置面板中的提示文案 ([#5494](https://github.com/nocobase/nocobase/pull/5494)) by @mytharcher
### 🐛 修复
- **[文件管理器]** 修复文件表在关联区块内无法上传和删除记录的问题 ([#5493](https://github.com/nocobase/nocobase/pull/5493)) by @mytharcher
## [v1.3.36-beta](https://github.com/nocobase/nocobase/compare/v1.3.35-beta...v1.3.36-beta) - 2024-10-22
### 🐛 修复
- **[数据表:树]** 修复继承的树表没有自动创建路径表的问题 ([#5486](https://github.com/nocobase/nocobase/pull/5486)) by @2013xile
- **[日历]** 当表格有数据时分页器应该显示 ([#5480](https://github.com/nocobase/nocobase/pull/5480)) by @katherinehhh
- **[文件管理器]** 修复由于上传规则 hook 改动导致文件无法上传的问题 ([#5460](https://github.com/nocobase/nocobase/pull/5460)) by @mytharcher
- **[数据表字段:公式]** 修复 多层子表格嵌套时,公式计算结果的错误 ([#5469](https://github.com/nocobase/nocobase/pull/5469)) by @gu-zhichao
## [v1.3.35-beta](https://github.com/nocobase/nocobase/compare/v1.3.34-beta...v1.3.35-beta) - 2024-10-21
### 🚀 优化
- **[工作流:邮件发送节点]** 为邮件节点的表单项增加占位提示内容 ([#5470](https://github.com/nocobase/nocobase/pull/5470)) by @mytharcher
## [v1.3.34-beta](https://github.com/nocobase/nocobase/compare/v1.3.33-beta...v1.3.34-beta) - 2024-10-21
### 🎉 新特性
- **[test]** 筛选表单中的关系字段支持配置是否多选 ([#5451](https://github.com/nocobase/nocobase/pull/5451)) by @zhangzhonghe
- **[client]** 添加一个名为“上级对象”的变量 ([#5449](https://github.com/nocobase/nocobase/pull/5449)) by @zhangzhonghe
参考文档:[上级对象](https://docs-cn.nocobase.com/handbook/ui/variables#%E4%B8%8A%E7%BA%A7%E5%AF%B9%E8%B1%A1)
### 🐛 修复
- **[client]**
- 修复 URL 查询参数变量不会被解析的问题 ([#5454](https://github.com/nocobase/nocobase/pull/5454)) by @zhangzhonghe
- 多层关系下的子表格中关系字段设置数据范围后,选择关系数据后其他行记录被清空 ([#5441](https://github.com/nocobase/nocobase/pull/5441)) by @katherinehhh
- 修复表格行选中时的背景颜色 ([#5445](https://github.com/nocobase/nocobase/pull/5445)) by @mytharcher
- **[区块:地图]** 表格中的地图字段不应该有缩放等级配置项 ([#5457](https://github.com/nocobase/nocobase/pull/5457)) by @katherinehhh
- **[文件管理器]** 屏蔽阅读模式下附件字段对存储规则不必要的查询 ([#5447](https://github.com/nocobase/nocobase/pull/5447)) by @mytharcher
- **[数据源:主数据库]** 修复由于更换组件导致的 E2E 测试不通过 ([#5437](https://github.com/nocobase/nocobase/pull/5437)) by @mytharcher
## [v1.3.33-beta](https://github.com/nocobase/nocobase/compare/v1.3.32-beta...v1.3.33-beta) - 2024-10-16
### 🚀 优化
- **[工作流]** 对更新数据节点的批量模式增加关于关系字段的提示 ([#5426](https://github.com/nocobase/nocobase/pull/5426)) by @mytharcher
### 🐛 修复
- **[client]**
- 修复个人资料配置弹窗被子页面遮挡住的问题 ([#5409](https://github.com/nocobase/nocobase/pull/5409)) by @zhangzhonghe
- 工作流节点变量不显示继承表字段 ([#5415](https://github.com/nocobase/nocobase/pull/5415)) by @chenos
- 使用筛选区块筛选表格数据时,清空筛选数据查询数据分页器没有跟着调整 ([#5411](https://github.com/nocobase/nocobase/pull/5411)) by @katherinehhh
- **[文件管理器]** 移除文件表选择存储空间时仅加载 20 个的限制 ([#5430](https://github.com/nocobase/nocobase/pull/5430)) by @mytharcher
- **[操作:复制记录]** 修复批量编辑弹窗不显示内容的问题 ([#5412](https://github.com/nocobase/nocobase/pull/5412)) by @zhangzhonghe
- **[数据可视化]** 修复图表筛选区块中不显示默认值的问题 ([#5405](https://github.com/nocobase/nocobase/pull/5405)) by @zhangzhonghe
## [v1.3.32-beta](https://github.com/nocobase/nocobase/compare/v1.3.31-beta...v1.3.32-beta) - 2024-10-13 ## [v1.3.32-beta](https://github.com/nocobase/nocobase/compare/v1.3.31-beta...v1.3.32-beta) - 2024-10-13
### 🐛 修复 ### 🐛 修复

View File

@ -41,7 +41,7 @@ RUN cd /app \
&& tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app . && tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app .
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

View File

@ -1,8 +1,8 @@
Updated Date: September 17, 2024 Updated Date: October 15, 2024
NocoBase License Agreement NocoBase License Agreement
NOCOBASE PTE. LTD.a Singaporean Exempt Private Company Limited by Shares with its principal place of business located at #03-01 ROBINSON 112 ("The Company") https://www.nocobase.com/ issues this License Agreement ("Agreement") to you. You, as an individual or a company ("User"), will be deemed to voluntarily accept all terms of this Agreement by using NocoBase (including but not limited to obtaining NocoBase source code or installation package in any form, installing and using NocoBase, purchasing NocoBase commercial license and services, purchasing NocoBase commercial plugins). If the User does not agree to any term of this Agreement, or cannot accurately understand our interpretation of the relevant terms, please stop using it immediately. NOCOBASE PTE. LTD.a Singaporean Exempt Private Company Limited by Shares with its principal place of business located at #03-01 ROBINSON 112 ("The Company") https://www.nocobase.com/ issues this License Agreement ("Agreement") to you. You, as an individual or a company ("the User"), will be deemed to voluntarily accept all terms of this Agreement by using NocoBase (including but not limited to obtaining NocoBase source code or installation package in any form, installing and using NocoBase, purchasing NocoBase commercial license and services, purchasing NocoBase commercial plugins). If the User does not agree to any term of this Agreement, or cannot accurately understand our interpretation of the relevant terms, please stop using it immediately.
This Agreement applies to any use, quotation, contract, invoice, and all software delivered by the Company. The User and the Company or NocoBase's agents can no longer sign a separate license agreement for the sale and delivery of the software. This Agreement applies to any use, quotation, contract, invoice, and all software delivered by the Company. The User and the Company or NocoBase's agents can no longer sign a separate license agreement for the sale and delivery of the software.
@ -14,15 +14,19 @@ The Company reserves the right to formulate and modify this Agreement from time
1.1 "Software" refers to the NocoBase kernel and plugins placed in the same code repository as the kernel, including their source code, installation packages, images, and all their modifications, updates, and upgrades. 1.1 "Software" refers to the NocoBase kernel and plugins placed in the same code repository as the kernel, including their source code, installation packages, images, and all their modifications, updates, and upgrades.
1.2 "Marketplace" refers to the marketplace provided by the Company for selling Software plugins and solutions. 1.2 "Community Edition" refers to the free version of the Software provided to the User through public channels.
1.3 "Commercial Plugin" refers to the paid plugins sold in the Marketplace. 1.3 "Commercial Edition" refers to the paid version of the Software purchased by the User from the Company or its agents, downloaded through exclusive channels, and includes additional benefits. It consists of three versions: Standard Edition, Professional Edition, and Enterprise Edition.
1.4 "Upper Layer Application" refers to a specific business use case application serving internal or external customers of the User, developed based on Software and Commercial Plugins, such as ERP/CRM. 1.4 "Marketplace" refers to the marketplace provided by the Company for selling Software plugins and solutions.
1.5 "Customer" refers to the clients who purchase the User's Upper Layer Application. 1.5 "Commercial Plugin" refers to the paid plugins sold in the Marketplace.
1.6 "Third-Party Open Source Software" refers to open source software provided with Software and Commercial Plugins. They are licensed through various published open source software licenses or copyright notices accompanying such software. 1.6 "Upper Layer Application" refers to a specific business use case application serving internal or external customers of the User, developed based on Software and Commercial Plugins, such as ERP/CRM.
1.7 "Customer" refers to the clients who purchase the User's Upper Layer Application.
1.8 "Third-Party Open Source Software" refers to open source software provided with Software and Commercial Plugins. They are licensed through various published open source software licenses or copyright notices accompanying such software.
=================================== ===================================
2. Intellectual Property Protection 2. Intellectual Property Protection
@ -34,7 +38,7 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr
3. Disclaimer 3. Disclaimer
============= =============
3.1 Users shall not use the Software and Commercial Plugins to engage in activities that violate local laws and regulations, religious beliefs. All legal responsibilities and legal consequences arising from the use of Software and Commercial Plugins shall be borne by the User. 3.1 The User shall not use the Software and Commercial Plugins to engage in activities that violate local laws and regulations, religious beliefs. All legal responsibilities and legal consequences arising from the use of Software and Commercial Plugins shall be borne by the User.
3.2 The Company shall not be liable for any direct, indirect, special, incidental, or consequential damages (including but not limited to loss of profits, business interruption, data loss, or business information disclosure) caused by the User's use of the Software and Commercial Plugins, even if it has been previously informed of the possibility of such damages. 3.2 The Company shall not be liable for any direct, indirect, special, incidental, or consequential damages (including but not limited to loss of profits, business interruption, data loss, or business information disclosure) caused by the User's use of the Software and Commercial Plugins, even if it has been previously informed of the possibility of such damages.
@ -44,7 +48,11 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr
4.1 The Software uses a dual license type of AGPL-3.0 Open Source License https://www.gnu.org/licenses/agpl-3..en.htm and Commercial License. 4.1 The Software uses a dual license type of AGPL-3.0 Open Source License https://www.gnu.org/licenses/agpl-3..en.htm and Commercial License.
4.2 Commercial Plugins use Commercial License. 4.2 The Community Edition uses the AGPL-3.0 Open Source License https://www.gnu.org/licenses/agpl-3.0.en.html.
4.3 The Commercial Edition (including Standard, Professional, and Enterprise Editions) uses the Commercial License.
4.4 Commercial Plugins use the Commercial Plugin License.
================================================ ================================================
5. Rights and Obligations of Open Source License 5. Rights and Obligations of Open Source License
@ -52,9 +60,9 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr
5.1 The Software can be used for commercial purposes. 5.1 The Software can be used for commercial purposes.
5.2 User can sell plugins developed for the Software in the Marketplace. 5.2 The User can sell plugins developed for the Software in the Marketplace.
5.3 Outside the Marketplace, changes and plugins to the Software developed by User or third parties, and third-party applications developed based on the Software must all be open-sourced under the AGPL-3.0 license. 5.3 Outside the Marketplace, changes and plugins to the Software developed by the User or third parties, and third-party applications developed based on the Software must all be open-sourced under the AGPL-3.0 license.
5.4 It is not allowed to remove or change the brand, name, link, version number, license, and other information about NocoBase on the Software interface, except for the main LOGO in the upper left corner of the page. 5.4 It is not allowed to remove or change the brand, name, link, version number, license, and other information about NocoBase on the Software interface, except for the main LOGO in the upper left corner of the page.
@ -68,11 +76,11 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr
6. Rights of Commercial License 6. Rights of Commercial License
=============================== ===============================
6.1 Obtain a permanent commercial license of the Software or Commercial Plugin. 6.1 Obtain a permanent commercial license of the Software.
6.2 Get 12 months of upgrade and exclusive technical support. 6.2 Get 12 months of upgrade and exclusive technical support.
6.3 The licensed Software and Commercial Plugins can be used for commercial purposes with no restrictions on the number of applications and users. 6.3 The licensed Software can be used for commercial purposes with no restrictions on the number of applications and users.
6.4 Changes and plugins to the Software, and applications integrated with the Software do not need to be open sourced. 6.4 Changes and plugins to the Software, and applications integrated with the Software do not need to be open sourced.
@ -80,7 +88,7 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr
6.6 Can sell plugins developed for Software in the Marketplace. 6.6 Can sell plugins developed for Software in the Marketplace.
6.7 Users with a Professional or Enterprise Edition license can sell Upper Layer Application to their clients. 6.7 The User with a Professional or Enterprise Edition License can sell Upper Layer Application to their clients.
6.8 Not restricted by the AGPL-3.0 agreement. 6.8 Not restricted by the AGPL-3.0 agreement.
@ -98,26 +106,54 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr
7.4 It is not allowed to provide any form of no-code, zero-code, low-code platform SaaS products to the public using the original or modified Software. 7.4 It is not allowed to provide any form of no-code, zero-code, low-code platform SaaS products to the public using the original or modified Software.
7.5 It is not allowed for Users with a standard license to sell Upper Layer Application to clients without a commercial license. 7.5 It is not allowed for the User with a Standard Edition license to sell Upper Layer Application to clients without a Commercial license.
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.6 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.7 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.8 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. ======================================
8. Rights of Commercial Plugin License
======================================
7.10 If there are other agreements in the contract for the above obligations, the contract agreement shall prevail. 8.1 Obtain a permanent Commercial Plugin License for the Commercial Plugin.
8.2 Receive 12 months of upgrades and exclusive technical support.
8.3 Can be used for commercial purposes without restrictions on the number of applications or users.
8.4 The User with a Professional or Enterprise Edition License can use the Commercial Plugin in Upper Layer Applications sold to their customers.
8.5 Not restricted by the AGPL-3.0 agreement.
8.6 If there are other agreements in the contract regarding the above rights, the contract agreement shall prevail.
===========================================
9. Obligations of Commercial Plugin License
===========================================
9.1 It is not allowed to remove or change any intellectual property statements about NocoBase and the plugin authors in the code.
9.2 It is not allowed to sell, transfer, lease, share, or gift the Commercial Plugin.
9.3 It is not allowed to use reverse engineering, decompilation, or other methods to attempt to discover the source code of Commercial Plugins without obtaining a source code license.
9.4 It is not allowed to disclose the source code of Commercial Plugins to any third party.
9.5 If there is any 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.
9.6 If there are other agreements in the contract regarding the above obligations, the contract agreement shall prevail.
============================================================= =============================================================
8. Legal Jurisdiction, Interpretation, and Dispute Resolution 10. Legal Jurisdiction, Interpretation, and Dispute Resolution
============================================================= =============================================================
8.1 Except for Mainland China, the interpretation, application, and all matters related to this agreement are subject to the jurisdiction of Singapore law. 10.1 Except for Mainland China, the interpretation, application, and all matters related to this agreement are subject to the jurisdiction of Singapore law.
8.2 Any dispute related to this Agreement should first be resolved through friendly negotiation. If the negotiation fails to resolve the dispute, the dispute should be submitted to the International Chamber of Commerce (ICC) for arbitration. The arbitration venue should be Singapore, conducted in English. 10.2 Any dispute related to this Agreement should first be resolved through friendly negotiation. If the negotiation fails to resolve the dispute, the dispute should be submitted to the International Chamber of Commerce (ICC) for arbitration. The arbitration venue should be Singapore, conducted in English.
8.3 All terms and conditions of this Agreement shall be deemed enforceable to the maximum extent permitted by applicable law. If any term of this Agreement is deemed invalid by any applicable law, the invalidity of that term does not affect the validity of any other term of this Agreement, and it should be deemed that the invalid term has been modified as much as possible to make it valid and enforceable, or if the term cannot be modified, it should be deemed to have been deleted from this Agreement. 10.3 All terms and conditions of this Agreement shall be deemed enforceable to the maximum extent permitted by applicable law. If any term of this Agreement is deemed invalid by any applicable law, the invalidity of that term does not affect the validity of any other term of this Agreement, and it should be deemed that the invalid term has been modified as much as possible to make it valid and enforceable, or if the term cannot be modified, it should be deemed to have been deleted from this Agreement.
8.4 The arbitration award is final, binding on both parties, and can be enforced in any court with jurisdiction. 10.4 The arbitration award is final, binding on both parties, and can be enforced in any court with jurisdiction.

View File

@ -7,9 +7,6 @@ https://github.com/nocobase/nocobase/assets/1267426/29623e45-9a48-4598-bb9e-9dd1
<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>
## 加入我们
我们正在招聘远程 **全栈开发工程师****测试工程师****技术培训与文档专家**。 欢迎对 NocoBase 有强烈兴趣的伙伴加入。[查看详情](https://www.nocobase.com/cn/recruitment)
## 最近重要更新 ## 最近重要更新
- [v1.3REST API 数据源、移动端 V2 和更多功能 - 2024/08/29](https://www.nocobase.com/cn/blog/nocobase-1-3) - [v1.3REST API 数据源、移动端 V2 和更多功能 - 2024/08/29](https://www.nocobase.com/cn/blog/nocobase-1-3)
- [v1.0.1-alpha.1:区块支持高度设置 - 2024/06/07](https://www.nocobase.com/cn/blog/release-v101-alpha1) - [v1.0.1-alpha.1:区块支持高度设置 - 2024/06/07](https://www.nocobase.com/cn/blog/release-v101-alpha1)
@ -78,3 +75,9 @@ NocoBase 支持三种安装方式:
- <a target="_blank" href="https://docs-cn.nocobase.com/welcome/getting-started/installation/git-clone">Git 源码安装</a> - <a target="_blank" href="https://docs-cn.nocobase.com/welcome/getting-started/installation/git-clone">Git 源码安装</a>
如果你想体验最新未发布版本,或者想参与贡献,需要在源码上进行修改、调试,建议选择这种安装方式,对开发技术水平要求较高,如果代码有更新,可以走 git 流程拉取最新代码。 如果你想体验最新未发布版本,或者想参与贡献,需要在源码上进行修改、调试,建议选择这种安装方式,对开发技术水平要求较高,如果代码有更新,可以走 git 流程拉取最新代码。
## 一键部署
通过云厂商一键部署 NocoBase并享受多种部署选项的灵活性
- [阿里云](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=NocoBase%20%E7%A4%BE%E5%8C%BA%E7%89%88)

View File

@ -24,6 +24,24 @@ services:
- "${DB_MYSQL_PORT}:3306" - "${DB_MYSQL_PORT}:3306"
networks: networks:
- nocobase - nocobase
kingbase:
image: registry.cn-shanghai.aliyuncs.com/nocobase/kingbase:v009r001c001b0030_single_x86
platform: linux/amd64
restart: always
privileged: true
networks:
- nocobase
ports:
- "${DB_KINGBASE_PORT}:54321"
environment:
ENABLE_CI: no
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
DB_MODE: pg
NEED_START: yes
command: ["/usr/sbin/init"]
volumes:
- ./storage/db/kingbase:/home/kingbase/userdata
postgres: postgres:
image: postgres:latest image: postgres:latest
restart: always restart: always

View File

@ -27,7 +27,7 @@ RUN ARCH= && dpkgArch="$(dpkg --print-architecture)" \
esac \ esac \
&& set -ex \ && set -ex \
# libatomic1 for arm # libatomic1 for arm
&& apt-get update && apt-get install -y nginx && 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 --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz COPY --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz

View File

@ -0,0 +1,43 @@
FROM node:18-bullseye-slim as builder
WORKDIR /app
RUN cd /app \
&& yarn config set network-timeout 600000 -g \
&& npx -y create-nocobase-app@next my-nocobase-app -a -e APP_ENV=production \
&& cd /app/my-nocobase-app \
&& yarn install --production
RUN cd /app \
&& rm -rf nocobase.tar.gz \
&& tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app .
FROM node:18-bullseye-slim
# COPY ./sources.list /etc/apt/sources.list
RUN ARCH= && dpkgArch="$(dpkg --print-architecture)" \
&& case "${dpkgArch##*-}" in \
amd64) ARCH='x64';; \
ppc64el) ARCH='ppc64le';; \
s390x) ARCH='s390x';; \
arm64) ARCH='arm64';; \
armhf) ARCH='armv7l';; \
i386) ARCH='x86';; \
*) echo "unsupported architecture"; exit 1 ;; \
esac \
&& set -ex \
# libatomic1 for arm
&& apt-get update && apt-get install -y nginx libaio1
RUN rm -rf /etc/nginx/sites-enabled/default
COPY --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz
WORKDIR /app/nocobase
COPY docker-entrypoint.sh /app/
# COPY docker-entrypoint.sh /usr/local/bin/
# ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 80/tcp
CMD ["/app/docker-entrypoint.sh"]

View File

@ -11,7 +11,8 @@ else
CONTENT="/node_modules CONTENT="/node_modules
/docker /docker
/docs /docs
/src" /src
/dist/node_modules/external-db-data-source/src"
fi fi
echo $CONTENT echo $CONTENT

View File

@ -1,5 +1,5 @@
{ {
"version": "1.4.0-alpha", "version": "1.4.0-alpha.11",
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"npmClientArgs": ["--ignore-engines"], "npmClientArgs": ["--ignore-engines"],

View File

@ -0,0 +1,30 @@
# NocoBase
<video width="100%" controls>
<source src="https://static-docs.nocobase.com/NocoBase0510.mp4" type="video/mp4">
</video>
## What is NocoBase
NocoBase is a scalability-first, open-source no-code development platform.
Instead of investing years of time and millions of dollars in research and development, deploy NocoBase in a few minutes and you'll have a private, controllable, and extremely scalable no-code development platform!
Homepage:
https://www.nocobase.com/
Online Demo:
https://demo.nocobase.com/new
Documents:
https://docs.nocobase.com/
Commericial license & plugins:
https://www.nocobase.com/en/commercial
License agreement:
https://www.nocobase.com/en/agreement
## Contact Us:
hello@nocobase.com

View File

@ -1,13 +1,13 @@
{ {
"name": "@nocobase/acl", "name": "@nocobase/acl",
"version": "1.4.0-alpha", "version": "1.4.0-alpha.11",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/resourcer": "1.4.0-alpha", "@nocobase/resourcer": "1.4.0-alpha.11",
"@nocobase/utils": "1.4.0-alpha", "@nocobase/utils": "1.4.0-alpha.11",
"minimatch": "^5.1.1" "minimatch": "^5.1.1"
}, },
"repository": { "repository": {

View File

@ -0,0 +1,30 @@
# NocoBase
<video width="100%" controls>
<source src="https://static-docs.nocobase.com/NocoBase0510.mp4" type="video/mp4">
</video>
## What is NocoBase
NocoBase is a scalability-first, open-source no-code development platform.
Instead of investing years of time and millions of dollars in research and development, deploy NocoBase in a few minutes and you'll have a private, controllable, and extremely scalable no-code development platform!
Homepage:
https://www.nocobase.com/
Online Demo:
https://demo.nocobase.com/new
Documents:
https://docs.nocobase.com/
Commericial license & plugins:
https://www.nocobase.com/en/commercial
License agreement:
https://www.nocobase.com/en/agreement
## Contact Us:
hello@nocobase.com

View File

@ -1,14 +1,14 @@
{ {
"name": "@nocobase/actions", "name": "@nocobase/actions",
"version": "1.4.0-alpha", "version": "1.4.0-alpha.11",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/cache": "1.4.0-alpha", "@nocobase/cache": "1.4.0-alpha.11",
"@nocobase/database": "1.4.0-alpha", "@nocobase/database": "1.4.0-alpha.11",
"@nocobase/resourcer": "1.4.0-alpha" "@nocobase/resourcer": "1.4.0-alpha.11"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -0,0 +1,30 @@
# NocoBase
<video width="100%" controls>
<source src="https://static-docs.nocobase.com/NocoBase0510.mp4" type="video/mp4">
</video>
## What is NocoBase
NocoBase is a scalability-first, open-source no-code development platform.
Instead of investing years of time and millions of dollars in research and development, deploy NocoBase in a few minutes and you'll have a private, controllable, and extremely scalable no-code development platform!
Homepage:
https://www.nocobase.com/
Online Demo:
https://demo.nocobase.com/new
Documents:
https://docs.nocobase.com/
Commericial license & plugins:
https://www.nocobase.com/en/commercial
License agreement:
https://www.nocobase.com/en/agreement
## Contact Us:
hello@nocobase.com

View File

@ -18,24 +18,26 @@ export default defineConfig({
devtool: process.env.NODE_ENV === 'development' ? 'source-map' : false, devtool: process.env.NODE_ENV === 'development' ? 'source-map' : false,
favicons: [`${appPublicPath}favicon/favicon.ico`], favicons: [`${appPublicPath}favicon/favicon.ico`],
metas: [{ name: 'viewport', content: 'initial-scale=0.1' }], metas: [{ name: 'viewport', content: 'initial-scale=0.1' }],
links: [ links: [{ rel: 'stylesheet', href: `${appPublicPath}global.css` }],
{ rel: 'stylesheet', href: `${appPublicPath}global.css` },
],
headScripts: [ headScripts: [
{ {
src: `${appPublicPath}browser-checker.js`, src: `${appPublicPath}browser-checker.js`,
}, },
{ {
content: isDevCmd ? '' : ` content: isDevCmd
? ''
: `
window['__webpack_public_path__'] = '{{env.APP_PUBLIC_PATH}}'; window['__webpack_public_path__'] = '{{env.APP_PUBLIC_PATH}}';
window['__nocobase_public_path__'] = '{{env.APP_PUBLIC_PATH}}'; window['__nocobase_public_path__'] = '{{env.APP_PUBLIC_PATH}}';
window['__nocobase_api_base_url__'] = '{{env.API_BASE_URL}}'; window['__nocobase_api_base_url__'] = '{{env.API_BASE_URL}}';
window['__nocobase_api_client_storage_prefix__'] = '{{env.API_CLIENT_STORAGE_PREFIX}}'; window['__nocobase_api_client_storage_prefix__'] = '{{env.API_CLIENT_STORAGE_PREFIX}}';
window['__nocobase_api_client_storage_type__'] = '{{env.API_CLIENT_STORAGE_TYPE}}';
window['__nocobase_ws_url__'] = '{{env.WS_URL}}'; window['__nocobase_ws_url__'] = '{{env.WS_URL}}';
window['__nocobase_ws_path__'] = '{{env.WS_PATH}}'; window['__nocobase_ws_path__'] = '{{env.WS_PATH}}';
`, `,
}, },
], ],
cacheDirectoryPath: process.env.APP_CLIENT_CACHE_DIR || `node_modules/.cache`,
outputPath: path.resolve(__dirname, '../dist/client'), outputPath: path.resolve(__dirname, '../dist/client'),
hash: true, hash: true,
alias: { alias: {
@ -62,8 +64,11 @@ export default defineConfig({
edge: 79, edge: 79,
safari: 12, safari: 12,
}, },
jsMinifierOptions: {
target: ['chrome80', 'es2020'],
},
codeSplitting: { codeSplitting: {
jsStrategy: 'depPerChunk' jsStrategy: 'depPerChunk',
}, },
chainWebpack(config, { env }) { chainWebpack(config, { env }) {
if (env === 'production') { if (env === 'production') {

View File

@ -13,6 +13,9 @@ import devDynamicImport from '../.plugins/index';
export const app = new Application({ export const app = new Application({
apiClient: { apiClient: {
storageType:
// @ts-ignore
window['__nocobase_api_client_storage_type__'] || process.env.API_CLIENT_STORAGE_TYPE || 'localStorage',
// @ts-ignore // @ts-ignore
storagePrefix: storagePrefix:
// @ts-ignore // @ts-ignore

View File

@ -1,17 +1,17 @@
{ {
"name": "@nocobase/app", "name": "@nocobase/app",
"version": "1.4.0-alpha", "version": "1.4.0-alpha.11",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/database": "1.4.0-alpha", "@nocobase/database": "1.4.0-alpha.11",
"@nocobase/preset-nocobase": "1.4.0-alpha", "@nocobase/preset-nocobase": "1.4.0-alpha.11",
"@nocobase/server": "1.4.0-alpha" "@nocobase/server": "1.4.0-alpha.11"
}, },
"devDependencies": { "devDependencies": {
"@nocobase/client": "1.4.0-alpha" "@nocobase/client": "1.4.0-alpha.11"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -7,15 +7,18 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { Gateway } from '@nocobase/server'; import { Gateway, runPluginStaticImports } from '@nocobase/server';
import { getConfig } from './config'; import { getConfig } from './config';
getConfig() async function initializeGateway() {
.then((config) => { await runPluginStaticImports();
return Gateway.getInstance().run({ const config = await getConfig();
await Gateway.getInstance().run({
mainAppOptions: config, mainAppOptions: config,
}); });
}) }
.catch((e) => {
// console.error(e); initializeGateway().catch((e) => {
console.error(e);
process.exit(1);
}); });

View File

@ -0,0 +1,30 @@
# NocoBase
<video width="100%" controls>
<source src="https://static-docs.nocobase.com/NocoBase0510.mp4" type="video/mp4">
</video>
## What is NocoBase
NocoBase is a scalability-first, open-source no-code development platform.
Instead of investing years of time and millions of dollars in research and development, deploy NocoBase in a few minutes and you'll have a private, controllable, and extremely scalable no-code development platform!
Homepage:
https://www.nocobase.com/
Online Demo:
https://demo.nocobase.com/new
Documents:
https://docs.nocobase.com/
Commericial license & plugins:
https://www.nocobase.com/en/commercial
License agreement:
https://www.nocobase.com/en/agreement
## Contact Us:
hello@nocobase.com

View File

@ -1,16 +1,16 @@
{ {
"name": "@nocobase/auth", "name": "@nocobase/auth",
"version": "1.4.0-alpha", "version": "1.4.0-alpha.11",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/actions": "1.4.0-alpha", "@nocobase/actions": "1.4.0-alpha.11",
"@nocobase/cache": "1.4.0-alpha", "@nocobase/cache": "1.4.0-alpha.11",
"@nocobase/database": "1.4.0-alpha", "@nocobase/database": "1.4.0-alpha.11",
"@nocobase/resourcer": "1.4.0-alpha", "@nocobase/resourcer": "1.4.0-alpha.11",
"@nocobase/utils": "1.4.0-alpha", "@nocobase/utils": "1.4.0-alpha.11",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"jsonwebtoken": "^8.5.1" "jsonwebtoken": "^8.5.1"
}, },

View File

@ -106,7 +106,9 @@ export class AuthManager {
* @description Auth middleware, used to check the authentication status. * @description Auth middleware, used to check the authentication status.
*/ */
middleware() { middleware() {
return async (ctx: Context & { auth: Auth }, next: Next) => { const self = this;
return async function AuthManagerMiddleware(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, { return ctx.throw(401, {
@ -115,7 +117,8 @@ export class AuthManager {
}); });
} }
const name = ctx.get(this.options.authKey) || this.options.default; const name = ctx.get(self.options.authKey) || self.options.default;
let authenticator: Auth; let authenticator: Auth;
try { try {
authenticator = await ctx.app.authManager.get(name, ctx); authenticator = await ctx.app.authManager.get(name, ctx);

30
packages/core/build/README.md Executable file → Normal file
View File

@ -0,0 +1,30 @@
# NocoBase
<video width="100%" controls>
<source src="https://static-docs.nocobase.com/NocoBase0510.mp4" type="video/mp4">
</video>
## What is NocoBase
NocoBase is a scalability-first, open-source no-code development platform.
Instead of investing years of time and millions of dollars in research and development, deploy NocoBase in a few minutes and you'll have a private, controllable, and extremely scalable no-code development platform!
Homepage:
https://www.nocobase.com/
Online Demo:
https://demo.nocobase.com/new
Documents:
https://docs.nocobase.com/
Commericial license & plugins:
https://www.nocobase.com/en/commercial
License agreement:
https://www.nocobase.com/en/agreement
## Contact Us:
hello@nocobase.com

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/build", "name": "@nocobase/build",
"version": "1.4.0-alpha", "version": "1.4.0-alpha.11",
"description": "Library build tool based on rollup.", "description": "Library build tool based on rollup.",
"main": "lib/index.js", "main": "lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
@ -14,16 +14,25 @@
"@babel/preset-env": "7.25.4", "@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",
"@rspack/core": "1.0.14",
"@svgr/webpack": "^8.1.0",
"@types/gulp": "^4.0.13", "@types/gulp": "^4.0.13",
"@types/lerna__package": "5.1.0", "@types/lerna__package": "5.1.0",
"@types/lerna__project": "5.1.0", "@types/lerna__project": "5.1.0",
"@types/tar": "^6.1.5", "@types/tar": "^6.1.5",
"@vercel/ncc": "0.36.1", "@vercel/ncc": "0.36.1",
"chalk": "2.4.2", "chalk": "2.4.2",
"css-loader": "^6.8.1",
"esbuild-register": "^3.4.2", "esbuild-register": "^3.4.2",
"fast-glob": "^3.3.1", "fast-glob": "^3.3.1",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-typescript": "6.0.0-alpha.1", "gulp-typescript": "6.0.0-alpha.1",
"less": "^4.1.3",
"less-loader": "11.1.0",
"postcss": "^8.4.29",
"postcss-loader": "^7.3.3",
"postcss-preset-env": "^9.1.2",
"style-loader": "^3.3.3",
"tar": "^6.2.0", "tar": "^6.2.0",
"tsup": "8.2.4", "tsup": "8.2.4",
"typescript": "5.1.3", "typescript": "5.1.3",

View File

@ -17,6 +17,7 @@ import { libInjectCss } from 'vite-plugin-lib-inject-css';
import { globExcludeFiles } from './constant'; import { globExcludeFiles } from './constant';
import { PkgLog, UserConfig, getEnvDefine } from './utils'; import { PkgLog, UserConfig, getEnvDefine } from './utils';
import { rspack } from '@rspack/core';
export async function buildClient(cwd: string, userConfig: UserConfig, sourcemap: boolean = false, log: PkgLog) { export async function buildClient(cwd: string, userConfig: UserConfig, sourcemap: boolean = false, log: PkgLog) {
log('build client'); log('build client');
@ -29,7 +30,7 @@ export async function buildClient(cwd: string, userConfig: UserConfig, sourcemap
return true; return true;
}; };
await buildClientEsm(cwd, userConfig, sourcemap, external, log); await buildClientEsm(cwd, userConfig, sourcemap, external, log);
await buildClientLib(cwd, userConfig, sourcemap, external, log); // await buildClientLib(cwd, userConfig, sourcemap, external, log);
await buildLocale(cwd, userConfig, log); await buildLocale(cwd, userConfig, log);
} }
@ -39,31 +40,170 @@ function buildClientEsm(cwd: string, userConfig: UserConfig, sourcemap: boolean,
log('build client esm'); log('build client esm');
const entry = path.join(cwd, 'src/index.ts').replaceAll(/\\/g, '/'); const entry = path.join(cwd, 'src/index.ts').replaceAll(/\\/g, '/');
const outDir = path.resolve(cwd, 'es'); const outDir = path.resolve(cwd, 'es');
return viteBuild(
userConfig.modifyViteConfig({ return rspack({
mode: process.env.NODE_ENV || 'production', entry: {
define: getEnvDefine(), index: entry,
build: {
minify: process.env.NODE_ENV === 'production',
outDir,
cssCodeSplit: true,
emptyOutDir: true,
sourcemap,
lib: {
entry,
formats: ['es'],
fileName: 'index',
}, },
target: ['es2015', 'edge88', 'firefox78', 'chrome87', 'safari14'], output: {
rollupOptions: { path: outDir,
cache: true, library: {
treeshake: true, type: 'module',
external, },
clean: true,
},
target: ['es2015', 'web'],
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
optimization: {
minimize: process.env.NODE_ENV === 'production',
moduleIds: 'deterministic',
sideEffects: true,
},
resolve: {
tsConfig: path.join(process.cwd(), 'tsconfig.json'),
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.less', '.css'],
},
module: {
rules: [
{
test: /\.less$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{ loader: require.resolve('less-loader') },
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: {
'postcss-preset-env': {
browsers: ['last 2 versions', '> 1%', 'cover 99.5%', 'not dead'],
},
autoprefixer: {},
}, },
}, },
plugins: [react(), libInjectCss()], },
}), },
); ],
type: 'javascript/auto',
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: {
'postcss-preset-env': {
browsers: ['last 2 versions', '> 1%', 'cover 99.5%', 'not dead'],
},
autoprefixer: {},
},
},
},
},
],
type: 'javascript/auto',
},
{
test: /\.(png|jpe?g|gif)$/i,
type: 'asset',
},
{
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack'],
},
{
test: /\.jsx$/,
exclude: /[\\/]node_modules[\\/]/,
loader: 'builtin:swc-loader',
options: {
sourceMap: true,
jsc: {
parser: {
syntax: 'ecmascript',
jsx: true,
},
target: 'es5',
},
},
},
{
test: /\.tsx$/,
exclude: /[\\/]node_modules[\\/]/,
loader: 'builtin:swc-loader',
options: {
sourceMap: true,
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
target: 'es5',
},
},
},
{
test: /\.ts$/,
exclude: /[\\/]node_modules[\\/]/,
loader: 'builtin:swc-loader',
options: {
sourceMap: true,
jsc: {
parser: {
syntax: 'typescript',
},
target: 'es5',
},
},
},
],
},
externals: [
function ({ request }, callback) {
if (external(request)) {
return callback(null, true);
}
callback();
}
],
plugins: [
new rspack.DefinePlugin(getEnvDefine()),
],
stats: 'errors-warnings',
});
// const entry = path.join(cwd, 'src/index.ts').replaceAll(/\\/g, '/');
// const outDir = path.resolve(cwd, 'es');
// return viteBuild(
// userConfig.modifyViteConfig({
// mode: process.env.NODE_ENV || 'production',
// define: getEnvDefine(),
// build: {
// minify: process.env.NODE_ENV === 'production',
// outDir,
// cssCodeSplit: true,
// emptyOutDir: true,
// sourcemap,
// lib: {
// entry,
// formats: ['es'],
// fileName: 'index',
// },
// target: ['es2015', 'edge88', 'firefox78', 'chrome87', 'safari14'],
// rollupOptions: {
// cache: true,
// treeshake: true,
// external,
// },
// },
// plugins: [react(), libInjectCss()],
// }),
// );
} }
async function buildClientLib( async function buildClientLib(

View File

@ -7,11 +7,11 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import path from 'path'; import path from 'path';
import { PkgLog, UserConfig, getEnvDefine } from './utils'; import { PkgLog, UserConfig, getEnvDefine } from './utils';
import { build as viteBuild } from 'vite'; import { build as viteBuild } from 'vite';
import fg from 'fast-glob'; import fg from 'fast-glob';
import { rspack } from '@rspack/core';
const clientExt = '.{ts,tsx,js,jsx}'; const clientExt = '.{ts,tsx,js,jsx}';
@ -45,7 +45,14 @@ export async function buildEsm(cwd: string, userConfig: UserConfig, sourcemap: b
} }
} }
function build(cwd: string, entry: string, outDir: string, userConfig: UserConfig, sourcemap: boolean = false, log: PkgLog) { function build(
cwd: string,
entry: string,
outDir: string,
userConfig: UserConfig,
sourcemap: boolean = false,
log: PkgLog,
) {
const cwdWin = cwd.replaceAll(/\\/g, '/'); const cwdWin = cwd.replaceAll(/\\/g, '/');
const cwdUnix = cwd.replaceAll(/\//g, '\\'); const cwdUnix = cwd.replaceAll(/\//g, '\\');
const external = function (id: string) { const external = function (id: string) {
@ -54,28 +61,158 @@ function build(cwd: string, entry: string, outDir: string, userConfig: UserConfi
} }
return true; return true;
}; };
return viteBuild(
userConfig.modifyViteConfig({ return rspack({
mode: process.env.NODE_ENV || 'production', entry: {
define: getEnvDefine(), index: entry,
build: { },
minify: false, output: {
outDir, path: outDir,
cssCodeSplit: true, library: {
emptyOutDir: true, type: 'module',
sourcemap, },
lib: { clean: true,
entry,
formats: ['es'],
fileName: 'index',
}, },
target: ['node16'], target: ['node16'],
rollupOptions: { mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
cache: true, resolve: {
treeshake: true, tsConfig: path.join(process.cwd(), 'tsconfig.json'),
external, extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.less', '.css'],
},
module: {
rules: [
{
test: /\.less$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{ loader: require.resolve('less-loader') },
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: {
'postcss-preset-env': {
browsers: ['last 2 versions', '> 1%', 'cover 99.5%', 'not dead'],
},
autoprefixer: {},
}, },
}, },
}), },
); },
],
type: 'javascript/auto',
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: {
'postcss-preset-env': {
browsers: ['last 2 versions', '> 1%', 'cover 99.5%', 'not dead'],
},
autoprefixer: {},
},
},
},
},
],
type: 'javascript/auto',
},
{
test: /\.(png|jpe?g|gif)$/i,
type: 'asset',
},
{
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack'],
},
{
test: /\.jsx$/,
exclude: /[\\/]node_modules[\\/]/,
loader: 'builtin:swc-loader',
options: {
sourceMap: true,
jsc: {
parser: {
syntax: 'ecmascript',
jsx: true,
},
target: 'es5',
},
},
},
{
test: /\.tsx$/,
exclude: /[\\/]node_modules[\\/]/,
loader: 'builtin:swc-loader',
options: {
sourceMap: true,
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
target: 'es5',
},
},
},
{
test: /\.ts$/,
exclude: /[\\/]node_modules[\\/]/,
loader: 'builtin:swc-loader',
options: {
sourceMap: true,
jsc: {
parser: {
syntax: 'typescript',
},
target: 'es5',
},
},
},
],
},
externals: [
function ({ request }, callback) {
if (external(request)) {
return callback(null, true);
}
callback();
},
],
plugins: [new rspack.DefinePlugin(getEnvDefine())],
stats: 'errors-warnings',
});
// return viteBuild(
// userConfig.modifyViteConfig({
// mode: process.env.NODE_ENV || 'production',
// define: getEnvDefine(),
// build: {
// minify: false,
// outDir,
// cssCodeSplit: true,
// emptyOutDir: true,
// sourcemap,
// lib: {
// entry,
// formats: ['es'],
// fileName: 'index',
// },
// target: ['node16'],
// rollupOptions: {
// cache: true,
// treeshake: true,
// external,
// },
// },
// }),
// );
} }

View File

@ -16,9 +16,10 @@ import path from 'path';
import { build as tsupBuild } from 'tsup'; import { build as tsupBuild } from 'tsup';
import { build as viteBuild } from 'vite'; import { build as viteBuild } from 'vite';
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
import { rspack } from '@rspack/core';
import { EsbuildSupportExts, globExcludeFiles } from './constant'; import { EsbuildSupportExts, globExcludeFiles } from './constant';
import { PkgLog, UserConfig, getEnvDefine, getPackageJson } from './utils'; import { PkgLog, UserConfig, getPackageJson } from './utils';
import { import {
buildCheck, buildCheck,
checkFileSize, checkFileSize,
@ -130,6 +131,7 @@ const external = [
'ahooks', 'ahooks',
'lodash', 'lodash',
'china-division', 'china-division',
'file-saver',
]; ];
const pluginPrefix = ( const pluginPrefix = (
process.env.PLUGIN_PACKAGE_PREFIX || '@nocobase/plugin-,@nocobase/preset-,@nocobase/plugin-pro-' process.env.PLUGIN_PACKAGE_PREFIX || '@nocobase/plugin-,@nocobase/preset-,@nocobase/plugin-pro-'
@ -158,7 +160,9 @@ export function deleteServerFiles(cwd: string, log: PkgLog) {
export function writeExternalPackageVersion(cwd: string, log: PkgLog) { export function writeExternalPackageVersion(cwd: string, log: PkgLog) {
log('write external version'); log('write external version');
const sourceFiles = fg.globSync(sourceGlobalFiles, { cwd, absolute: true }).map((item) => fs.readFileSync(item, 'utf-8')); const sourceFiles = fg
.globSync(sourceGlobalFiles, { cwd, absolute: true })
.map((item) => fs.readFileSync(item, 'utf-8'));
const sourcePackages = getSourcePackages(sourceFiles); const sourcePackages = getSourcePackages(sourceFiles);
const excludePackages = getExcludePackages(sourcePackages, external, pluginPrefix); const excludePackages = getExcludePackages(sourcePackages, external, pluginPrefix);
const data = excludePackages.reduce<Record<string, string>>((prev, packageName) => { const data = excludePackages.reduce<Record<string, string>>((prev, packageName) => {
@ -174,7 +178,9 @@ export function writeExternalPackageVersion(cwd: string, log: PkgLog) {
export async function buildServerDeps(cwd: string, serverFiles: string[], log: PkgLog) { export async function buildServerDeps(cwd: string, serverFiles: string[], log: PkgLog) {
log('build plugin server dependencies'); log('build plugin server dependencies');
const outDir = path.join(cwd, target_dir, 'node_modules'); const outDir = path.join(cwd, target_dir, 'node_modules');
const serverFileSource = serverFiles.filter(item => validExts.includes(path.extname(item))).map((item) => fs.readFileSync(item, 'utf-8')); const serverFileSource = serverFiles
.filter((item) => validExts.includes(path.extname(item)))
.map((item) => fs.readFileSync(item, 'utf-8'));
const sourcePackages = getSourcePackages(serverFileSource); const sourcePackages = getSourcePackages(serverFileSource);
const includePackages = getIncludePackages(sourcePackages, external, pluginPrefix); const includePackages = getIncludePackages(sourcePackages, external, pluginPrefix);
const excludePackages = getExcludePackages(sourcePackages, external, pluginPrefix); const excludePackages = getExcludePackages(sourcePackages, external, pluginPrefix);
@ -190,7 +196,9 @@ export async function buildServerDeps(cwd: string, serverFiles: string[], log: P
if (excludePackages.length) { if (excludePackages.length) {
tips.push(`These packages ${chalk.yellow(excludePackages.join(', '))} will be ${chalk.italic('exclude')}.`); tips.push(`These packages ${chalk.yellow(excludePackages.join(', '))} will be ${chalk.italic('exclude')}.`);
} }
tips.push(`For more information, please refer to: ${chalk.blue('https://docs.nocobase.com/development/deps')}.`); tips.push(
`For more information, please refer to: ${chalk.blue('https://docs.nocobase.com/development/others/deps')}.`,
);
log(tips.join(' ')); log(tips.join(' '));
if (!includePackages.length) return; if (!includePackages.length) return;
@ -268,14 +276,17 @@ export async function buildPluginServer(cwd: string, userConfig: UserConfig, sou
const packageJson = getPackageJson(cwd); const packageJson = getPackageJson(cwd);
const serverFiles = fg.globSync(serverGlobalFiles, { cwd, absolute: true }); const serverFiles = fg.globSync(serverGlobalFiles, { cwd, absolute: true });
buildCheck({ cwd, packageJson, entry: 'server', files: serverFiles, log }); buildCheck({ cwd, packageJson, entry: 'server', files: serverFiles, log });
const otherExts = Array.from(new Set(serverFiles.map((item) => path.extname(item)).filter((item) => !EsbuildSupportExts.includes(item)))); const otherExts = Array.from(
new Set(serverFiles.map((item) => path.extname(item)).filter((item) => !EsbuildSupportExts.includes(item))),
);
if (otherExts.length) { if (otherExts.length) {
log('%s will not be processed, only be copied to the dist directory.', chalk.yellow(otherExts.join(','))); log('%s will not be processed, only be copied to the dist directory.', chalk.yellow(otherExts.join(',')));
} }
deleteServerFiles(cwd, log); deleteServerFiles(cwd, log);
await tsupBuild(userConfig.modifyTsupConfig({ await tsupBuild(
userConfig.modifyTsupConfig({
entry: serverFiles, entry: serverFiles,
splitting: false, splitting: false,
clean: false, clean: false,
@ -291,7 +302,8 @@ export async function buildPluginServer(cwd: string, userConfig: UserConfig, sou
...otherExts.reduce((prev, cur) => ({ ...prev, [cur]: 'copy' }), {}), ...otherExts.reduce((prev, cur) => ({ ...prev, [cur]: 'copy' }), {}),
'.json': 'copy', '.json': 'copy',
}, },
})); }),
);
await buildServerDeps(cwd, serverFiles, log); await buildServerDeps(cwd, serverFiles, log);
} }
@ -316,46 +328,199 @@ export async function buildPluginClient(cwd: string, userConfig: UserConfig, sou
return prev; return prev;
}, {}); }, {});
const entry = fg.globSync('src/client/index.{ts,tsx,js,jsx}', { absolute: true, cwd }); const entry = fg.globSync('index.{ts,tsx,js,jsx}', { absolute: false, cwd: path.join(cwd, 'src/client') });
const outputFileName = 'index.js'; const outputFileName = 'index.js';
const compiler = rspack({
await viteBuild(userConfig.modifyViteConfig({ mode: 'production',
mode: process.env.NODE_ENV || 'production', // mode: "development",
define: getEnvDefine(), context: cwd,
logLevel: 'warn', entry: './src/client/' + entry[0],
build: { target: ['web', 'es5'],
minify: process.env.NODE_ENV === 'production',
outDir,
cssCodeSplit: false,
emptyOutDir: true,
sourcemap,
lib: {
entry,
formats: ['umd'],
name: packageJson.name,
fileName: () => outputFileName,
},
target: ['es2015', 'edge88', 'firefox78', 'chrome87', 'safari14'],
rollupOptions: {
cache: true,
external: [...Object.keys(globals), 'react', 'react/jsx-runtime'],
output: { output: {
exports: 'named', path: outDir,
globals: { filename: outputFileName,
react: 'React', publicPath: `/static/plugins/${packageJson.name}/dist/client/`,
'react/jsx-runtime': 'jsxRuntime', clean: true,
...globals, library: {
name: packageJson.name,
type: 'umd',
umdNamedDefine: true,
},
},
resolve: {
tsConfig: path.join(process.cwd(), 'tsconfig.json'),
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.less', '.css'],
},
module: {
rules: [
{
test: /\.less$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{ loader: require.resolve('less-loader') },
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: {
'postcss-preset-env': {
browsers: ['last 2 versions', '> 1%', 'cover 99.5%', 'not dead'],
},
autoprefixer: {},
}, },
}, },
}, },
}, },
],
type: 'javascript/auto',
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: {
'postcss-preset-env': {
browsers: ['last 2 versions', '> 1%', 'cover 99.5%', 'not dead'],
},
autoprefixer: {},
},
},
},
},
],
type: 'javascript/auto',
},
{
test: /\.(png|jpe?g|gif)$/i,
type: 'asset',
},
{
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack'],
},
{
test: /\.jsx$/,
exclude: /[\\/]node_modules[\\/]/,
loader: 'builtin:swc-loader',
options: {
sourceMap: true,
jsc: {
parser: {
syntax: 'ecmascript',
jsx: true,
},
target: 'es5',
},
},
},
{
test: /\.tsx$/,
exclude: /[\\/]node_modules[\\/]/,
loader: 'builtin:swc-loader',
options: {
sourceMap: true,
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
target: 'es5',
},
},
},
{
test: /\.ts$/,
exclude: /[\\/]node_modules[\\/]/,
loader: 'builtin:swc-loader',
options: {
sourceMap: true,
jsc: {
parser: {
syntax: 'typescript',
},
target: 'es5',
},
},
},
],
},
plugins: [ plugins: [
react(), new rspack.DefinePlugin({
cssInjectedByJsPlugin({ styleId: packageJson.name }), 'process.env.NODE_ENV': JSON.stringify('production'),
}),
], ],
})); node: {
global: true,
},
externals: {
react: 'React',
lodash: 'lodash',
// 'react/jsx-runtime': 'jsxRuntime',
...globals,
},
stats: 'errors-warnings',
});
checkFileSize(outDir, log); return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
const compilationErrors = stats?.compilation.errors;
const infos = stats.toString({
colors: true,
});
if (err || compilationErrors?.length) {
reject(err || infos);
return;
}
console.log(infos);
resolve(null);
});
});
// await viteBuild(userConfig.modifyViteConfig({
// mode: 'production',
// define: {
// 'process.env.NODE_ENV': JSON.stringify('production'),
// },
// logLevel: 'warn',
// build: {
// minify: true,
// outDir,
// cssCodeSplit: false,
// emptyOutDir: true,
// sourcemap,
// lib: {
// entry,
// formats: ['umd'],
// name: packageJson.name,
// fileName: () => outputFileName,
// },
// target: ['es2015', 'edge88', 'firefox78', 'chrome87', 'safari14'],
// rollupOptions: {
// cache: true,
// external: [...Object.keys(globals), 'react', 'react/jsx-runtime'],
// output: {
// exports: 'named',
// globals: {
// react: 'React',
// 'react/jsx-runtime': 'jsxRuntime',
// ...globals,
// },
// },
// },
// },
// plugins: [
// react(),
// cssInjectedByJsPlugin({ styleId: packageJson.name }),
// ],
// }));
// checkFileSize(outDir, log);
} }
export async function buildPlugin(cwd: string, userConfig: UserConfig, sourcemap: boolean, log: PkgLog) { export async function buildPlugin(cwd: string, userConfig: UserConfig, sourcemap: boolean, log: PkgLog) {

View File

@ -112,7 +112,7 @@ export function checkDependencies(packageJson: Record<string, any>, log: Log) {
chalk.yellow(packages.join(', ')), chalk.yellow(packages.join(', ')),
chalk.yellow('dependencies'), chalk.yellow('dependencies'),
chalk.yellow('devDependencies'), chalk.yellow('devDependencies'),
chalk.blue(chalk.blue('https://docs.nocobase.com/development/deps')), chalk.blue(chalk.blue('https://docs.nocobase.com/development/others/deps')),
); );
} }

30
packages/core/cache/README.md vendored Normal file
View File

@ -0,0 +1,30 @@
# NocoBase
<video width="100%" controls>
<source src="https://static-docs.nocobase.com/NocoBase0510.mp4" type="video/mp4">
</video>
## What is NocoBase
NocoBase is a scalability-first, open-source no-code development platform.
Instead of investing years of time and millions of dollars in research and development, deploy NocoBase in a few minutes and you'll have a private, controllable, and extremely scalable no-code development platform!
Homepage:
https://www.nocobase.com/
Online Demo:
https://demo.nocobase.com/new
Documents:
https://docs.nocobase.com/
Commericial license & plugins:
https://www.nocobase.com/en/commercial
License agreement:
https://www.nocobase.com/en/agreement
## Contact Us:
hello@nocobase.com

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/cache", "name": "@nocobase/cache",
"version": "1.4.0-alpha", "version": "1.4.0-alpha.11",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",

View File

@ -100,7 +100,7 @@ export class CacheManager {
async createCache(options: { name: string; prefix?: string; store?: string; [key: string]: any }) { async createCache(options: { name: string; prefix?: string; store?: string; [key: string]: any }) {
const { name, prefix, store = this.defaultStore, ...config } = options; const { name, prefix, store = this.defaultStore, ...config } = options;
if (!lodash.isEmpty(config)) { if (!lodash.isEmpty(config) || store === 'memory') {
const newStore = await this.createStore({ name, storeType: store, ...config }); const newStore = await this.createStore({ name, storeType: store, ...config });
return this.newCache({ name, prefix, store: newStore }); return this.newCache({ name, prefix, store: newStore });
} }

View File

@ -0,0 +1,30 @@
# NocoBase
<video width="100%" controls>
<source src="https://static-docs.nocobase.com/NocoBase0510.mp4" type="video/mp4">
</video>
## What is NocoBase
NocoBase is a scalability-first, open-source no-code development platform.
Instead of investing years of time and millions of dollars in research and development, deploy NocoBase in a few minutes and you'll have a private, controllable, and extremely scalable no-code development platform!
Homepage:
https://www.nocobase.com/
Online Demo:
https://demo.nocobase.com/new
Documents:
https://docs.nocobase.com/
Commericial license & plugins:
https://www.nocobase.com/en/commercial
License agreement:
https://www.nocobase.com/en/agreement
## Contact Us:
hello@nocobase.com

View File

@ -11,6 +11,14 @@ if (require('semver').satisfies(process.version, '<16')) {
process.exit(1); process.exit(1);
} }
if (__dirname.includes(' ')) {
console.error(chalk.red(`[nocobase cli]: PathError: Invalid path "${process.cwd()}"`));
console.error(
chalk.red('[nocobase cli]: PathError: The path cannot contain spaces. Please modify the path and try again.'),
);
process.exit(1);
}
// if (require('semver').satisfies(process.version, '>16') && !process.env.UNSET_NODE_OPTIONS) { // if (require('semver').satisfies(process.version, '>16') && !process.env.UNSET_NODE_OPTIONS) {
// if (process.env.NODE_OPTIONS) { // if (process.env.NODE_OPTIONS) {
// let opts = process.env.NODE_OPTIONS; // let opts = process.env.NODE_OPTIONS;

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/cli", "name": "@nocobase/cli",
"version": "1.4.0-alpha", "version": "1.4.0-alpha.11",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./src/index.js", "main": "./src/index.js",
@ -8,7 +8,7 @@
"nocobase": "./bin/index.js" "nocobase": "./bin/index.js"
}, },
"dependencies": { "dependencies": {
"@nocobase/app": "1.4.0-alpha", "@nocobase/app": "1.4.0-alpha.11",
"@types/fs-extra": "^11.0.1", "@types/fs-extra": "^11.0.1",
"@umijs/utils": "3.5.20", "@umijs/utils": "3.5.20",
"chalk": "^4.1.1", "chalk": "^4.1.1",
@ -25,7 +25,7 @@
"tsx": "^4.19.0" "tsx": "^4.19.0"
}, },
"devDependencies": { "devDependencies": {
"@nocobase/devtools": "1.4.0-alpha" "@nocobase/devtools": "1.4.0-alpha.11"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -38,16 +38,16 @@ module.exports = (cli) => {
depth: 1, // 只监听第一层目录 depth: 1, // 只监听第一层目录
}); });
await fs.promises.mkdir(path.dirname(process.env.WATCH_FILE), { recursive: true });
watcher watcher
.on('addDir', async (pathname) => { .on('addDir', async (pathname) => {
generatePlugins(); generatePlugins();
const file = path.resolve(process.cwd(), 'storage/app.watch.ts'); await fs.promises.writeFile(process.env.WATCH_FILE, `export const watchId = '${uid()}';`, 'utf-8');
await fs.promises.writeFile(file, `export const watchId = '${uid()}';`, 'utf-8');
}) })
.on('unlinkDir', async (pathname) => { .on('unlinkDir', async (pathname) => {
generatePlugins(); generatePlugins();
const file = path.resolve(process.cwd(), 'storage/app.watch.ts'); await fs.promises.writeFile(process.env.WATCH_FILE, `export const watchId = '${uid()}';`, 'utf-8');
await fs.promises.writeFile(file, `export const watchId = '${uid()}';`, 'utf-8');
}); });
promptForTs(); promptForTs();

View File

@ -18,9 +18,16 @@ function writeToExclude() {
const excludePath = resolve(process.cwd(), '.git', 'info', 'exclude'); const excludePath = resolve(process.cwd(), '.git', 'info', 'exclude');
const content = 'packages/pro-plugins/\n'; const content = 'packages/pro-plugins/\n';
const dirPath = dirname(excludePath); const dirPath = dirname(excludePath);
if (!existsSync(dirPath)) { if (!existsSync(dirPath)) {
try {
mkdirSync(dirPath, { recursive: true }); mkdirSync(dirPath, { recursive: true });
} catch (e) {
console.log(`${e.message}, ignore write to git exclude`);
return;
} }
}
let fileContent = ''; let fileContent = '';
if (existsSync(excludePath)) { if (existsSync(excludePath)) {
fileContent = readFileSync(excludePath, 'utf-8'); fileContent = readFileSync(excludePath, 'utf-8');

View File

@ -291,6 +291,7 @@ function buildIndexHtml(force = false) {
const data = fs.readFileSync(tpl, 'utf-8'); const data = fs.readFileSync(tpl, 'utf-8');
const replacedData = data const replacedData = data
.replace(/\{\{env.APP_PUBLIC_PATH\}\}/g, process.env.APP_PUBLIC_PATH) .replace(/\{\{env.APP_PUBLIC_PATH\}\}/g, process.env.APP_PUBLIC_PATH)
.replace(/\{\{env.API_CLIENT_STORAGE_TYPE\}\}/g, process.env.API_CLIENT_STORAGE_TYPE)
.replace(/\{\{env.API_CLIENT_STORAGE_PREFIX\}\}/g, process.env.API_CLIENT_STORAGE_PREFIX) .replace(/\{\{env.API_CLIENT_STORAGE_PREFIX\}\}/g, process.env.API_CLIENT_STORAGE_PREFIX)
.replace(/\{\{env.API_BASE_URL\}\}/g, process.env.API_BASE_URL || process.env.API_BASE_PATH) .replace(/\{\{env.API_BASE_URL\}\}/g, process.env.API_BASE_URL || process.env.API_BASE_PATH)
.replace(/\{\{env.WS_URL\}\}/g, process.env.WEBSOCKET_URL || '') .replace(/\{\{env.WS_URL\}\}/g, process.env.WEBSOCKET_URL || '')
@ -327,6 +328,7 @@ exports.initEnv = function initEnv() {
APP_PORT: 13000, APP_PORT: 13000,
API_BASE_PATH: '/api/', API_BASE_PATH: '/api/',
API_CLIENT_STORAGE_PREFIX: 'NOCOBASE_', API_CLIENT_STORAGE_PREFIX: 'NOCOBASE_',
API_CLIENT_STORAGE_TYPE: 'localStorage',
DB_DIALECT: 'sqlite', DB_DIALECT: 'sqlite',
DB_STORAGE: 'storage/db/nocobase.sqlite', DB_STORAGE: 'storage/db/nocobase.sqlite',
// DB_TIMEZONE: '+00:00', // DB_TIMEZONE: '+00:00',
@ -348,6 +350,7 @@ exports.initEnv = function initEnv() {
LOGGER_BASE_PATH: 'storage/logs', LOGGER_BASE_PATH: 'storage/logs',
APP_SERVER_BASE_URL: '', APP_SERVER_BASE_URL: '',
APP_PUBLIC_PATH: '/', APP_PUBLIC_PATH: '/',
WATCH_FILE: resolve(process.cwd(), 'storage/app.watch.ts'),
}; };
if ( if (

View File

@ -26,6 +26,9 @@ export default defineConfig({
{ type: 'component', dir: 'src/schema-component/antd' }, { type: 'component', dir: 'src/schema-component/antd' },
], ],
}, },
jsMinifierOptions: {
target: ['chrome80', 'es2020'],
},
locales: lang === 'zh-CN' ? [{ id: 'zh-CN', name: '中文' },] : [{ id: 'en-US', name: 'English' }], locales: lang === 'zh-CN' ? [{ id: 'zh-CN', name: '中文' },] : [{ id: 'en-US', name: 'English' }],
themeConfig: defineThemeConfig({ themeConfig: defineThemeConfig({
title: 'NocoBase', title: 'NocoBase',

View File

@ -0,0 +1,30 @@
# NocoBase
<video width="100%" controls>
<source src="https://static-docs.nocobase.com/NocoBase0510.mp4" type="video/mp4">
</video>
## What is NocoBase
NocoBase is a scalability-first, open-source no-code development platform.
Instead of investing years of time and millions of dollars in research and development, deploy NocoBase in a few minutes and you'll have a private, controllable, and extremely scalable no-code development platform!
Homepage:
https://www.nocobase.com/
Online Demo:
https://demo.nocobase.com/new
Documents:
https://docs.nocobase.com/
Commericial license & plugins:
https://www.nocobase.com/en/commercial
License agreement:
https://www.nocobase.com/en/agreement
## Contact Us:
hello@nocobase.com

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/client", "name": "@nocobase/client",
"version": "1.4.0-alpha", "version": "1.4.0-alpha.11",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "lib/index.js", "main": "lib/index.js",
"module": "es/index.mjs", "module": "es/index.mjs",
@ -11,7 +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", "@budibase/handlebars-helpers": "^0.14.0",
"@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",
@ -27,13 +27,13 @@
"@formily/reactive-react": "^2.2.27", "@formily/reactive-react": "^2.2.27",
"@formily/shared": "^2.2.27", "@formily/shared": "^2.2.27",
"@formily/validator": "^2.2.27", "@formily/validator": "^2.2.27",
"@nocobase/evaluators": "1.4.0-alpha", "@nocobase/evaluators": "1.4.0-alpha.11",
"@nocobase/sdk": "1.4.0-alpha", "@nocobase/sdk": "1.4.0-alpha.11",
"@nocobase/utils": "1.4.0-alpha", "@nocobase/utils": "1.4.0-alpha.11",
"ahooks": "^3.7.2", "ahooks": "^3.7.2",
"antd": "5.12.8", "antd": "5.12.8",
"antd-style": "3.4.5", "antd-style": "3.7.1",
"axios": "^0.26.1", "axios": "^1.7.0",
"bignumber.js": "^9.1.2", "bignumber.js": "^9.1.2",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"cronstrue": "^2.11.0", "cronstrue": "^2.11.0",

View File

@ -15,12 +15,11 @@ import { Navigate } from 'react-router-dom';
import { useAPIClient, useRequest } from '../api-client'; import { useAPIClient, useRequest } from '../api-client';
import { useAppSpin } from '../application/hooks/useAppSpin'; import { useAppSpin } from '../application/hooks/useAppSpin';
import { useBlockRequestContext } from '../block-provider/BlockProvider'; import { useBlockRequestContext } from '../block-provider/BlockProvider';
import { useCollectionManager_deprecated, useCollection_deprecated } from '../collection-manager';
import { useResourceActionContext } from '../collection-manager/ResourceActionProvider'; import { useResourceActionContext } from '../collection-manager/ResourceActionProvider';
import { CollectionNotAllowViewPlaceholder, useCollection, useCollectionManager } from '../data-source';
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';
@ -115,22 +114,32 @@ export const useACLRolesCheck = () => {
const dataSourceName = useDataSourceKey(); const dataSourceName = useDataSourceKey();
const { dataSources: dataSourcesAcl } = ctx?.data?.meta || {}; const { dataSources: dataSourcesAcl } = ctx?.data?.meta || {};
const data = { ...ctx?.data?.data, ...omit(dataSourcesAcl?.[dataSourceName], 'snippets') }; const data = { ...ctx?.data?.data, ...omit(dataSourcesAcl?.[dataSourceName], 'snippets') };
const getActionAlias = (actionPath: string) => { const getActionAlias = useCallback(
(actionPath: string) => {
const actionName = actionPath.split(':').pop(); const actionName = actionPath.split(':').pop();
return data?.actionAlias?.[actionName] || actionName; return data?.actionAlias?.[actionName] || actionName;
}; },
[data?.actionAlias],
);
return { return {
data, data,
getActionAlias, getActionAlias,
inResources: (resourceName: string) => { inResources: useCallback(
(resourceName: string) => {
return data?.resources?.includes?.(resourceName); return data?.resources?.includes?.(resourceName);
}, },
getResourceActionParams: (actionPath: string) => { [data?.resources],
),
getResourceActionParams: useCallback(
(actionPath: string) => {
const [resourceName] = actionPath.split(':'); const [resourceName] = actionPath.split(':');
const actionAlias = getActionAlias(actionPath); const actionAlias = getActionAlias(actionPath);
return data?.actions?.[`${resourceName}:${actionAlias}`] || data?.actions?.[actionPath]; return data?.actions?.[`${resourceName}:${actionAlias}`] || data?.actions?.[actionPath];
}, },
getStrategyActionParams: (actionPath: string) => { [data?.actions, getActionAlias],
),
getStrategyActionParams: useCallback(
(actionPath: string) => {
const actionAlias = getActionAlias(actionPath); const actionAlias = getActionAlias(actionPath);
const strategyAction = data?.strategy?.actions?.find((action) => { const strategyAction = data?.strategy?.actions?.find((action) => {
const [value] = action.split(':'); const [value] = action.split(':');
@ -138,6 +147,8 @@ export const useACLRolesCheck = () => {
}); });
return strategyAction ? {} : null; return strategyAction ? {} : null;
}, },
[data?.strategy?.actions, getActionAlias],
),
}; };
}; };
@ -179,19 +190,24 @@ const useResourceName = () => {
export function useACLRoleContext() { export function useACLRoleContext() {
const { data, getActionAlias, inResources, getResourceActionParams, getStrategyActionParams } = useACLRolesCheck(); const { data, getActionAlias, inResources, getResourceActionParams, getStrategyActionParams } = useACLRolesCheck();
const allowedActions = useAllowedActions(); const allowedActions = useAllowedActions();
const { getCollectionJoinField } = useCollectionManager_deprecated(); const cm = useCollectionManager();
const verifyScope = (actionName: string, recordPkValue: any) => { const verifyScope = useCallback(
(actionName: string, recordPkValue: any) => {
const actionAlias = getActionAlias(actionName); const actionAlias = getActionAlias(actionName);
if (!Array.isArray(allowedActions?.[actionAlias])) { if (!Array.isArray(allowedActions?.[actionAlias])) {
return null; return null;
} }
return allowedActions[actionAlias].includes(recordPkValue); return allowedActions[actionAlias].includes(recordPkValue);
}; },
[allowedActions, getActionAlias],
);
return { return {
...data, ...data,
parseAction: (actionPath: string, options: any = {}) => { parseAction: useCallback(
const [resourceName, actionName] = actionPath.split(':'); (actionPath: string, options: any = {}) => {
const targetResource = resourceName?.includes('.') && getCollectionJoinField(resourceName)?.target; const [resourceName, actionName] = actionPath?.split(':') || [];
const targetResource = resourceName?.includes('.') && cm.getCollectionField(resourceName)?.target;
if (!getIgnoreScope(options)) { if (!getIgnoreScope(options)) {
const r = verifyScope(actionName, options.recordPkValue); const r = verifyScope(actionName, options.recordPkValue);
if (r !== null) { if (r !== null) {
@ -209,6 +225,8 @@ export function useACLRoleContext() {
} }
return getStrategyActionParams(actionPath); return getStrategyActionParams(actionPath);
}, },
[cm, data?.allowAll, getResourceActionParams, getStrategyActionParams, inResources, verifyScope],
),
}; };
} }
@ -228,19 +246,29 @@ export const ACLCollectionProvider = (props) => {
const { allowAll: customAllowAll } = useACLCustomContext(); const { allowAll: customAllowAll } = useACLCustomContext();
const app = useApp(); const app = useApp();
const schema = useFieldSchema(); const schema = useFieldSchema();
if (allowAll || app.disableAcl || customAllowAll) {
return props.children;
}
let actionPath = schema?.['x-acl-action'] || props.actionPath; let actionPath = schema?.['x-acl-action'] || props.actionPath;
const resoureName = schema?.['x-decorator-props']?.['association'] || schema?.['x-decorator-props']?.['collection']; const resoureName = schema?.['x-decorator-props']?.['association'] || schema?.['x-decorator-props']?.['collection'];
// 兼容 undefined 的情况 // 兼容 undefined 的情况
if (actionPath === 'undefined:list' && resoureName && resoureName !== 'undefined') { if (actionPath === 'undefined:list' && resoureName && resoureName !== 'undefined') {
actionPath = `${resoureName}:list`; actionPath = `${resoureName}:list`;
} }
const params = useMemo(() => {
if (!actionPath) {
return null;
}
return parseAction(actionPath, { schema });
}, [parseAction, actionPath, schema]);
if (allowAll || app.disableAcl || customAllowAll) {
return props.children;
}
if (!actionPath) { if (!actionPath) {
return props.children; return props.children;
} }
const params = parseAction(actionPath, { schema });
if (!params) { if (!params) {
return <CollectionNotAllowViewPlaceholder />; return <CollectionNotAllowViewPlaceholder />;
} }
@ -254,39 +282,51 @@ export const useACLActionParamsContext = () => {
}; };
export const useRecordPkValue = () => { export const useRecordPkValue = () => {
const { getPrimaryKey } = useCollection_deprecated(); const collection = useCollection();
const record = useRecord(); const record = useRecord();
const primaryKey = getPrimaryKey();
if (!collection) {
return;
}
const primaryKey = collection.getPrimaryKey();
return record?.[primaryKey]; return record?.[primaryKey];
}; };
export const ACLActionProvider = (props) => { export const ACLActionProvider = (props) => {
const { template, writableView } = useCollection_deprecated(); const collection = useCollection();
const recordPkValue = useRecordPkValue(); const recordPkValue = useRecordPkValue();
const resource = useResourceName(); const resource = useResourceName();
const { parseAction } = useACLRoleContext(); const { parseAction } = useACLRoleContext();
const schema = useFieldSchema(); const schema = useFieldSchema();
let actionPath = schema['x-acl-action']; let actionPath = schema['x-acl-action'];
const editablePath = ['create', 'update', 'destroy', 'importXlsx']; const editablePath = ['create', 'update', 'destroy', 'importXlsx'];
if (!actionPath && resource && schema['x-action']) { if (!actionPath && resource && schema['x-action']) {
actionPath = `${resource}:${schema['x-action']}`; actionPath = `${resource}:${schema['x-action']}`;
} }
if (!actionPath?.includes(':')) { if (!actionPath?.includes(':')) {
actionPath = `${resource}:${actionPath}`; actionPath = `${resource}:${actionPath}`;
} }
const params = useMemo(
() => parseAction(actionPath, { schema, recordPkValue }),
[parseAction, actionPath, schema, recordPkValue],
);
if (!actionPath) { if (!actionPath) {
return <>{props.children}</>; return <>{props.children}</>;
} }
if (!resource) { if (!resource) {
return <>{props.children}</>; return <>{props.children}</>;
} }
const params = parseAction(actionPath, { schema, recordPkValue });
if (!params) { if (!params) {
return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>; return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
} }
//视图表无编辑权限时不显示 //视图表无编辑权限时不显示
if (editablePath.includes(actionPath) || editablePath.includes(actionPath?.split(':')[1])) { if (editablePath.includes(actionPath) || editablePath.includes(actionPath?.split(':')[1])) {
if (template !== 'view' || writableView) { if ((collection && collection.template !== 'view') || collection?.writableView) {
return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>; return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
} }
return null; return null;
@ -305,7 +345,7 @@ export const useACLFieldWhitelist = () => {
return { return {
whitelist, whitelist,
schemaInWhitelist: useCallback( schemaInWhitelist: useCallback(
(fieldSchema: Schema, isSkip?) => { (fieldSchema: Schema | any, isSkip?) => {
if (isSkip) { if (isSkip) {
return true; return true;
} }
@ -319,7 +359,8 @@ export const useACLFieldWhitelist = () => {
return true; return true;
} }
const [key1, key2] = fieldSchema['x-collection-field'].split('.'); const [key1, key2] = fieldSchema['x-collection-field'].split('.');
return whitelist?.includes(key2 || key1); const [associationField] = fieldSchema['name'].split('.');
return whitelist?.includes(associationField || key2 || key1);
}, },
[whitelist], [whitelist],
), ),

View File

@ -61,6 +61,18 @@ export class APIClient extends APIClientSDK {
/** 该值会在 AntdAppProvider 中被重新赋值 */ /** 该值会在 AntdAppProvider 中被重新赋值 */
notification: any = notification; notification: any = notification;
cloneInstance() {
const api = new APIClient(this.options);
api.options = this.options;
api.services = this.services;
api.storage = this.storage;
api.app = this.app;
api.auth = this.auth;
api.storagePrefix = this.storagePrefix;
api.notification = this.notification;
return api;
}
getHeaders() { getHeaders() {
const headers = super.getHeaders(); const headers = super.getHeaders();
const appName = this.app.getName(); const appName = this.app.getName();
@ -180,7 +192,8 @@ export class APIClient extends APIClientSDK {
} }
silent() { silent() {
this.silence = true; const api = this.cloneInstance();
return this; api.silence = true;
return api;
} }
} }

View File

@ -467,7 +467,7 @@ describe('Application', () => {
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
[ [
{ {
"label": "TestComponent", "label": "{{t("TestComponent")}}",
"useProps": [Function], "useProps": [Function],
"value": "TestComponent", "value": "TestComponent",
}, },

View File

@ -71,11 +71,13 @@ describe('useGetSchemaInitializerMenuItems', () => {
"key": "parent-2-item1-0", "key": "parent-2-item1-0",
"label": "item1", "label": "item1",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
{ {
"key": "parent-2-item2-1", "key": "parent-2-item2-1",
"label": "item2", "label": "item2",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
{ {
"associationField": "a.b", "associationField": "a.b",
@ -139,6 +141,7 @@ describe('useGetSchemaInitializerMenuItems', () => {
"key": "group-0-Item 1-0", "key": "group-0-Item 1-0",
"label": "Item 1", "label": "Item 1",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
], ],
"key": "group-0", "key": "group-0",
@ -151,6 +154,7 @@ describe('useGetSchemaInitializerMenuItems', () => {
"key": "parent-item-group-1-Item 1-0", "key": "parent-item-group-1-Item 1-0",
"label": "Item 1", "label": "Item 1",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
], ],
"key": "parent-item-group-1", "key": "parent-item-group-1",
@ -204,6 +208,7 @@ describe('useGetSchemaInitializerMenuItems', () => {
"key": "submenu-1-SubItem 1-0", "key": "submenu-1-SubItem 1-0",
"label": "SubItem 1", "label": "SubItem 1",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
], ],
"key": "submenu-1", "key": "submenu-1",
@ -215,6 +220,7 @@ describe('useGetSchemaInitializerMenuItems', () => {
"key": "submenu-2-SubItem 1-0", "key": "submenu-2-SubItem 1-0",
"label": "SubItem 1", "label": "SubItem 1",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
], ],
"key": "submenu-2", "key": "submenu-2",
@ -226,6 +232,7 @@ describe('useGetSchemaInitializerMenuItems', () => {
"key": "submenu-3-SubItem 1-0", "key": "submenu-3-SubItem 1-0",
"label": "SubItem 1", "label": "SubItem 1",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
], ],
"key": "submenu-3", "key": "submenu-3",
@ -289,11 +296,13 @@ describe('useSchemaInitializerMenuItems', () => {
"key": 1, "key": 1,
"label": "item1", "label": "item1",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
{ {
"key": 2, "key": 2,
"label": "item2", "label": "item2",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
] ]
`); `);

View File

@ -15,7 +15,7 @@ import { SchemaInitializerOptions } from '../types';
import { SchemaInitializerChildren } from './SchemaInitializerChildren'; import { SchemaInitializerChildren } from './SchemaInitializerChildren';
import { SchemaInitializerDivider } from './SchemaInitializerDivider'; import { SchemaInitializerDivider } from './SchemaInitializerDivider';
import { useSchemaInitializerStyles } from './style'; import { useSchemaInitializerStyles } from './style';
import { useMenuSearch } from './SchemaInitializerItemSearchFields';
export interface SchemaInitializerItemGroupProps { export interface SchemaInitializerItemGroupProps {
title: string; title: string;
children?: SchemaInitializerOptions['items']; children?: SchemaInitializerOptions['items'];
@ -44,7 +44,14 @@ export const SchemaInitializerItemGroup: FC<SchemaInitializerItemGroupProps> = (
/** /**
* @internal * @internal
*/ */
export const SchemaInitializerItemGroupInternal = () => { export const SchemaInitializerItemGroupInternal = () => {
const itemConfig = useSchemaInitializerItem<SchemaInitializerItemGroupProps>(); const itemConfig: any = useSchemaInitializerItem<SchemaInitializerItemGroupProps>();
const searchedChildren = useMenuSearch(itemConfig);
if (itemConfig.name !== 'displayFields') {
return <SchemaInitializerItemGroup {...itemConfig} />; return <SchemaInitializerItemGroup {...itemConfig} />;
}
/* eslint-disable react/no-children-prop */
return <SchemaInitializerItemGroup {...itemConfig} children={searchedChildren} />;
}; };

View File

@ -0,0 +1,183 @@
/**
* 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 { uid } from '@formily/shared';
import { Divider, Empty, Input, MenuProps } from 'antd';
import React, { useEffect, useMemo, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useCompile } from '../../../';
function getPrefixAndCompare(a, b) {
const prefixA = a.replace(/-displayCollectionFields$/, '');
const prefixB = b.replace(/-displayCollectionFields$/, '');
// 判断 a 是否包含 b如果包含则返回 false否则返回 true
return !prefixA.includes(prefixB);
}
export const SearchFields = ({ value: outValue, onChange, name }) => {
const { t } = useTranslation();
const [value, setValue] = useState<string>(outValue);
const inputRef = useRef<any>('');
// 生成唯一的ID用于区分不同层级的SearchFields
const uniqueId = useRef(`${name || Math.random().toString(10).substr(2, 9)}`);
useEffect(() => {
setValue(outValue);
}, [outValue]);
useEffect(() => {
const focusInput = () => {
if (
document.activeElement?.id !== inputRef.current.input.id &&
getPrefixAndCompare(document.activeElement?.id, inputRef.current.input.id)
) {
inputRef.current?.focus();
}
};
// 观察当前元素是否在视图中
const observer = new IntersectionObserver((entries) => {
if (entries.some((v) => v.isIntersecting)) {
focusInput();
}
});
if (inputRef.current?.input) {
inputRef.current.input.id = uniqueId.current; // 设置唯一ID
observer.observe(inputRef.current.input);
}
return () => {
observer.disconnect();
};
}, []);
const compositionRef = useRef<boolean>(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!compositionRef.current) {
onChange(e.target.value);
setValue(e.target.value);
}
};
const Composition = (e: React.CompositionEvent<HTMLInputElement> | any) => {
if (e.type === 'compositionend') {
compositionRef.current = false;
handleChange(e);
} else {
compositionRef.current = true;
}
};
return (
<div onClick={(e) => e.stopPropagation()}>
<Input
ref={inputRef}
allowClear
style={{ padding: '0 4px 6px 16px', boxShadow: 'none' }}
bordered={false}
placeholder={t('Search')}
defaultValue={value}
onClick={(e) => {
e.stopPropagation();
}}
onChange={handleChange}
onCompositionStart={Composition}
onCompositionEnd={Composition}
onCompositionUpdate={Composition}
/>
<Divider style={{ margin: 0 }} />
</div>
);
};
export const useMenuSearch = (props: { children: any[]; showType?: boolean; hideSearch?: boolean; name?: string }) => {
const { children, showType, hideSearch, name } = props;
const items = children?.concat?.() || [];
const [searchValue, setSearchValue] = useState(null);
const compile = useCompile();
// 处理搜索逻辑
const limitedSearchedItems = useMemo(() => {
if (!searchValue || searchValue === '') {
return items;
}
const lowerSearchValue = searchValue.toLocaleLowerCase();
return items.filter((item) => {
return (
(item.title || item.label) &&
String(compile(item.title || item.label))
.toLocaleLowerCase()
.includes(lowerSearchValue)
);
});
}, [searchValue, items]);
// 最终结果项
const resultItems = useMemo<MenuProps['items']>(() => {
const res = [];
if (!hideSearch && (items.length > 10 || searchValue)) {
res.push({
key: `search-${uid()}`,
Component: () => (
<SearchFields
name={name}
value={searchValue}
onChange={(val: string) => {
setSearchValue(val);
}}
/>
),
onClick({ domEvent }) {
domEvent.stopPropagation();
},
...(showType ? { isMenuType: true } : {}),
});
}
if (limitedSearchedItems.length > 0) {
res.push(...limitedSearchedItems);
} else {
res.push({
key: 'empty',
style: {
height: 150,
},
Component: () => (
<div onClick={(e) => e.stopPropagation()}>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
</div>
),
...(showType ? { isMenuType: true } : {}),
});
}
return res;
}, [hideSearch, limitedSearchedItems, searchValue, showType]);
const result = processedResult(resultItems, showType, hideSearch, name);
return children ? result : undefined;
};
// 处理嵌套子菜单
const processedResult = (resultItems, showType, hideSearch, name) => {
return resultItems.map((item: any) => {
if (['subMenu', 'itemGroup'].includes(item.type)) {
const childItems = useMenuSearch({
children: item.children,
showType,
hideSearch,
name: item.name,
});
return { ...item, children: childItems };
}
return item;
});
};

View File

@ -101,6 +101,7 @@ export function useGetSchemaInitializerMenuItems(onClick?: (args: any) => void)
onClick: handleClick, onClick: handleClick,
} }
: { : {
style: item.style,
key, key,
label, label,
onClick: handleClick, onClick: handleClick,

View File

@ -83,7 +83,7 @@ export const SchemaSettingsChildren: FC<SchemaSettingsChildrenProps> = (props) =
// 此时如果使用 item.name 作为 key会导致 React 认为其前后是同一个组件;因为 SchemaSettingsChild 的某些 hooks 是通过 props 传入的, // 此时如果使用 item.name 作为 key会导致 React 认为其前后是同一个组件;因为 SchemaSettingsChild 的某些 hooks 是通过 props 传入的,
// 两次渲染之间 props 可能发生变化,就可能报 hooks 调用顺序的错误。所以这里使用 fieldComponentName 和 item.name 拼成 // 两次渲染之间 props 可能发生变化,就可能报 hooks 调用顺序的错误。所以这里使用 fieldComponentName 和 item.name 拼成
// 一个不会重复的 key保证每次渲染都是新的组件。 // 一个不会重复的 key保证每次渲染都是新的组件。
const key = `${fieldComponentName ? fieldComponentName + '-' : ''}${item.name}`; const key = `${fieldComponentName ? fieldComponentName + '-' : ''}${item?.name}`;
return ( return (
<ErrorBoundary <ErrorBoundary
key={key} key={key}

View File

@ -41,6 +41,7 @@ import * as ReactRouter from 'react-router';
import * as ReactRouterDom from 'react-router-dom'; import * as ReactRouterDom from 'react-router-dom';
import jsxRuntime from 'react/jsx-runtime'; import jsxRuntime from 'react/jsx-runtime';
import * as nocobaseClient from '../../index'; import * as nocobaseClient from '../../index';
import * as FileSaver from 'file-saver';
import type { RequireJS } from './requirejs'; import type { RequireJS } from './requirejs';
@ -101,4 +102,5 @@ export function defineGlobalDeps(requirejs: RequireJS) {
requirejs.define('ahooks', () => ahooks); requirejs.define('ahooks', () => ahooks);
requirejs.define('@emotion/css', () => emotionCss); requirejs.define('@emotion/css', () => emotionCss);
requirejs.define('dayjs', () => dayjs); requirejs.define('dayjs', () => dayjs);
requirejs.define('file-saver', () => FileSaver);
} }

View File

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

View File

@ -24,6 +24,8 @@ import { useActionContext } from '../schema-component';
import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { BlockProvider, useBlockRequestContext } from './BlockProvider';
import { TemplateBlockProvider } from './TemplateBlockProvider'; import { TemplateBlockProvider } from './TemplateBlockProvider';
import { FormActiveFieldsProvider } from './hooks/useFormActiveFields'; import { FormActiveFieldsProvider } from './hooks/useFormActiveFields';
import { useDesignable } from '../schema-component';
import { useCollectionRecordData } from '../data-source';
export const FormBlockContext = createContext<{ export const FormBlockContext = createContext<{
form?: any; form?: any;
@ -123,6 +125,18 @@ export const useIsDetailBlock = () => {
export const FormBlockProvider = withDynamicSchemaProps((props) => { export const FormBlockProvider = withDynamicSchemaProps((props) => {
const parentRecordData = useCollectionParentRecordData(); const parentRecordData = useCollectionParentRecordData();
const { parentRecord } = props; const { parentRecord } = props;
const record = useCollectionRecordData();
const { association } = props;
const cm = useCollectionManager();
const { __collection } = record || {};
const { designable } = useDesignable();
const collection = props.collection || cm.getCollection(association).name;
if (!designable && __collection) {
if (__collection !== collection) {
return null;
}
}
return ( return (
<TemplateBlockProvider> <TemplateBlockProvider>

View File

@ -297,7 +297,7 @@ export const useTableSelectorProps = () => {
field.value = data; field.value = data;
field?.setInitialValue?.(data); field?.setInitialValue?.(data);
field.data = field.data || {}; field.data = field.data || {};
field.data.selectedRowKeys = ctx?.field?.data?.selectedRowKeys; field.data.selectedRowKeys = [];
field.componentProps.pagination = field.componentProps.pagination || {}; field.componentProps.pagination = field.componentProps.pagination || {};
field.componentProps.pagination.pageSize = ctx?.service?.data?.meta?.pageSize; field.componentProps.pagination.pageSize = ctx?.service?.data?.meta?.pageSize;
field.componentProps.pagination.total = ctx?.service?.data?.meta?.count; field.componentProps.pagination.total = ctx?.service?.data?.meta?.count;

View File

@ -0,0 +1,248 @@
/**
* 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 { Schema } from '@formily/json-schema';
import { describe, expect, it } from 'vitest';
import { getAppends } from '../../hooks/index';
describe('getAppends', () => {
const mockGetCollectionJoinField = (name: string) => {
const fields = {
'users.profile': {
type: 'hasOne',
target: 'profiles',
},
'users.posts': {
type: 'hasMany',
target: 'posts',
},
'posts.author': {
type: 'belongsTo',
target: 'users',
},
'users.roles': {
type: 'belongsToMany',
target: 'roles',
},
'users.categories': {
type: 'belongsToArray',
target: 'categories',
},
};
return fields[name];
};
const mockGetCollection = (name: string) => {
const collections = {
categories: {
template: 'tree',
},
users: {
template: 'general',
},
};
return collections[name];
};
const createSchema = (properties) => {
return new Schema({
properties,
});
};
it('should handle basic association fields', () => {
const schema = createSchema({
profile: {
'x-component': 'Input',
'x-collection-field': 'users.profile',
name: 'profile',
},
});
const updateAssociationValues = new Set<string>();
const appends = new Set<string>();
getAppends({
schema,
prefix: '',
updateAssociationValues,
appends,
getCollectionJoinField: mockGetCollectionJoinField,
getCollection: mockGetCollection,
dataSource: 'main',
});
expect(Array.from(appends)).toEqual(['profile']);
expect(Array.from(updateAssociationValues)).toEqual([]);
});
it('should handle tree collection fields', () => {
const schema = createSchema({
categories: {
'x-component': 'Input',
'x-collection-field': 'users.categories',
name: 'categories',
},
});
const updateAssociationValues = new Set<string>();
const appends = new Set<string>();
getAppends({
schema,
prefix: '',
updateAssociationValues,
appends,
getCollectionJoinField: mockGetCollectionJoinField,
getCollection: mockGetCollection,
dataSource: 'main',
});
expect(Array.from(appends)).toEqual(['categories', 'categories.parent(recursively=true)']);
});
it('should handle nested fields with sorting', () => {
const schema = createSchema({
posts: {
'x-component': 'Input',
'x-collection-field': 'users.posts',
'x-component-props': {
sortArr: 'createdAt',
},
name: 'posts',
},
});
const updateAssociationValues = new Set<string>();
const appends = new Set<string>();
getAppends({
schema,
prefix: '',
updateAssociationValues,
appends,
getCollectionJoinField: mockGetCollectionJoinField,
getCollection: mockGetCollection,
dataSource: 'main',
});
expect(Array.from(appends)).toEqual(['posts(sort=createdAt)']);
});
it('should handle nested SubTable mode', () => {
const schema = createSchema({
posts: {
'x-component': 'Input',
'x-collection-field': 'users.posts',
'x-component-props': {
mode: 'SubTable',
},
name: 'posts',
properties: {
author: {
'x-component': 'Input',
'x-collection-field': 'posts.author',
name: 'author',
},
},
},
});
const updateAssociationValues = new Set<string>();
const appends = new Set<string>();
getAppends({
schema,
prefix: '',
updateAssociationValues,
appends,
getCollectionJoinField: mockGetCollectionJoinField,
getCollection: mockGetCollection,
dataSource: 'main',
});
expect(Array.from(appends)).toEqual(['posts', 'posts.author']);
expect(Array.from(updateAssociationValues)).toEqual(['posts']);
});
it('should ignore TableField components', () => {
const schema = createSchema({
posts: {
'x-component': 'TableField',
'x-collection-field': 'users.posts',
name: 'posts',
},
});
const updateAssociationValues = new Set<string>();
const appends = new Set<string>();
getAppends({
schema,
prefix: '',
updateAssociationValues,
appends,
getCollectionJoinField: mockGetCollectionJoinField,
getCollection: mockGetCollection,
dataSource: 'main',
});
expect(Array.from(appends)).toEqual([]);
expect(Array.from(updateAssociationValues)).toEqual([]);
});
it('should ignore Kanban.CardViewer components', () => {
const schema = createSchema({
cardViewer: {
'x-component': 'Kanban.CardViewer',
name: 'cardViewer',
properties: {
drawer: {
name: 'drawer',
type: 'void',
properties: {
grid: {
name: 'grid',
type: 'void',
properties: {
field1: {
'x-component': 'Input',
'x-collection-field': 'users.posts',
name: 'field1',
},
field2: {
'x-component': 'Input',
'x-collection-field': 'posts.author',
name: 'field2',
},
},
},
},
},
},
},
});
const updateAssociationValues = new Set<string>();
const appends = new Set<string>();
getAppends({
schema,
prefix: '',
updateAssociationValues,
appends,
getCollectionJoinField: mockGetCollectionJoinField,
getCollection: mockGetCollection,
dataSource: 'main',
});
expect(Array.from(appends)).toEqual([]);
expect(Array.from(updateAssociationValues)).toEqual([]);
});
});

View File

@ -35,7 +35,7 @@ import {
import { useAPIClient, useRequest } from '../../api-client'; import { useAPIClient, useRequest } from '../../api-client';
import { useNavigateNoUpdate } from '../../application/CustomRouterContextProvider'; import { useNavigateNoUpdate } from '../../application/CustomRouterContextProvider';
import { useFormBlockContext } from '../../block-provider/FormBlockProvider'; import { useFormBlockContext } from '../../block-provider/FormBlockProvider';
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../collection-manager'; import { CollectionOptions, useCollectionManager_deprecated, useCollection_deprecated } from '../../collection-manager';
import { DataBlock, useFilterBlock } from '../../filter-provider/FilterProvider'; import { DataBlock, useFilterBlock } from '../../filter-provider/FilterProvider';
import { mergeFilter, transformToFilter } from '../../filter-provider/utils'; import { mergeFilter, transformToFilter } from '../../filter-provider/utils';
import { useTreeParentRecord } from '../../modules/blocks/data-blocks/table/TreeRecordProvider'; import { useTreeParentRecord } from '../../modules/blocks/data-blocks/table/TreeRecordProvider';
@ -1284,7 +1284,7 @@ export const useOptionalFieldList = () => {
const isOptionalField = (field) => { const isOptionalField = (field) => {
const optionalInterfaces = ['select', 'multipleSelect', 'checkbox', 'checkboxGroup', 'chinaRegion']; const optionalInterfaces = ['select', 'multipleSelect', 'checkbox', 'checkboxGroup', 'chinaRegion'];
return optionalInterfaces.includes(field.interface) && field.uiSchema.enum; return optionalInterfaces.includes(field?.interface) && field?.uiSchema?.enum;
}; };
export const useAssociationFilterBlockProps = () => { export const useAssociationFilterBlockProps = () => {
@ -1329,7 +1329,7 @@ export const useAssociationFilterBlockProps = () => {
useEffect(() => { useEffect(() => {
// 由于选项字段不需要触发当前请求,所以请求单独在关系字段的时候触发 // 由于选项字段不需要触发当前请求,所以请求单独在关系字段的时候触发
if (!isOptionalField(collectionField) && parseVariableLoading === false) { if (collectionField && !isOptionalField(collectionField) && parseVariableLoading === false) {
run(); run();
} }
@ -1494,14 +1494,25 @@ export function getAssociationPath(str) {
return str; return str;
} }
export const useAssociationNames = (dataSource?: string) => { export const getAppends = ({
let updateAssociationValues = new Set([]); schema,
let appends = new Set([]); prefix: defaultPrefix,
const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated(dataSource); updateAssociationValues,
const fieldSchema = useFieldSchema(); appends,
const _getAssociationAppends = (schema, str) => { getCollectionJoinField,
getCollection,
dataSource,
}: {
schema: any;
prefix: string;
updateAssociationValues: Set<string>;
appends: Set<string>;
getCollectionJoinField: (name: string, dataSource: string) => any;
getCollection: (name: any, customDataSource?: string) => CollectionOptions;
dataSource: string;
}) => {
schema.reduceProperties((pre, s) => { schema.reduceProperties((pre, s) => {
const prefix = pre || str; const prefix = pre || defaultPrefix;
const collectionField = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field'], dataSource); const collectionField = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field'], dataSource);
const isAssociationSubfield = s.name.includes('.'); const isAssociationSubfield = s.name.includes('.');
const isAssociationField = const isAssociationField =
@ -1534,8 +1545,10 @@ export const useAssociationNames = (dataSource?: string) => {
collectAppends(condition); collectAppends(condition);
}); });
} }
const isTreeCollection = const isTreeCollection =
isAssociationField && getCollection(collectionField.target, dataSource)?.template === 'tree'; isAssociationField && getCollection(collectionField.target, dataSource)?.template === 'tree';
if (collectionField && (isAssociationField || isAssociationSubfield) && s['x-component'] !== 'TableField') { if (collectionField && (isAssociationField || isAssociationSubfield) && s['x-component'] !== 'TableField') {
const fieldPath = !isAssociationField && isAssociationSubfield ? getAssociationPath(s.name) : s.name; const fieldPath = !isAssociationField && isAssociationSubfield ? getAssociationPath(s.name) : s.name;
const path = prefix === '' || !prefix ? fieldPath : prefix + '.' + fieldPath; const path = prefix === '' || !prefix ? fieldPath : prefix + '.' + fieldPath;
@ -1550,10 +1563,18 @@ export const useAssociationNames = (dataSource?: string) => {
appends.add(path); appends.add(path);
} }
} }
if (['Nester', 'SubTable', 'PopoverNester'].includes(s['x-component-props']?.mode)) { if (isSubMode(s)) {
updateAssociationValues.add(path); updateAssociationValues.add(path);
const bufPrefix = prefix && prefix !== '' ? prefix + '.' + s.name : s.name; const bufPrefix = prefix && prefix !== '' ? prefix + '.' + s.name : s.name;
_getAssociationAppends(s, bufPrefix); getAppends({
schema: s,
prefix: bufPrefix,
updateAssociationValues,
appends,
getCollectionJoinField,
getCollection,
dataSource,
});
} }
} else if ( } else if (
![ ![
@ -1567,17 +1588,43 @@ export const useAssociationNames = (dataSource?: string) => {
'AssociationField.Selector', 'AssociationField.Selector',
'AssociationField.AddNewer', 'AssociationField.AddNewer',
'TableField', 'TableField',
'Kanban.CardViewer',
].includes(s['x-component']) ].includes(s['x-component'])
) { ) {
_getAssociationAppends(s, str); getAppends({
schema: s,
prefix: defaultPrefix,
updateAssociationValues,
appends,
getCollectionJoinField,
getCollection,
dataSource,
});
} }
}, str); }, defaultPrefix);
}; };
export const useAssociationNames = (dataSource?: string) => {
const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated(dataSource);
const fieldSchema = useFieldSchema();
const getAssociationAppends = () => { const getAssociationAppends = () => {
updateAssociationValues = new Set([]); const updateAssociationValues = new Set([]);
appends = new Set([]); let appends = new Set([]);
_getAssociationAppends(fieldSchema, '');
getAppends({
schema: fieldSchema,
prefix: '',
updateAssociationValues,
appends,
getCollectionJoinField,
getCollection,
dataSource,
});
appends = fillParentFields(appends); appends = fillParentFields(appends);
console.log('appends', appends);
return { appends: [...appends], updateAssociationValues: [...updateAssociationValues] }; return { appends: [...appends], updateAssociationValues: [...updateAssociationValues] };
}; };

View File

@ -35,7 +35,7 @@ export const getPageSchema = (schema) => {
return getPageSchema(schema.parent); return getPageSchema(schema.parent);
}; };
const getCardItemSchema = (schema) => { export const getCardItemSchema = (schema) => {
if (!schema) return null; if (!schema) return null;
if (['BlockItem', 'CardItem'].includes(schema['x-component'])) { if (['BlockItem', 'CardItem'].includes(schema['x-component'])) {
return schema; return schema;

View File

@ -28,16 +28,7 @@ export const CollectionManagerProvider_deprecated: React.FC<CollectionManagerOpt
); );
}; };
const coptions = {
url: 'collectionCategories:list',
params: {
paginate: false,
sort: ['sort'],
},
};
export const RemoteCollectionManagerProvider = (props: any) => { export const RemoteCollectionManagerProvider = (props: any) => {
const api = useAPIClient();
const dm = useDataSourceManager(); const dm = useDataSourceManager();
const { refreshCH } = useCollectionHistory(); const { refreshCH } = useCollectionHistory();
@ -46,26 +37,13 @@ export const RemoteCollectionManagerProvider = (props: any) => {
}>(() => { }>(() => {
return dm.reload().then(refreshCH); return dm.reload().then(refreshCH);
}); });
const result = useRequest<{
data: any;
}>(coptions);
const { render } = useAppSpin(); const { render } = useAppSpin();
const refreshCategory = useCallback(async () => {
const { data } = await api.request(coptions);
result.mutate(data);
return data?.data || [];
}, [result]);
if (service.loading) { if (service.loading) {
return render(); return render();
} }
return ( return <CollectionManagerProvider_deprecated {...props}></CollectionManagerProvider_deprecated>;
<CollectionCategoriesProvider service={result} refreshCategory={refreshCategory}>
<CollectionManagerProvider_deprecated {...props}></CollectionManagerProvider_deprecated>
</CollectionCategoriesProvider>
);
}; };
export const CollectionCategoriesProvider = (props) => { export const CollectionCategoriesProvider = (props) => {

View File

@ -18,7 +18,9 @@ import {
ColorFieldInterface, ColorFieldInterface,
CreatedAtFieldInterface, CreatedAtFieldInterface,
CreatedByFieldInterface, CreatedByFieldInterface,
DateFieldInterface,
DatetimeFieldInterface, DatetimeFieldInterface,
DatetimeNoTzFieldInterface,
EmailFieldInterface, EmailFieldInterface,
IconFieldInterface, IconFieldInterface,
IdFieldInterface, IdFieldInterface,
@ -30,29 +32,28 @@ import {
M2OFieldInterface, M2OFieldInterface,
MarkdownFieldInterface, MarkdownFieldInterface,
MultipleSelectFieldInterface, MultipleSelectFieldInterface,
NanoidFieldInterface,
NumberFieldInterface, NumberFieldInterface,
O2MFieldInterface, O2MFieldInterface,
O2OFieldInterface, O2OFieldInterface,
OHOFieldInterface,
OBOFieldInterface, OBOFieldInterface,
OHOFieldInterface,
PasswordFieldInterface, PasswordFieldInterface,
PercentFieldInterface, PercentFieldInterface,
PhoneFieldInterface, PhoneFieldInterface,
RadioGroupFieldInterface, RadioGroupFieldInterface,
RichTextFieldInterface, RichTextFieldInterface,
SelectFieldInterface, SelectFieldInterface,
SortFieldInterface,
SubTableFieldInterface, SubTableFieldInterface,
TableoidFieldInterface, TableoidFieldInterface,
TextareaFieldInterface, TextareaFieldInterface,
TimeFieldInterface, TimeFieldInterface,
UnixTimestampFieldInterface,
UpdatedAtFieldInterface, UpdatedAtFieldInterface,
UpdatedByFieldInterface, UpdatedByFieldInterface,
UrlFieldInterface, UrlFieldInterface,
UUIDFieldInterface, UUIDFieldInterface,
NanoidFieldInterface,
UnixTimestampFieldInterface,
DateFieldInterface,
DatetimeNoTzFieldInterface,
} from './interfaces'; } from './interfaces';
import { import {
GeneralCollectionTemplate, GeneralCollectionTemplate,
@ -67,17 +68,11 @@ class MainDataSource extends DataSource {
async getDataSource() { async getDataSource() {
const service = await this.app.apiClient.request({ const service = await this.app.apiClient.request({
resource: 'collections', resource: 'collections',
action: 'list', action: 'listMeta',
params: {
paginate: false,
appends: ['fields', 'category'],
filter: {
// inherit: false,
},
sort: ['sort'],
},
}); });
const collections = service?.data?.data || []; const collections = service?.data?.data || [];
return { return {
collections, collections,
}; };

View File

@ -31,7 +31,7 @@ export class IdFieldInterface extends CollectionFieldInterface {
'x-read-pretty': true, 'x-read-pretty': true,
}, },
}; };
availableTypes = ['bigInt', 'integer', 'string']; availableTypes = ['bigInt', 'integer'];
properties = { properties = {
'uiSchema.title': { 'uiSchema.title': {
type: 'string', type: 'string',

View File

@ -47,3 +47,5 @@ export * from './nanoid';
export * from './unixTimestamp'; export * from './unixTimestamp';
export * from './dateOnly'; export * from './dateOnly';
export * from './datetimeNoTz'; export * from './datetimeNoTz';
export { getUniqueKeyFromCollection } from './utils';

View File

@ -24,7 +24,7 @@ export class NanoidFieldInterface extends CollectionFieldInterface {
'x-component': 'NanoIDInput', 'x-component': 'NanoIDInput',
}, },
}; };
availableTypes = ['string', 'nanoid']; availableTypes = ['nanoid'];
properties = { properties = {
'uiSchema.title': { 'uiSchema.title': {
type: 'string', type: 'string',

View File

@ -47,6 +47,9 @@ export class NumberFieldInterface extends CollectionFieldInterface {
{ value: '0.001', label: '1.000' }, { value: '0.001', label: '1.000' },
{ value: '0.0001', label: '1.0000' }, { value: '0.0001', label: '1.0000' },
{ value: '0.00001', label: '1.00000' }, { value: '0.00001', label: '1.00000' },
{ value: '0.000001', label: '1.000000' },
{ value: '0.0000001', label: '1.0000000' },
{ value: '0.00000001', label: '1.00000000' },
], ],
}, },
}; };

View File

@ -117,7 +117,7 @@ export class O2MFieldInterface extends CollectionFieldInterface {
type: 'string', type: 'string',
title: '{{t("Target collection")}}', title: '{{t("Target collection")}}',
required: true, required: true,
'x-reactions': ['{{useAsyncDataSource(loadCollections, ["file"])}}'], 'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'],
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Select', 'x-component': 'Select',
'x-disabled': '{{ !createOnly }}', 'x-disabled': '{{ !createOnly }}',

View File

@ -118,7 +118,7 @@ export class O2OFieldInterface extends CollectionFieldInterface {
type: 'string', type: 'string',
title: '{{t("Target collection")}}', title: '{{t("Target collection")}}',
required: true, required: true,
'x-reactions': ['{{useAsyncDataSource(loadCollections, ["file"])}}'], 'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'],
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Select', 'x-component': 'Select',
'x-disabled': '{{ !createOnly }}', 'x-disabled': '{{ !createOnly }}',
@ -306,7 +306,7 @@ export class OHOFieldInterface extends CollectionFieldInterface {
type: 'string', type: 'string',
title: '{{t("Target collection")}}', title: '{{t("Target collection")}}',
required: true, required: true,
'x-reactions': ['{{useAsyncDataSource(loadCollections, ["file"])}}'], 'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'],
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Select', 'x-component': 'Select',
'x-disabled': '{{ !createOnly }}', 'x-disabled': '{{ !createOnly }}',

View File

@ -147,7 +147,6 @@ export const reverseFieldProperties: Record<string, ISchema> = {
reverse: { reverse: {
type: 'void', type: 'void',
'x-component': 'div', 'x-component': 'div',
'x-hidden': '{{ !showReverseFieldConfig }}',
properties: { properties: {
autoCreateReverseField: { autoCreateReverseField: {
type: 'boolean', type: 'boolean',
@ -198,6 +197,12 @@ export const reverseFieldProperties: Record<string, ISchema> = {
}, },
}, },
}, },
(field) => {
const values = field.form.values;
const { reverseField } = values;
field.value = !!reverseField?.key;
field.disabled = !!reverseField?.key;
},
], ],
}, },
'reverseField.type': { 'reverseField.type': {
@ -211,6 +216,7 @@ export const reverseFieldProperties: Record<string, ISchema> = {
required: true, required: true,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Input', 'x-component': 'Input',
'x-disabled': '{{ !showReverseFieldConfig }}',
}, },
'reverseField.name': { 'reverseField.name': {
type: 'string', type: 'string',
@ -219,6 +225,7 @@ export const reverseFieldProperties: Record<string, ISchema> = {
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Input', 'x-component': 'Input',
'x-validator': 'uid', 'x-validator': 'uid',
'x-disabled': '{{ !showReverseFieldConfig }}',
description: description:
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}", "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
}, },

View File

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

View File

@ -156,7 +156,7 @@ describe('CollectionFieldInterfaceManager', () => {
expect(fieldInterface.componentOptions).toMatchInlineSnapshot(` expect(fieldInterface.componentOptions).toMatchInlineSnapshot(`
[ [
{ {
"label": "A", "label": "{{t("A")}}",
"useProps": [Function], "useProps": [Function],
"value": "A", "value": "A",
}, },
@ -190,7 +190,7 @@ describe('CollectionFieldInterfaceManager', () => {
expect(fieldInterface.componentOptions).toMatchInlineSnapshot(` expect(fieldInterface.componentOptions).toMatchInlineSnapshot(`
[ [
{ {
"label": "A", "label": "{{t("A")}}",
"useProps": [Function], "useProps": [Function],
"value": "A", "value": "A",
}, },
@ -268,7 +268,7 @@ describe('CollectionFieldInterfaceManager', () => {
expect(fieldInterface.componentOptions).toMatchInlineSnapshot(` expect(fieldInterface.componentOptions).toMatchInlineSnapshot(`
[ [
{ {
"label": "B", "label": "{{t("B")}}",
"useProps": [Function], "useProps": [Function],
"value": "A.B", "value": "A.B",
}, },
@ -292,7 +292,7 @@ describe('CollectionFieldInterfaceManager', () => {
}; };
componentOptions = [ componentOptions = [
{ {
label: 'A', label: '{{t("A")}}',
value: 'A', value: 'A',
}, },
]; ];
@ -308,7 +308,7 @@ describe('CollectionFieldInterfaceManager', () => {
expect(fieldInterface.componentOptions).toMatchInlineSnapshot(` expect(fieldInterface.componentOptions).toMatchInlineSnapshot(`
[ [
{ {
"label": "A", "label": "{{t("A")}}",
"value": "A", "value": "A",
}, },
{ {

View File

@ -181,6 +181,23 @@ describe('Collection', () => {
}); });
}); });
describe('getFilterTargetKey()', () => {
test('not set as id', () => {
const collection = getCollection({ name: 'test' });
expect(collection.getFilterTargetKey()).toBe('id');
});
test('single ftk', () => {
const collection = getCollection({ name: 'test', filterTargetKey: 'a' });
expect(collection.getFilterTargetKey()).toBe('a');
});
test('multiple ftk', () => {
const collection = getCollection({ name: 'test', filterTargetKey: ['a', 'b'] });
expect(collection.getFilterTargetKey()).toMatchObject(['a', 'b']);
});
});
test('properties', () => { test('properties', () => {
const app = new Application({ const app = new Application({
dataSourceManager: { dataSourceManager: {

View File

@ -8,10 +8,11 @@
*/ */
import type { ISchema } from '@formily/react'; import type { ISchema } from '@formily/react';
import { cloneDeep } from 'lodash'; import { cloneDeep, capitalize } from 'lodash';
import type { CollectionFieldOptions } from '../collection'; import type { CollectionFieldOptions } from '../collection';
import { CollectionFieldInterfaceManager } from './CollectionFieldInterfaceManager'; import { CollectionFieldInterfaceManager } from './CollectionFieldInterfaceManager';
import { defaultProps } from '../../collection-manager/interfaces/properties'; import { defaultProps } from '../../collection-manager/interfaces/properties';
import { tval } from '@nocobase/utils/client';
export type CollectionFieldInterfaceFactory = new ( export type CollectionFieldInterfaceFactory = new (
collectionFieldInterfaceManager: CollectionFieldInterfaceManager, collectionFieldInterfaceManager: CollectionFieldInterfaceManager,
) => CollectionFieldInterface; ) => CollectionFieldInterface;
@ -71,9 +72,11 @@ export abstract class CollectionFieldInterface {
const xComponent = this.default?.uiSchema?.['x-component']; const xComponent = this.default?.uiSchema?.['x-component'];
const componentProps = this.default?.uiSchema?.['x-component-props']; const componentProps = this.default?.uiSchema?.['x-component-props'];
if (xComponent) { if (xComponent) {
const schemaType = this.default?.uiSchema?.type || 'string';
const label = tval(xComponent.startsWith('Input') ? capitalize(schemaType) : xComponent.split('.').pop());
this.componentOptions = [ this.componentOptions = [
{ {
label: xComponent.split('.').pop(), label,
value: xComponent, value: xComponent,
useProps() { useProps() {
return componentProps || {}; return componentProps || {};

View File

@ -8,10 +8,11 @@
*/ */
import { Field } from '@formily/core'; import { Field } from '@formily/core';
import { connect, useField, useFieldSchema } from '@formily/react'; import { connect, Schema, useField, useFieldSchema } from '@formily/react';
import { untracked } from '@formily/reactive';
import { merge } from '@formily/shared'; import { merge } from '@formily/shared';
import { concat } from 'lodash'; import { concat } from 'lodash';
import React, { useCallback, useEffect, useMemo } from 'react'; import React, { useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { useFormBlockContext } from '../../block-provider/FormBlockProvider'; import { useFormBlockContext } from '../../block-provider/FormBlockProvider';
import { useDynamicComponentProps } from '../../hoc/withDynamicSchemaProps'; import { useDynamicComponentProps } from '../../hoc/withDynamicSchemaProps';
@ -24,54 +25,48 @@ type Props = {
children?: React.ReactNode; children?: React.ReactNode;
}; };
const setFieldProps = (field: Field, key: string, value: any) => {
untracked(() => {
if (field[key] === undefined) {
field[key] = value;
}
});
};
const setRequired = (field: Field, fieldSchema: Schema, uiSchema: Schema) => {
if (typeof fieldSchema['required'] === 'undefined') {
field.required = !!uiSchema['required'];
}
};
/** /**
* TODO: 初步适配 * TODO: 初步适配
* @internal * @internal
*/ */
export const CollectionFieldInternalField: React.FC = (props: Props) => { export const CollectionFieldInternalField: React.FC = (props: Props) => {
const { component } = props;
const compile = useCompile(); const compile = useCompile();
const field = useField<Field>(); const field = useField<Field>();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const collectionField = useCollectionField(); const { uiSchema: uiSchemaOrigin, defaultValue } = useCollectionField();
const { uiSchema: uiSchemaOrigin, defaultValue } = collectionField;
const { isAllowToSetDefaultValue } = useIsAllowToSetDefaultValue(); const { isAllowToSetDefaultValue } = useIsAllowToSetDefaultValue();
const uiSchema = useMemo(() => compile(uiSchemaOrigin), [JSON.stringify(uiSchemaOrigin)]);
const Component = useComponent( const Component = useComponent(
fieldSchema['x-component-props']?.['component'] || uiSchema?.['x-component'] || 'Input', fieldSchema['x-component-props']?.['component'] || uiSchemaOrigin?.['x-component'] || 'Input',
); );
const setFieldProps = useCallback(
(key, value) => {
field[key] = typeof field[key] === 'undefined' ? value : field[key];
},
[field],
);
const setRequired = useCallback(() => {
if (typeof fieldSchema['required'] === 'undefined') {
field.required = !!uiSchema['required'];
}
}, [fieldSchema, uiSchema]);
const ctx = useFormBlockContext(); const ctx = useFormBlockContext();
const dynamicProps = useDynamicComponentProps(uiSchemaOrigin?.['x-use-component-props'], props);
const dynamicProps = useDynamicComponentProps(uiSchema?.['x-use-component-props'], props);
useEffect(() => {
if (ctx?.field) {
ctx.field.added = ctx.field.added || new Set();
ctx.field.added.add(fieldSchema.name);
}
});
// TODO: 初步适配 // TODO: 初步适配
useEffect(() => { useEffect(() => {
if (!uiSchema) { if (!uiSchemaOrigin) {
return; return;
} }
setFieldProps('content', uiSchema['x-content']); const uiSchema = compile(uiSchemaOrigin);
setFieldProps('title', uiSchema.title); setFieldProps(field, 'content', uiSchema['x-content']);
setFieldProps('description', uiSchema.description); setFieldProps(field, 'title', uiSchema.title);
setFieldProps(field, 'description', uiSchema.description);
if (ctx?.form) { if (ctx?.form) {
const defaultVal = isAllowToSetDefaultValue() ? fieldSchema.default || defaultValue : undefined; const defaultVal = isAllowToSetDefaultValue() ? fieldSchema.default || defaultValue : undefined;
defaultVal !== null && defaultVal !== undefined && setFieldProps('initialValue', defaultVal); defaultVal !== null && defaultVal !== undefined && setFieldProps(field, 'initialValue', defaultVal);
} }
if (!field.validator && (uiSchema['x-validator'] || fieldSchema['x-validator'])) { if (!field.validator && (uiSchema['x-validator'] || fieldSchema['x-validator'])) {
@ -84,20 +79,21 @@ export const CollectionFieldInternalField: React.FC = (props: Props) => {
if (fieldSchema['x-read-pretty'] === true) { if (fieldSchema['x-read-pretty'] === true) {
field.readPretty = true; field.readPretty = true;
} }
setRequired(); setRequired(field, fieldSchema, uiSchema);
// @ts-ignore // @ts-ignore
field.dataSource = uiSchema.enum; field.dataSource = uiSchema.enum;
const originalProps = compile(uiSchema['x-component-props']) || {}; const originalProps = compile(uiSchema['x-component-props']) || {};
field.componentProps = merge(originalProps, field.componentProps || {}, dynamicProps || {}); field.componentProps = merge(originalProps, field.componentProps || {}, dynamicProps || {});
}, [uiSchema]); }, [uiSchemaOrigin]);
if (!uiSchema) return null; if (!uiSchemaOrigin) return null;
return <Component {...props} {...dynamicProps} />; return <Component {...props} {...dynamicProps} />;
}; };
export const CollectionField = connect((props) => { export const CollectionField = connect((props) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const field = useField<Field>();
return ( return (
<ErrorBoundary FallbackComponent={ErrorFallback.Modal} onError={(err) => console.log(err)}> <ErrorBoundary FallbackComponent={ErrorFallback.Modal} onError={(err) => console.log(err)}>
<CollectionFieldProvider name={fieldSchema.name}> <CollectionFieldProvider name={fieldSchema.name}>

View File

@ -164,6 +164,9 @@ export class Collection {
return this.primaryKey; return this.primaryKey;
} }
getFilterTargetKey() {
return this.filterTargetKey || this.getPrimaryKey() || 'id';
}
get inherits() { get inherits() {
return this.options.inherits || []; return this.options.inherits || [];
@ -250,6 +253,10 @@ export class Collection {
return predicate ? filter(this.fields, predicate) : this.fields; return predicate ? filter(this.fields, predicate) : this.fields;
} }
getAllFields(predicate?: GetCollectionFieldPredicate) {
return this.getFields(predicate);
}
protected getFieldsMap() { protected getFieldsMap() {
if (!this.fieldsMap) { if (!this.fieldsMap) {
this.fieldsMap = this.getFields().reduce((memo, field) => { this.fieldsMap = this.getFields().reduce((memo, field) => {

View File

@ -141,6 +141,10 @@ export class CollectionManager {
return this.getCollection(collectionName)?.getFields(predicate) || []; return this.getCollection(collectionName)?.getFields(predicate) || [];
} }
getCollectionAllFields(collectionName: string, predicate?: GetCollectionFieldPredicate) {
return this.getCollection(collectionName)?.getAllFields(predicate) || [];
}
/** /**
* @example * @example
* getFilterByTK('users', { id: 1 }); // 1 * getFilterByTK('users', { id: 1 }); // 1
@ -160,7 +164,6 @@ export class CollectionManager {
); );
return; return;
} }
const getTargetKey = (collection: Collection) => collection.filterTargetKey || collection.getPrimaryKey() || 'id';
const buildFilterByTk = (targetKey: string | string[], record: Record<string, any>) => { const buildFilterByTk = (targetKey: string | string[], record: Record<string, any>) => {
if (Array.isArray(targetKey)) { if (Array.isArray(targetKey)) {
@ -175,7 +178,7 @@ export class CollectionManager {
}; };
if (collectionOrAssociation instanceof Collection) { if (collectionOrAssociation instanceof Collection) {
const targetKey = getTargetKey(collectionOrAssociation); const targetKey = collectionOrAssociation.getFilterTargetKey();
return buildFilterByTk(targetKey, collectionRecordOrAssociationRecord); return buildFilterByTk(targetKey, collectionRecordOrAssociationRecord);
} }
@ -200,7 +203,7 @@ export class CollectionManager {
); );
return; return;
} }
const targetKey = getTargetKey(targetCollection); const targetKey = targetCollection.getFilterTargetKey();
return buildFilterByTk(targetKey, collectionRecordOrAssociationRecord); return buildFilterByTk(targetKey, collectionRecordOrAssociationRecord);
} }

View File

@ -8,8 +8,8 @@
*/ */
import { IResource } from '@nocobase/sdk'; import { IResource } from '@nocobase/sdk';
import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react';
import { isArray } from 'lodash'; import { isArray } from 'lodash';
import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react';
import { useAPIClient } from '../../api-client'; import { useAPIClient } from '../../api-client';
import { useCollectionManager } from '../collection'; import { useCollectionManager } from '../collection';
import { CollectionRecord } from '../collection-record'; import { CollectionRecord } from '../collection-record';

View File

@ -27,10 +27,7 @@ const getHook = (str: string, scope: Record<string, any>, allText: string) => {
return res || useDefaultDynamicComponentProps; return res || useDefaultDynamicComponentProps;
}; };
export function useDynamicComponentProps(useComponentPropsStr?: string, props?: any) { const getUseDynamicProps = (useComponentPropsStr: string, scope: Record<string, any>) => {
const scope = useExpressionScope();
const useDynamicProps = useMemo(() => {
if (!useComponentPropsStr) { if (!useComponentPropsStr) {
return useDefaultDynamicComponentProps; return useDefaultDynamicComponentProps;
} }
@ -47,9 +44,11 @@ export function useDynamicComponentProps(useComponentPropsStr?: string, props?:
} }
return result; return result;
}, [useComponentPropsStr]); };
const res = useDynamicProps(props); export function useDynamicComponentProps(useComponentPropsStr?: string, props?: any) {
const scope = useExpressionScope();
const res = getUseDynamicProps(useComponentPropsStr, scope)(props);
return res; return res;
} }

View File

@ -842,8 +842,12 @@
"is any of": "is any of", "is any of": "is any of",
"Plugin dependency version mismatch": "Plugin dependency version mismatch", "Plugin dependency version mismatch": "Plugin dependency version mismatch",
"The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?", "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?",
"Allow multiple selection": "Allow multiple selection",
"Parent object": "Parent object",
"Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data", "Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data",
"Enable secondary confirmation": "Enable secondary confirmation", "Enable secondary confirmation": "Enable secondary confirmation",
"Notification": "Notification", "Notification": "Notification",
"Ellipsis overflow content": "Ellipsis overflow content" "Ellipsis overflow content": "Ellipsis overflow content",
"Hide column": "Hide column",
"In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect."
} }

View File

@ -766,5 +766,9 @@
"Clear default value": "Borrar valor por defecto", "Clear default value": "Borrar valor por defecto",
"Open in new window": "Abrir en una nueva ventana", "Open in new window": "Abrir en una nueva ventana",
"Sorry, the page you visited does not exist.": "Lo siento, la página que visitaste no existe.", "Sorry, the page you visited does not exist.": "Lo siento, la página que visitaste no existe.",
"Ellipsis overflow content": "Contenido de desbordamiento de elipsis" "Allow multiple selection": "Permitir selección múltiple",
"Parent object": "Objeto padre",
"Ellipsis overflow content": "Contenido de desbordamiento de elipsis",
"Hide column": "Ocultar columna",
"In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "En modo de configuración, toda la columna se vuelve transparente. En modo de no configuración, toda la columna se ocultará. Incluso si toda la columna está oculta, sus valores predeterminados configurados y otras configuraciones seguirán tomando efecto."
} }

View File

@ -786,5 +786,9 @@
"Clear default value": "Effacer la valeur par défaut", "Clear default value": "Effacer la valeur par défaut",
"Open in new window": "Ouvrir dans une nouvelle fenêtre", "Open in new window": "Ouvrir dans une nouvelle fenêtre",
"Sorry, the page you visited does not exist.": "Désolé, la page que vous avez visitée n'existe pas.", "Sorry, the page you visited does not exist.": "Désolé, la page que vous avez visitée n'existe pas.",
"Ellipsis overflow content": "Contenu de débordement avec ellipse" "Allow multiple selection": "Permettre la sélection multiple",
"Parent object": "Objet parent",
"Ellipsis overflow content": "Contenu de débordement avec ellipse",
"Hide column": "Masquer la colonne",
"In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "En mode de configuration, toute la colonne devient transparente. En mode de non-configuration, toute la colonne sera masquée. Même si toute la colonne est masquée, ses valeurs par défaut configurées et les autres paramètres resteront toujours en vigueur."
} }

View File

@ -1004,5 +1004,9 @@
"Use simple pagination mode": "シンプルなページネーションモードを使用", "Use simple pagination mode": "シンプルなページネーションモードを使用",
"Set Template Engine": "テンプレートエンジンを設定", "Set Template Engine": "テンプレートエンジンを設定",
"Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "ページング時にテーブルレコードの総数取得をスキップして、読み込み速度を向上させます。データ量が多い場合にこのオプションの使用をお勧めします。", "Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "ページング時にテーブルレコードの総数取得をスキップして、読み込み速度を向上させます。データ量が多い場合にこのオプションの使用をお勧めします。",
"The current user only has the UI configuration permission, but don't have view permission for collection \"{{name}}\"": "現在のユーザーにはUI設定の権限しかなく、コレクション「{{name}}」を閲覧する権限はありません。" "The current user only has the UI configuration permission, but don't have view permission for collection \"{{name}}\"": "現在のユーザーにはUI設定の権限しかなく、コレクション「{{name}}」を閲覧する権限はありません。",
"Allow multiple selection": "複数選択を許可",
"Parent object": "親オブジェクト",
"Hide column": "列を非表示",
"In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "設定モードでは、列全体が透明になります。非設定モードでは、列全体が非表示になります。列全体が非表示になっても、設定されたデフォルト値やその他の設定は依然として有効です。"
} }

View File

@ -877,5 +877,9 @@
"Clear default value": "기본값 지우기", "Clear default value": "기본값 지우기",
"Open in new window": "새 창에서 열기", "Open in new window": "새 창에서 열기",
"Sorry, the page you visited does not exist.": "죄송합니다. 방문한 페이지가 존재하지 않습니다.", "Sorry, the page you visited does not exist.": "죄송합니다. 방문한 페이지가 존재하지 않습니다.",
"Ellipsis overflow content": "생략 부호로 내용 줄임" "Allow multiple selection": "다중 선택 허용",
"Parent object": "부모 객체",
"Ellipsis overflow content": "생략 부호로 내용 줄임",
"Hide column": "열 숨기기",
"In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "구성 모드에서는 전체 열이 투명해집니다. 비구성 모드에서는 전체 열이 숨겨집니다. 전체 열이 숨겨져도 구성된 기본값 및 기타 설정은 여전히 적용됩니다."
} }

View File

@ -743,5 +743,9 @@
"Clear default value": "Limpar valor padrão", "Clear default value": "Limpar valor padrão",
"Open in new window": "Abrir em nova janela", "Open in new window": "Abrir em nova janela",
"Sorry, the page you visited does not exist.": "Desculpe, a página que você visitou não existe.", "Sorry, the page you visited does not exist.": "Desculpe, a página que você visitou não existe.",
"Ellipsis overflow content": "Conteúdo de transbordamento com reticências" "Allow multiple selection": "Permitir seleção múltipla",
"Parent object": "Objeto pai",
"Ellipsis overflow content": "Conteúdo de transbordamento com reticências",
"Hide column": "Ocultar coluna",
"In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "Em modo de configuração, a coluna inteira se torna transparente. Em modo de não configuração, a coluna inteira será ocultada. Mesmo se a coluna inteira estiver oculta, seus valores padrão configurados e outras configurações ainda terão efeito."
} }

View File

@ -580,5 +580,9 @@
"Clear default value": "Очистить значение по умолчанию", "Clear default value": "Очистить значение по умолчанию",
"Open in new window": "Открыть в новом окне", "Open in new window": "Открыть в новом окне",
"Sorry, the page you visited does not exist.": "Извините, посещенной вами страницы не существует.", "Sorry, the page you visited does not exist.": "Извините, посещенной вами страницы не существует.",
"Ellipsis overflow content": "Содержимое с многоточием при переполнении" "Allow multiple selection": "Разрешить множественный выбор",
"Parent object": "Родительский объект",
"Ellipsis overflow content": "Содержимое с многоточием при переполнении",
"Hide column": "Скрыть столбец",
"In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "В режиме конфигурации вся колонка становится прозрачной. В режиме не конфигурации вся колонка будет скрыта. Даже если вся колонка будет скрыта, её настроенные значения по умолчанию и другие настройки все равно будут действовать."
} }

View File

@ -578,5 +578,9 @@
"Clear default value": "Varsayılan değeri temizle", "Clear default value": "Varsayılan değeri temizle",
"Open in new window": "Yeni pencerede aç", "Open in new window": "Yeni pencerede aç",
"Sorry, the page you visited does not exist.": "Üzgünüz, ziyaret ettiğiniz sayfa mevcut değil.", "Sorry, the page you visited does not exist.": "Üzgünüz, ziyaret ettiğiniz sayfa mevcut değil.",
"Ellipsis overflow content": "Üç nokta ile taşan içerik" "Allow multiple selection": "Çoklu seçim izni",
"Parent object": "Üst nesne",
"Ellipsis overflow content": "Üç nokta ile taşan içerik",
"Hide column": "Sütunu gizle",
"In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "Yapılandırma modunda, tüm sütun tamamen saydamlık alır. Yapılandırma modu olmayan durumda, tüm sütun gizlenir. Tamamen sütun gizlendiğinde bile, yapılandırılmış varsayılan değerleri ve diğer ayarları hâlâ etkin olur."
} }

View File

@ -786,5 +786,9 @@
"Clear default value": "Очистити значення за замовчуванням", "Clear default value": "Очистити значення за замовчуванням",
"Open in new window": "Відкрити в новому вікні", "Open in new window": "Відкрити в новому вікні",
"Sorry, the page you visited does not exist.": "Вибачте, сторінка, яку ви відвідали, не існує.", "Sorry, the page you visited does not exist.": "Вибачте, сторінка, яку ви відвідали, не існує.",
"Ellipsis overflow content": "Вміст з багатокрапкою при переповненні" "Allow multiple selection": "Дозволити множинний вибір",
"Parent object": "Батьківський об'єкт",
"Ellipsis overflow content": "Вміст з багатокрапкою при переповненні",
"Hide column": "Сховати стовпець",
"In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "В режимі конфігурації вся колонка стає прозорою. В режимі не конфігурації вся колонка буде прихована. Якщо вся колонка буде прихована, її налаштовані значення за замовчуванням і інші налаштування все одно будуть діяти."
} }

View File

@ -363,7 +363,7 @@
"is empty": "为空", "is empty": "为空",
"is not empty": "不为空", "is not empty": "不为空",
"Edit chart": "编辑图表", "Edit chart": "编辑图表",
"Add text": "添加文本", "Add Markdown": "添加 Markdown",
"Filterable fields": "可筛选字段", "Filterable fields": "可筛选字段",
"Edit button": "编辑按钮", "Edit button": "编辑按钮",
"Hide": "隐藏", "Hide": "隐藏",
@ -979,6 +979,8 @@
"The current user only has the UI configuration permission, but don't have view permission for collection \"{{name}}\"": "当前用户只有 UI 配置权限,但没有数据表 \"{{name}}\" 查看权限。", "The current user only has the UI configuration permission, but don't have view permission for collection \"{{name}}\"": "当前用户只有 UI 配置权限,但没有数据表 \"{{name}}\" 查看权限。",
"Plugin dependency version mismatch": "插件依赖版本不一致", "Plugin dependency version mismatch": "插件依赖版本不一致",
"The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "当前插件的依赖版本与应用的版本不一致,可能无法正常工作。您确定要继续激活插件吗?", "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "当前插件的依赖版本与应用的版本不一致,可能无法正常工作。您确定要继续激活插件吗?",
"Allow multiple selection": "允许多选",
"Parent object": "上级对象",
"Default value to current time": "设置字段默认值为当前时间", "Default value to current time": "设置字段默认值为当前时间",
"Automatically update timestamp on update": "当记录更新时自动设置字段值为当前时间", "Automatically update timestamp on update": "当记录更新时自动设置字段值为当前时间",
"Default value to current server time": "设置字段默认值为当前服务端时间", "Default value to current server time": "设置字段默认值为当前服务端时间",
@ -1004,5 +1006,26 @@
"Switching the picker, the value and default value will be cleared": "切换选择器时,字段的值和默认值将会被清空", "Switching the picker, the value and default value will be cleared": "切换选择器时,字段的值和默认值将会被清空",
"Stay on the current popup or page": "停留在当前弹窗或页面", "Stay on the current popup or page": "停留在当前弹窗或页面",
"Return to the previous popup or page": "返回上一层弹窗或页面", "Return to the previous popup or page": "返回上一层弹窗或页面",
"Action after successful submission":"提交成功后动作" "Action after successful submission": "提交成功后动作",
"Allow disassociation": "允许解除已有数据关联",
"Layout": "布局",
"Vertical": "垂直",
"Horizontal": "水平",
"Edit group title": "编辑分组标题",
"Title position": "标题位置",
"Dashed": "虚线",
"Left": "左",
"Center": "居中",
"Right": "右",
"Divider line color": "分割线颜色",
"Label align": "字段标题对齐方式",
"Label width": "字段标题宽度",
"When the Label exceeds the width": "字段标题超出宽度时",
"Line break": "换行",
"Ellipsis": "省略",
"Set block layout": "设置区块布局",
"Add & Update": "添加 & 更新",
"Table size":"表格大小",
"Hide column": "隐藏列",
"In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "在配置模式下,整个列会变为透明色。在非配置模式下,整个列将被隐藏。即使整个列被隐藏了,其配置的默认值和其他设置仍然有效。"
} }

View File

@ -876,5 +876,9 @@
"Clear default value": "清除預設值", "Clear default value": "清除預設值",
"Open in new window": "新窗口打開", "Open in new window": "新窗口打開",
"Sorry, the page you visited does not exist.": "抱歉,你訪問的頁面不存在。", "Sorry, the page you visited does not exist.": "抱歉,你訪問的頁面不存在。",
"Ellipsis overflow content": "省略超出長度的內容" "Allow multiple selection": "允許多選",
"Parent object": "上級物件",
"Ellipsis overflow content": "省略超出長度的內容",
"Hide column": "隱藏列",
"In configuration mode, the entire column becomes transparent. In non-configuration mode, the entire column will be hidden. Even if the entire column is hidden, its configured default values and other settings will still take effect.": "在配置模式下,整個列會變為透明色。在非配置模式下,整個列將被隱藏。即使整個列被隱藏了,其配置的默認值和其他設置仍然有效。"
} }

View File

@ -8,7 +8,12 @@
*/ */
import { expect, test } from '@nocobase/test/e2e'; import { expect, test } from '@nocobase/test/e2e';
import { PopupAndSubPageWithParams, oneEmptyTableWithUsers, openInNewWidow } from './templates'; import {
PopupAndSubPageWithParams,
URLSearchParamsUseAssociationFieldValue,
oneEmptyTableWithUsers,
openInNewWidow,
} from './templates';
test.describe('Link', () => { test.describe('Link', () => {
test('basic', async ({ page, mockPage, mockRecords }) => { test('basic', async ({ page, mockPage, mockRecords }) => {
@ -140,4 +145,13 @@ test.describe('Link', () => {
await page.reload(); await page.reload();
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('abc'); await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('abc');
}); });
test('URL search params: use association field value', async ({ page, mockPage, mockRecords }) => {
await mockPage(URLSearchParamsUseAssociationFieldValue).goto();
// After clicking the Link button, the browser URL will change, and the value of the input box using variables will be updated
await page.getByLabel('action-Action.Link-Link-').click();
await page.waitForTimeout(100);
await expect(page.getByLabel('block-item-CollectionField-')).toHaveText(`Roles:adminmemberroot`);
});
}); });

View File

@ -757,3 +757,317 @@ export const PopupAndSubPageWithParams = {
'x-index': 1, 'x-index': 1,
}, },
}; };
export const URLSearchParamsUseAssociationFieldValue = {
pageSchema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Page',
'x-app-version': '1.3.33-beta',
properties: {
gbfvfwym8ds: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'page:addBlock',
'x-app-version': '1.3.33-beta',
properties: {
'33bst9cikf7': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '1.3.33-beta',
properties: {
fewb4k72bc8: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
'x-app-version': '1.3.33-beta',
properties: {
'5cfypzqvw57': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'TableBlockProvider',
'x-acl-action': 'users:list',
'x-use-decorator-props': 'useTableBlockDecoratorProps',
'x-decorator-props': {
collection: 'users',
dataSource: 'main',
action: 'list',
params: {
pageSize: 20,
},
rowKey: 'id',
showIndex: true,
dragSort: false,
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:table',
'x-component': 'CardItem',
'x-filter-targets': [],
'x-app-version': '1.3.33-beta',
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-initializer': 'table:configureActions',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
'x-app-version': '1.3.33-beta',
'x-uid': '1xy1166yc9k',
'x-async': false,
'x-index': 1,
},
'6a59q3gjjqu': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'array',
'x-initializer': 'table:configureColumns',
'x-component': 'TableV2',
'x-use-component-props': 'useTableBlockProps',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
},
'x-app-version': '1.3.33-beta',
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("Actions") }}',
'x-action-column': 'actions',
'x-decorator': 'TableV2.Column.ActionBar',
'x-component': 'TableV2.Column',
'x-toolbar': 'TableColumnSchemaToolbar',
'x-initializer': 'table:configureItemActions',
'x-settings': 'fieldSettings:TableColumn',
'x-toolbar-props': {
initializer: 'table:configureItemActions',
},
'x-app-version': '1.3.33-beta',
properties: {
lzxiek232g3: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
'x-app-version': '1.3.33-beta',
properties: {
qhrjv5sk1tc: {
'x-uid': 'o7zrp842yhz',
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("Link") }}',
'x-action': 'customize:link',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:link',
'x-component': 'Action.Link',
'x-use-component-props': 'useLinkActionProps',
'x-designer-props': {
linkageAction: true,
},
'x-component-props': {
url: '/admin/ids0d9esx8k',
params: [
{
name: 'roles',
value: '{{$nRecord.roles}}',
},
],
},
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'y0m958j0dh0',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '4d4wnl1b6xx',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '238yee6oghy',
'x-async': false,
'x-index': 2,
},
},
'x-uid': 'gjzmrbmobaf',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'z4rsa4oitvz',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '6tec1zys03w',
'x-async': false,
'x-index': 1,
},
sbdgc6nmy9x: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '1.3.33-beta',
properties: {
'3sfuujjet76': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
'x-app-version': '1.3.33-beta',
properties: {
p3zmu2uua0x: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-acl-action-props': {
skipScopeCheck: true,
},
'x-acl-action': 'users:create',
'x-decorator': 'FormBlockProvider',
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: 'users',
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:createForm',
'x-component': 'CardItem',
'x-app-version': '1.3.33-beta',
properties: {
'1oq2w1ia4jy': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': 'useCreateFormBlockProps',
'x-app-version': '1.3.33-beta',
properties: {
grid: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'form:configureFields',
'x-app-version': '1.3.33-beta',
properties: {
efdbxp35iht: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '1.3.33-beta',
properties: {
k40ivzy5kcu: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
'x-app-version': '1.3.33-beta',
properties: {
roles: {
'x-uid': 'v8jvm1d9j8q',
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.roles',
'x-component-props': {
fieldNames: {
label: 'name',
value: 'name',
},
},
'x-app-version': '1.3.33-beta',
default: '{{$nURLSearchParams.roles}}',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '3rs4fwe2gak',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'tuqcvp6tzbg',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'dz9070niyqm',
'x-async': false,
'x-index': 1,
},
'5mu8w85umxn': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-initializer': 'createForm:configureActions',
'x-component': 'ActionBar',
'x-component-props': {
layout: 'one-column',
},
'x-app-version': '1.3.33-beta',
'x-uid': 'xgak2t61ukm',
'x-async': false,
'x-index': 2,
},
},
'x-uid': 'asfu0o75c3k',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'tgsr3gv33qk',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'pjd4g9evi3v',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'hj4wq3bdtip',
'x-async': false,
'x-index': 2,
},
},
'x-uid': '1xgpa64dn71',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'ocal3pnltf2',
'x-async': true,
'x-index': 1,
},
keepUid: true,
pageUid: 'ids0d9esx8k',
};

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