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
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.repository }}
cancel-in-progress: true
on:
workflow_dispatch:
inputs:
repository:
description: 'Please enter a repository name'
push:
branches:
- 'main'
@ -18,7 +22,7 @@ jobs:
with:
app-id: ${{ vars.NOCOBASE_APP_ID }}
private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }}
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
- name: Get GitHub App User ID
id: get-user-id
@ -28,11 +32,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
with:
repository: nocobase/nocobase
repository: nocobase/${{ inputs.repository || 'nocobase' }}
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true
fetch-depth: 0
- name: main -> next(nocobase)
- name: main -> next(${{ inputs.repository || 'nocobase' }})
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>'
@ -41,11 +45,11 @@ jobs:
git checkout next
git merge main
git push origin next
- name: push nocobase(next)
- name: push ${{ inputs.repository || 'nocobase' }}(next)
uses: ad-m/github-push-action@master
with:
branch: next
github_token: ${{ steps.app-token.outputs.token }}
repository: nocobase/nocobase
repository: nocobase/${{ inputs.repository || 'nocobase' }}
tags: true
atomic: true

View File

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

View File

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

View File

@ -16,18 +16,31 @@ on:
default: beta
push:
tags:
- 'v*-beta'
- 'v*'
jobs:
write-changelog-and-release:
runs-on: ubuntu-latest
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
id: app-token
with:
app-id: ${{ vars.NOCOBASE_APP_ID }}
private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }}
repositories: nocobase,pro-plugins,${{ join(fromJSON(steps.get-info.outputs.proRepos), ',') }}
skip-token-revoke: true
- name: Get GitHub App User ID
id: get-user-id
@ -38,7 +51,7 @@ jobs:
uses: actions/checkout@v4
with:
repository: nocobase/nocobase
ref: main
ref: ${{ steps.get-info.outputs.branch }}
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true
fetch-depth: 0
@ -47,15 +60,16 @@ jobs:
with:
repository: nocobase/pro-plugins
path: packages/pro-plugins
ref: ${{ steps.get-info.outputs.branch }}
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true
- name: Clone pro repos
shell: bash
run: |
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }}
for repo in ${{ join(fromJSON(steps.get-info.outputs.proRepos), ' ') }}
do
git clone -b main https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
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
- name: Set user
run: |
@ -70,11 +84,12 @@ jobs:
- name: Run script
shell: bash
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:
PRO_PLUGIN_REPOS: ${{ vars.PRO_PLUGIN_REPOS }}
PRO_PLUGIN_REPOS: ${{ steps.get-info.outputs.proRepos }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Commit and push
if: ${{ steps.get-info.outputs.version == 'beta' }}
run: |
git pull origin main
git add .

View File

@ -44,7 +44,7 @@ jobs:
echo "::set-output name=tags::pr-${{ github.event.pull_request.number }}"
fi
- name: copy files via ssh - ${{ steps.set-tags.outputs.tags }}
uses: appleboy/scp-action@v0.1.4
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.CN_CLIENT_HOST }}
username: ${{ secrets.CN_CLIENT_USERNAME }}

View File

@ -44,7 +44,7 @@ jobs:
uses: docker/build-push-action@v3
with:
context: .
file: Dockerfile
file: Dockerfile.pro
build-args: |
VERDACCIO_URL=http://localhost:4873/
COMMIT_HASH=${GITHUB_SHA}

View File

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

View File

@ -1,9 +1,11 @@
name: Build pro plugin docker image
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
run-name: Build pro plugin image ${{ github.ref }}-${{ inputs.pro_plugin }}-${{ inputs.pr_number }}-${{ inputs.nocobase_pr_number }}
on:
workflow_dispatch:
inputs:
@ -107,7 +109,7 @@ jobs:
uses: docker/build-push-action@v3
with:
context: .
file: Dockerfile
file: Dockerfile.pro
build-args: |
VERDACCIO_URL=http://localhost:4873/
COMMIT_HASH=${GITHUB_SHA}

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:
workflow_dispatch:
inputs:
is_feat:
description: 'is feat'
type: boolean
jobs:
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.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>'
echo "packages/pro-plugins" >> .git/info/exclude
bash release.sh $IS_FEAT
bash release.sh
env:
IS_FEAT: ${{ inputs.is_feat && '--is-feat' || '' }}
PRO_PLUGIN_REPOS: ${{ vars.PRO_PLUGIN_REPOS }}
CUSTOM_PRO_PLUGIN_REPOS: ${{ vars.CUSTOM_PRO_PLUGIN_REPOS }}
- name: Push and merge into next

View File

@ -39,8 +39,8 @@ jobs:
sqlite-test:
strategy:
matrix:
node_version: ['20']
underscored: [true, false]
node_version: [ '20' ]
underscored: [ true, false ]
runs-on: ubuntu-latest
container: node:${{ matrix.node_version }}
services:
@ -70,10 +70,10 @@ jobs:
postgres-test:
strategy:
matrix:
node_version: ['20']
underscored: [true, false]
schema: [public, nocobase]
collection_schema: [public, user_schema]
node_version: [ '20' ]
underscored: [ true, false ]
schema: [ public, nocobase ]
collection_schema: [ public, user_schema ]
runs-on: ubuntu-latest
container: node:${{ matrix.node_version }}
services:
@ -129,8 +129,8 @@ jobs:
mysql-test:
strategy:
matrix:
node_version: ['20']
underscored: [true, false]
node_version: [ '20' ]
underscored: [ true, false ]
runs-on: ubuntu-latest
container: node:${{ matrix.node_version }}
services:
@ -175,8 +175,8 @@ jobs:
mariadb-test:
strategy:
matrix:
node_version: ['20']
underscored: [true, false]
node_version: [ '20' ]
underscored: [ true, false ]
runs-on: ubuntu-latest
container: node:${{ matrix.node_version }}
services:

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
on:
workflow_dispatch:
push:
tags:
- 'v*'
@ -14,15 +15,28 @@ jobs:
runs-on: ubuntu-latest
container: node:18
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
id: app-token
with:
app-id: ${{ vars.NOCOBASE_APP_ID }}
private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }}
repositories: nocobase,pro-plugins,${{ join(fromJSON(steps.get-info.outputs.proRepos), ',') }},${{ join(fromJSON(vars.CUSTOM_PRO_PLUGIN_REPOS), ',') }}
skip-token-revoke: true
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.ref_name }}
- name: Send curl request and parse response
env:
PKG_USERNAME: ${{ secrets.PKG_USERNAME }}
@ -61,19 +75,20 @@ jobs:
yarn config set registry https://registry.npmjs.org/
yarn config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
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
uses: actions/checkout@v3
with:
repository: nocobase/pro-plugins
path: packages/pro-plugins
ref: ${{ github.ref_name }}
token: ${{ steps.app-token.outputs.token }}
- name: Clone pro repos
shell: bash
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
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
- name: Build Pro plugins
run: |
@ -84,17 +99,26 @@ jobs:
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
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
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
yarn release:force --no-verify-access --no-git-reset --registry https://pkg-src.nocobase.com --dist-tag=${{ steps.get-info.outputs.defaultTag }}
push-docker:
runs-on: ubuntu-latest
needs: publish-npm
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
uses: actions/checkout@v3
- name: Set up QEMU
@ -123,10 +147,21 @@ jobs:
registry: ${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}
username: ${{ secrets.ALI_DOCKER_USERNAME }}
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
with:
context: ./docker/nocobase
file: ./docker/nocobase/Dockerfile
platforms: linux/amd64,linux/arm64
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/)
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
### 🐛 Bug Fixes

View File

@ -5,6 +5,216 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
并且本项目遵循 [语义化版本](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
### 🐛 修复

View File

@ -41,7 +41,7 @@ RUN cd /app \
&& tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app .
FROM node:20.13-bullseye-slim
RUN apt-get update && apt-get install -y nginx
RUN apt-get update && apt-get install -y nginx libaio1
RUN rm -rf /etc/nginx/sites-enabled/default
COPY ./docker/nocobase/nocobase.conf /etc/nginx/sites-enabled/nocobase.conf

View File

@ -1,8 +1,8 @@
Updated Date: September 17, 2024
Updated Date: October 15, 2024
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.
@ -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.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
@ -34,7 +38,7 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr
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.
@ -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.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
@ -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.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.
@ -68,11 +76,11 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr
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.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.
@ -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.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.
@ -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.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>
## 加入我们
我们正在招聘远程 **全栈开发工程师****测试工程师****技术培训与文档专家**。 欢迎对 NocoBase 有强烈兴趣的伙伴加入。[查看详情](https://www.nocobase.com/cn/recruitment)
## 最近重要更新
- [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)
@ -39,7 +36,7 @@ https://demo-cn.nocobase.com/new
文档:
https://docs-cn.nocobase.com/
社区:
社区:
https://forum.nocobase.com/
## 与众不同之处
@ -78,3 +75,9 @@ NocoBase 支持三种安装方式:
- <a target="_blank" href="https://docs-cn.nocobase.com/welcome/getting-started/installation/git-clone">Git 源码安装</a>
如果你想体验最新未发布版本,或者想参与贡献,需要在源码上进行修改、调试,建议选择这种安装方式,对开发技术水平要求较高,如果代码有更新,可以走 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"
networks:
- 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:
image: postgres:latest
restart: always

View File

@ -27,7 +27,7 @@ RUN ARCH= && dpkgArch="$(dpkg --print-architecture)" \
esac \
&& set -ex \
# 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
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
/docker
/docs
/src"
/src
/dist/node_modules/external-db-data-source/src"
fi
echo $CONTENT

View File

@ -1,5 +1,5 @@
{
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.11",
"npmClient": "yarn",
"useWorkspaces": true,
"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",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.11",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
"@nocobase/resourcer": "1.4.0-alpha",
"@nocobase/utils": "1.4.0-alpha",
"@nocobase/resourcer": "1.4.0-alpha.11",
"@nocobase/utils": "1.4.0-alpha.11",
"minimatch": "^5.1.1"
},
"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",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.11",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
"@nocobase/cache": "1.4.0-alpha",
"@nocobase/database": "1.4.0-alpha",
"@nocobase/resourcer": "1.4.0-alpha"
"@nocobase/cache": "1.4.0-alpha.11",
"@nocobase/database": "1.4.0-alpha.11",
"@nocobase/resourcer": "1.4.0-alpha.11"
},
"repository": {
"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,
favicons: [`${appPublicPath}favicon/favicon.ico`],
metas: [{ name: 'viewport', content: 'initial-scale=0.1' }],
links: [
{ rel: 'stylesheet', href: `${appPublicPath}global.css` },
],
links: [{ rel: 'stylesheet', href: `${appPublicPath}global.css` }],
headScripts: [
{
src: `${appPublicPath}browser-checker.js`,
},
{
content: isDevCmd ? '' : `
content: isDevCmd
? ''
: `
window['__webpack_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_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_path__'] = '{{env.WS_PATH}}';
`,
},
],
cacheDirectoryPath: process.env.APP_CLIENT_CACHE_DIR || `node_modules/.cache`,
outputPath: path.resolve(__dirname, '../dist/client'),
hash: true,
alias: {
@ -62,8 +64,11 @@ export default defineConfig({
edge: 79,
safari: 12,
},
jsMinifierOptions: {
target: ['chrome80', 'es2020'],
},
codeSplitting: {
jsStrategy: 'depPerChunk'
jsStrategy: 'depPerChunk',
},
chainWebpack(config, { env }) {
if (env === 'production') {

View File

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

View File

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

View File

@ -7,15 +7,18 @@
* 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';
getConfig()
.then((config) => {
return Gateway.getInstance().run({
mainAppOptions: config,
});
})
.catch((e) => {
// console.error(e);
async function initializeGateway() {
await runPluginStaticImports();
const config = await getConfig();
await Gateway.getInstance().run({
mainAppOptions: config,
});
}
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",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.11",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
"@nocobase/actions": "1.4.0-alpha",
"@nocobase/cache": "1.4.0-alpha",
"@nocobase/database": "1.4.0-alpha",
"@nocobase/resourcer": "1.4.0-alpha",
"@nocobase/utils": "1.4.0-alpha",
"@nocobase/actions": "1.4.0-alpha.11",
"@nocobase/cache": "1.4.0-alpha.11",
"@nocobase/database": "1.4.0-alpha.11",
"@nocobase/resourcer": "1.4.0-alpha.11",
"@nocobase/utils": "1.4.0-alpha.11",
"@types/jsonwebtoken": "^8.5.8",
"jsonwebtoken": "^8.5.1"
},

View File

@ -106,7 +106,9 @@ export class AuthManager {
* @description Auth middleware, used to check the authentication status.
*/
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();
if (token && (await ctx.app.authManager.jwt.blacklist?.has(token))) {
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;
try {
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",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.11",
"description": "Library build tool based on rollup.",
"main": "lib/index.js",
"types": "./lib/index.d.ts",
@ -14,16 +14,25 @@
"@babel/preset-env": "7.25.4",
"@hapi/topo": "^6.0.0",
"@lerna/project": "4.0.0",
"@rspack/core": "1.0.14",
"@svgr/webpack": "^8.1.0",
"@types/gulp": "^4.0.13",
"@types/lerna__package": "5.1.0",
"@types/lerna__project": "5.1.0",
"@types/tar": "^6.1.5",
"@vercel/ncc": "0.36.1",
"chalk": "2.4.2",
"css-loader": "^6.8.1",
"esbuild-register": "^3.4.2",
"fast-glob": "^3.3.1",
"gulp": "4.0.2",
"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",
"tsup": "8.2.4",
"typescript": "5.1.3",

View File

@ -17,6 +17,7 @@ import { libInjectCss } from 'vite-plugin-lib-inject-css';
import { globExcludeFiles } from './constant';
import { PkgLog, UserConfig, getEnvDefine } from './utils';
import { rspack } from '@rspack/core';
export async function buildClient(cwd: string, userConfig: UserConfig, sourcemap: boolean = false, log: PkgLog) {
log('build client');
@ -29,7 +30,7 @@ export async function buildClient(cwd: string, userConfig: UserConfig, sourcemap
return true;
};
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);
}
@ -39,31 +40,170 @@ function buildClientEsm(cwd: string, userConfig: UserConfig, sourcemap: boolean,
log('build client esm');
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,
},
return rspack({
entry: {
index: entry,
},
output: {
path: outDir,
library: {
type: 'module',
},
plugins: [react(), libInjectCss()],
}),
);
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: {},
},
},
},
},
],
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(

View File

@ -7,11 +7,11 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import path from 'path';
import { PkgLog, UserConfig, getEnvDefine } from './utils';
import { build as viteBuild } from 'vite';
import fg from 'fast-glob';
import { rspack } from '@rspack/core';
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 cwdUnix = cwd.replaceAll(/\//g, '\\');
const external = function (id: string) {
@ -54,28 +61,158 @@ function build(cwd: string, entry: string, outDir: string, userConfig: UserConfi
}
return true;
};
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,
},
return rspack({
entry: {
index: entry,
},
output: {
path: outDir,
library: {
type: 'module',
},
}),
);
clean: true,
},
target: ['node16'],
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
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',
},
},
},
],
},
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 viteBuild } from 'vite';
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
import { rspack } from '@rspack/core';
import { EsbuildSupportExts, globExcludeFiles } from './constant';
import { PkgLog, UserConfig, getEnvDefine, getPackageJson } from './utils';
import { PkgLog, UserConfig, getPackageJson } from './utils';
import {
buildCheck,
checkFileSize,
@ -130,6 +131,7 @@ const external = [
'ahooks',
'lodash',
'china-division',
'file-saver',
];
const pluginPrefix = (
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) {
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 excludePackages = getExcludePackages(sourcePackages, external, pluginPrefix);
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) {
log('build plugin server dependencies');
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 includePackages = getIncludePackages(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) {
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(' '));
if (!includePackages.length) return;
@ -268,30 +276,34 @@ export async function buildPluginServer(cwd: string, userConfig: UserConfig, sou
const packageJson = getPackageJson(cwd);
const serverFiles = fg.globSync(serverGlobalFiles, { cwd, absolute: true });
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) {
log('%s will not be processed, only be copied to the dist directory.', chalk.yellow(otherExts.join(',')));
}
deleteServerFiles(cwd, log);
await tsupBuild(userConfig.modifyTsupConfig({
entry: serverFiles,
splitting: false,
clean: false,
bundle: false,
silent: true,
treeshake: false,
target: 'node16',
sourcemap,
outDir: path.join(cwd, target_dir),
format: 'cjs',
skipNodeModulesBundle: true,
loader: {
...otherExts.reduce((prev, cur) => ({ ...prev, [cur]: 'copy' }), {}),
'.json': 'copy',
},
}));
await tsupBuild(
userConfig.modifyTsupConfig({
entry: serverFiles,
splitting: false,
clean: false,
bundle: false,
silent: true,
treeshake: false,
target: 'node16',
sourcemap,
outDir: path.join(cwd, target_dir),
format: 'cjs',
skipNodeModulesBundle: true,
loader: {
...otherExts.reduce((prev, cur) => ({ ...prev, [cur]: 'copy' }), {}),
'.json': 'copy',
},
}),
);
await buildServerDeps(cwd, serverFiles, log);
}
@ -316,46 +328,199 @@ export async function buildPluginClient(cwd: string, userConfig: UserConfig, sou
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';
await viteBuild(userConfig.modifyViteConfig({
mode: process.env.NODE_ENV || 'production',
define: getEnvDefine(),
logLevel: 'warn',
build: {
minify: process.env.NODE_ENV === 'production',
outDir,
cssCodeSplit: false,
emptyOutDir: true,
sourcemap,
lib: {
entry,
formats: ['umd'],
const compiler = rspack({
mode: 'production',
// mode: "development",
context: cwd,
entry: './src/client/' + entry[0],
target: ['web', 'es5'],
output: {
path: outDir,
filename: outputFileName,
publicPath: `/static/plugins/${packageJson.name}/dist/client/`,
clean: true,
library: {
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,
},
},
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: [
react(),
cssInjectedByJsPlugin({ styleId: packageJson.name }),
new rspack.DefinePlugin({
'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) {

View File

@ -112,7 +112,7 @@ export function checkDependencies(packageJson: Record<string, any>, log: Log) {
chalk.yellow(packages.join(', ')),
chalk.yellow('dependencies'),
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",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.11",
"description": "",
"license": "AGPL-3.0",
"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 }) {
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 });
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);
}
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 (process.env.NODE_OPTIONS) {
// let opts = process.env.NODE_OPTIONS;

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,9 @@ export default defineConfig({
{ type: 'component', dir: 'src/schema-component/antd' },
],
},
jsMinifierOptions: {
target: ['chrome80', 'es2020'],
},
locales: lang === 'zh-CN' ? [{ id: 'zh-CN', name: '中文' },] : [{ id: 'en-US', name: 'English' }],
themeConfig: defineThemeConfig({
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",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.11",
"license": "AGPL-3.0",
"main": "lib/index.js",
"module": "es/index.mjs",
@ -11,7 +11,7 @@
"@ant-design/icons": "^5.1.4",
"@ant-design/pro-layout": "^7.16.11",
"@antv/g2plot": "^2.4.18",
"@budibase/handlebars-helpers": "^0.13.2",
"@budibase/handlebars-helpers": "^0.14.0",
"@ctrl/tinycolor": "^3.6.0",
"@dnd-kit/core": "^5.0.1",
"@dnd-kit/modifiers": "^6.0.0",
@ -27,13 +27,13 @@
"@formily/reactive-react": "^2.2.27",
"@formily/shared": "^2.2.27",
"@formily/validator": "^2.2.27",
"@nocobase/evaluators": "1.4.0-alpha",
"@nocobase/sdk": "1.4.0-alpha",
"@nocobase/utils": "1.4.0-alpha",
"@nocobase/evaluators": "1.4.0-alpha.11",
"@nocobase/sdk": "1.4.0-alpha.11",
"@nocobase/utils": "1.4.0-alpha.11",
"ahooks": "^3.7.2",
"antd": "5.12.8",
"antd-style": "3.4.5",
"axios": "^0.26.1",
"antd-style": "3.7.1",
"axios": "^1.7.0",
"bignumber.js": "^9.1.2",
"classnames": "^2.3.1",
"cronstrue": "^2.11.0",

View File

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

View File

@ -61,6 +61,18 @@ export class APIClient extends APIClientSDK {
/** 该值会在 AntdAppProvider 中被重新赋值 */
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() {
const headers = super.getHeaders();
const appName = this.app.getName();
@ -180,7 +192,8 @@ export class APIClient extends APIClientSDK {
}
silent() {
this.silence = true;
return this;
const api = this.cloneInstance();
api.silence = true;
return api;
}
}

View File

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

View File

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

View File

@ -15,7 +15,7 @@ import { SchemaInitializerOptions } from '../types';
import { SchemaInitializerChildren } from './SchemaInitializerChildren';
import { SchemaInitializerDivider } from './SchemaInitializerDivider';
import { useSchemaInitializerStyles } from './style';
import { useMenuSearch } from './SchemaInitializerItemSearchFields';
export interface SchemaInitializerItemGroupProps {
title: string;
children?: SchemaInitializerOptions['items'];
@ -44,7 +44,14 @@ export const SchemaInitializerItemGroup: FC<SchemaInitializerItemGroupProps> = (
/**
* @internal
*/
export const SchemaInitializerItemGroupInternal = () => {
const itemConfig = useSchemaInitializerItem<SchemaInitializerItemGroupProps>();
return <SchemaInitializerItemGroup {...itemConfig} />;
const itemConfig: any = useSchemaInitializerItem<SchemaInitializerItemGroupProps>();
const searchedChildren = useMenuSearch(itemConfig);
if (itemConfig.name !== 'displayFields') {
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,
}
: {
style: item.style,
key,
label,
onClick: handleClick,

View File

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

View File

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

View File

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

View File

@ -24,6 +24,8 @@ import { useActionContext } from '../schema-component';
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
import { TemplateBlockProvider } from './TemplateBlockProvider';
import { FormActiveFieldsProvider } from './hooks/useFormActiveFields';
import { useDesignable } from '../schema-component';
import { useCollectionRecordData } from '../data-source';
export const FormBlockContext = createContext<{
form?: any;
@ -123,6 +125,18 @@ export const useIsDetailBlock = () => {
export const FormBlockProvider = withDynamicSchemaProps((props) => {
const parentRecordData = useCollectionParentRecordData();
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 (
<TemplateBlockProvider>

View File

@ -297,7 +297,7 @@ export const useTableSelectorProps = () => {
field.value = data;
field?.setInitialValue?.(data);
field.data = field.data || {};
field.data.selectedRowKeys = ctx?.field?.data?.selectedRowKeys;
field.data.selectedRowKeys = [];
field.componentProps.pagination = field.componentProps.pagination || {};
field.componentProps.pagination.pageSize = ctx?.service?.data?.meta?.pageSize;
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 { useNavigateNoUpdate } from '../../application/CustomRouterContextProvider';
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 { mergeFilter, transformToFilter } from '../../filter-provider/utils';
import { useTreeParentRecord } from '../../modules/blocks/data-blocks/table/TreeRecordProvider';
@ -1284,7 +1284,7 @@ export const useOptionalFieldList = () => {
const isOptionalField = (field) => {
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 = () => {
@ -1329,7 +1329,7 @@ export const useAssociationFilterBlockProps = () => {
useEffect(() => {
// 由于选项字段不需要触发当前请求,所以请求单独在关系字段的时候触发
if (!isOptionalField(collectionField) && parseVariableLoading === false) {
if (collectionField && !isOptionalField(collectionField) && parseVariableLoading === false) {
run();
}
@ -1494,90 +1494,137 @@ export function getAssociationPath(str) {
return str;
}
export const useAssociationNames = (dataSource?: string) => {
let updateAssociationValues = new Set([]);
let appends = new Set([]);
const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated(dataSource);
const fieldSchema = useFieldSchema();
const _getAssociationAppends = (schema, str) => {
schema.reduceProperties((pre, s) => {
const prefix = pre || str;
const collectionField = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field'], dataSource);
const isAssociationSubfield = s.name.includes('.');
const isAssociationField =
collectionField &&
['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'belongsToArray'].includes(collectionField.type);
export const getAppends = ({
schema,
prefix: defaultPrefix,
updateAssociationValues,
appends,
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) => {
const prefix = pre || defaultPrefix;
const collectionField = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field'], dataSource);
const isAssociationSubfield = s.name.includes('.');
const isAssociationField =
collectionField &&
['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'belongsToArray'].includes(collectionField.type);
// 根据联动规则中条件的字段获取一些 appends
// 需要排除掉子表格和子表单中的联动规则
if (s['x-linkage-rules'] && !isSubMode(s)) {
const collectAppends = (obj) => {
const type = Object.keys(obj)[0] || '$and';
const list = obj[type];
// 根据联动规则中条件的字段获取一些 appends
// 需要排除掉子表格和子表单中的联动规则
if (s['x-linkage-rules'] && !isSubMode(s)) {
const collectAppends = (obj) => {
const type = Object.keys(obj)[0] || '$and';
const list = obj[type];
list.forEach((item) => {
if ('$and' in item || '$or' in item) {
return collectAppends(item);
}
list.forEach((item) => {
if ('$and' in item || '$or' in item) {
return collectAppends(item);
}
const fieldNames = getTargetField(item);
const fieldNames = getTargetField(item);
// 只应该收集关系字段,只有大于 1 的时候才是关系字段
if (fieldNames.length > 1) {
appends.add(fieldNames.join('.'));
}
});
};
// 只应该收集关系字段,只有大于 1 的时候才是关系字段
if (fieldNames.length > 1) {
appends.add(fieldNames.join('.'));
}
});
};
const rules = s['x-linkage-rules'];
rules.forEach(({ condition }) => {
collectAppends(condition);
const rules = s['x-linkage-rules'];
rules.forEach(({ condition }) => {
collectAppends(condition);
});
}
const isTreeCollection =
isAssociationField && getCollection(collectionField.target, dataSource)?.template === 'tree';
if (collectionField && (isAssociationField || isAssociationSubfield) && s['x-component'] !== 'TableField') {
const fieldPath = !isAssociationField && isAssociationSubfield ? getAssociationPath(s.name) : s.name;
const path = prefix === '' || !prefix ? fieldPath : prefix + '.' + fieldPath;
if (isTreeCollection) {
appends.add(path);
appends.add(`${path}.parent` + '(recursively=true)');
} else {
if (s['x-component-props']?.sortArr) {
const sort = s['x-component-props']?.sortArr;
appends.add(`${path}(sort=${sort})`);
} else {
appends.add(path);
}
}
if (isSubMode(s)) {
updateAssociationValues.add(path);
const bufPrefix = prefix && prefix !== '' ? prefix + '.' + s.name : s.name;
getAppends({
schema: s,
prefix: bufPrefix,
updateAssociationValues,
appends,
getCollectionJoinField,
getCollection,
dataSource,
});
}
const isTreeCollection =
isAssociationField && getCollection(collectionField.target, dataSource)?.template === 'tree';
if (collectionField && (isAssociationField || isAssociationSubfield) && s['x-component'] !== 'TableField') {
const fieldPath = !isAssociationField && isAssociationSubfield ? getAssociationPath(s.name) : s.name;
const path = prefix === '' || !prefix ? fieldPath : prefix + '.' + fieldPath;
if (isTreeCollection) {
appends.add(path);
appends.add(`${path}.parent` + '(recursively=true)');
} else {
if (s['x-component-props']?.sortArr) {
const sort = s['x-component-props']?.sortArr;
appends.add(`${path}(sort=${sort})`);
} else {
appends.add(path);
}
}
if (['Nester', 'SubTable', 'PopoverNester'].includes(s['x-component-props']?.mode)) {
updateAssociationValues.add(path);
const bufPrefix = prefix && prefix !== '' ? prefix + '.' + s.name : s.name;
_getAssociationAppends(s, bufPrefix);
}
} else if (
![
'ActionBar',
'Action',
'Action.Link',
'Action.Modal',
'Selector',
'Viewer',
'AddNewer',
'AssociationField.Selector',
'AssociationField.AddNewer',
'TableField',
].includes(s['x-component'])
) {
_getAssociationAppends(s, str);
}
}, str);
};
} else if (
![
'ActionBar',
'Action',
'Action.Link',
'Action.Modal',
'Selector',
'Viewer',
'AddNewer',
'AssociationField.Selector',
'AssociationField.AddNewer',
'TableField',
'Kanban.CardViewer',
].includes(s['x-component'])
) {
getAppends({
schema: s,
prefix: defaultPrefix,
updateAssociationValues,
appends,
getCollectionJoinField,
getCollection,
dataSource,
});
}
}, defaultPrefix);
};
export const useAssociationNames = (dataSource?: string) => {
const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated(dataSource);
const fieldSchema = useFieldSchema();
const getAssociationAppends = () => {
updateAssociationValues = new Set([]);
appends = new Set([]);
_getAssociationAppends(fieldSchema, '');
const updateAssociationValues = new Set([]);
let appends = new Set([]);
getAppends({
schema: fieldSchema,
prefix: '',
updateAssociationValues,
appends,
getCollectionJoinField,
getCollection,
dataSource,
});
appends = fillParentFields(appends);
console.log('appends', appends);
return { appends: [...appends], updateAssociationValues: [...updateAssociationValues] };
};

View File

@ -35,7 +35,7 @@ export const getPageSchema = (schema) => {
return getPageSchema(schema.parent);
};
const getCardItemSchema = (schema) => {
export const getCardItemSchema = (schema) => {
if (!schema) return null;
if (['BlockItem', 'CardItem'].includes(schema['x-component'])) {
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) => {
const api = useAPIClient();
const dm = useDataSourceManager();
const { refreshCH } = useCollectionHistory();
@ -46,26 +37,13 @@ export const RemoteCollectionManagerProvider = (props: any) => {
}>(() => {
return dm.reload().then(refreshCH);
});
const result = useRequest<{
data: any;
}>(coptions);
const { render } = useAppSpin();
const refreshCategory = useCallback(async () => {
const { data } = await api.request(coptions);
result.mutate(data);
return data?.data || [];
}, [result]);
if (service.loading) {
return render();
}
return (
<CollectionCategoriesProvider service={result} refreshCategory={refreshCategory}>
<CollectionManagerProvider_deprecated {...props}></CollectionManagerProvider_deprecated>
</CollectionCategoriesProvider>
);
return <CollectionManagerProvider_deprecated {...props}></CollectionManagerProvider_deprecated>;
};
export const CollectionCategoriesProvider = (props) => {

View File

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

View File

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

View File

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

View File

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

View File

@ -47,6 +47,9 @@ export class NumberFieldInterface extends CollectionFieldInterface {
{ value: '0.001', label: '1.000' },
{ value: '0.0001', label: '1.0000' },
{ 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',
title: '{{t("Target collection")}}',
required: true,
'x-reactions': ['{{useAsyncDataSource(loadCollections, ["file"])}}'],
'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'],
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-disabled': '{{ !createOnly }}',

View File

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

View File

@ -147,7 +147,6 @@ export const reverseFieldProperties: Record<string, ISchema> = {
reverse: {
type: 'void',
'x-component': 'div',
'x-hidden': '{{ !showReverseFieldConfig }}',
properties: {
autoCreateReverseField: {
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': {
@ -211,6 +216,7 @@ export const reverseFieldProperties: Record<string, ISchema> = {
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-disabled': '{{ !showReverseFieldConfig }}',
},
'reverseField.name': {
type: 'string',
@ -219,6 +225,7 @@ export const reverseFieldProperties: Record<string, ISchema> = {
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': 'uid',
'x-disabled': '{{ !showReverseFieldConfig }}',
description:
"{{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',
},
};
availableTypes = ['string', 'uuid'];
availableTypes = ['uid', 'uuid'];
properties = {
'uiSchema.title': {
type: 'string',

View File

@ -156,7 +156,7 @@ describe('CollectionFieldInterfaceManager', () => {
expect(fieldInterface.componentOptions).toMatchInlineSnapshot(`
[
{
"label": "A",
"label": "{{t("A")}}",
"useProps": [Function],
"value": "A",
},
@ -190,7 +190,7 @@ describe('CollectionFieldInterfaceManager', () => {
expect(fieldInterface.componentOptions).toMatchInlineSnapshot(`
[
{
"label": "A",
"label": "{{t("A")}}",
"useProps": [Function],
"value": "A",
},
@ -268,7 +268,7 @@ describe('CollectionFieldInterfaceManager', () => {
expect(fieldInterface.componentOptions).toMatchInlineSnapshot(`
[
{
"label": "B",
"label": "{{t("B")}}",
"useProps": [Function],
"value": "A.B",
},
@ -292,7 +292,7 @@ describe('CollectionFieldInterfaceManager', () => {
};
componentOptions = [
{
label: 'A',
label: '{{t("A")}}',
value: 'A',
},
];
@ -308,7 +308,7 @@ describe('CollectionFieldInterfaceManager', () => {
expect(fieldInterface.componentOptions).toMatchInlineSnapshot(`
[
{
"label": "A",
"label": "{{t("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', () => {
const app = new Application({
dataSourceManager: {

View File

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

View File

@ -8,10 +8,11 @@
*/
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 { concat } from 'lodash';
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useFormBlockContext } from '../../block-provider/FormBlockProvider';
import { useDynamicComponentProps } from '../../hoc/withDynamicSchemaProps';
@ -24,54 +25,48 @@ type Props = {
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: 初步适配
* @internal
*/
export const CollectionFieldInternalField: React.FC = (props: Props) => {
const { component } = props;
const compile = useCompile();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const collectionField = useCollectionField();
const { uiSchema: uiSchemaOrigin, defaultValue } = collectionField;
const { uiSchema: uiSchemaOrigin, defaultValue } = useCollectionField();
const { isAllowToSetDefaultValue } = useIsAllowToSetDefaultValue();
const uiSchema = useMemo(() => compile(uiSchemaOrigin), [JSON.stringify(uiSchemaOrigin)]);
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 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: 初步适配
useEffect(() => {
if (!uiSchema) {
if (!uiSchemaOrigin) {
return;
}
setFieldProps('content', uiSchema['x-content']);
setFieldProps('title', uiSchema.title);
setFieldProps('description', uiSchema.description);
const uiSchema = compile(uiSchemaOrigin);
setFieldProps(field, 'content', uiSchema['x-content']);
setFieldProps(field, 'title', uiSchema.title);
setFieldProps(field, 'description', uiSchema.description);
if (ctx?.form) {
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'])) {
@ -84,20 +79,21 @@ export const CollectionFieldInternalField: React.FC = (props: Props) => {
if (fieldSchema['x-read-pretty'] === true) {
field.readPretty = true;
}
setRequired();
setRequired(field, fieldSchema, uiSchema);
// @ts-ignore
field.dataSource = uiSchema.enum;
const originalProps = compile(uiSchema['x-component-props']) || {};
field.componentProps = merge(originalProps, field.componentProps || {}, dynamicProps || {});
}, [uiSchema]);
}, [uiSchemaOrigin]);
if (!uiSchema) return null;
if (!uiSchemaOrigin) return null;
return <Component {...props} {...dynamicProps} />;
};
export const CollectionField = connect((props) => {
const fieldSchema = useFieldSchema();
const field = useField<Field>();
return (
<ErrorBoundary FallbackComponent={ErrorFallback.Modal} onError={(err) => console.log(err)}>
<CollectionFieldProvider name={fieldSchema.name}>

View File

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

View File

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

View File

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

View File

@ -27,29 +27,28 @@ const getHook = (str: string, scope: Record<string, any>, allText: string) => {
return res || useDefaultDynamicComponentProps;
};
const getUseDynamicProps = (useComponentPropsStr: string, scope: Record<string, any>) => {
if (!useComponentPropsStr) {
return useDefaultDynamicComponentProps;
}
if (_.isFunction(useComponentPropsStr)) {
return useComponentPropsStr;
}
const pathList = useComponentPropsStr.split('.');
let result;
for (const item of pathList) {
result = getHook(item, result || scope, useComponentPropsStr);
}
return result;
};
export function useDynamicComponentProps(useComponentPropsStr?: string, props?: any) {
const scope = useExpressionScope();
const useDynamicProps = useMemo(() => {
if (!useComponentPropsStr) {
return useDefaultDynamicComponentProps;
}
if (_.isFunction(useComponentPropsStr)) {
return useComponentPropsStr;
}
const pathList = useComponentPropsStr.split('.');
let result;
for (const item of pathList) {
result = getHook(item, result || scope, useComponentPropsStr);
}
return result;
}, [useComponentPropsStr]);
const res = useDynamicProps(props);
const res = getUseDynamicProps(useComponentPropsStr, scope)(props);
return res;
}

View File

@ -842,8 +842,12 @@
"is any of": "is any of",
"Plugin dependency version mismatch": "Plugin dependency version mismatch",
"The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?",
"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",
"Enable secondary confirmation": "Enable secondary confirmation",
"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",
"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.",
"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",
"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.",
"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": "シンプルなページネーションモードを使用",
"Set Template Engine": "テンプレートエンジンを設定",
"Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "ページング時にテーブルレコードの総数取得をスキップして、読み込み速度を向上させます。データ量が多い場合にこのオプションの使用をお勧めします。",
"The current user only has the UI configuration permission, but don't have view permission for collection \"{{name}}\"": "現在のユーザーにはUI設定の権限しかなく、コレクション「{{name}}」を閲覧する権限はありません。"
"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

@ -876,6 +876,10 @@
"Expand All": "모두 펼치기",
"Clear default value": "기본값 지우기",
"Open in new window": "새 창에서 열기",
"Sorry, the page you visited does not exist.": "죄송합니다. 방문한 페이지가 존재하지 않습니다.",
"Ellipsis overflow content": "생략 부호로 내용 줄임"
"Sorry, the page you visited does not exist.": "죄송합니다. 방문한 페이지가 존재하지 않습니다.",
"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",
"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.",
"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": "Очистить значение по умолчанию",
"Open in new window": "Открыть в новом окне",
"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",
"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.",
"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": "Очистити значення за замовчуванням",
"Open in new window": "Відкрити в новому вікні",
"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 not empty": "不为空",
"Edit chart": "编辑图表",
"Add text": "添加文本",
"Add Markdown": "添加 Markdown",
"Filterable fields": "可筛选字段",
"Edit button": "编辑按钮",
"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}}\" 查看权限。",
"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?": "当前插件的依赖版本与应用的版本不一致,可能无法正常工作。您确定要继续激活插件吗?",
"Allow multiple selection": "允许多选",
"Parent object": "上级对象",
"Default value to current time": "设置字段默认值为当前时间",
"Automatically update timestamp on update": "当记录更新时自动设置字段值为当前时间",
"Default value to current server time": "设置字段默认值为当前服务端时间",
@ -1000,9 +1002,30 @@
"Are you sure you want to perform the Trigger workflow action?": "你确定执行触发工作流吗?",
"Ellipsis overflow content": "省略超出长度的内容",
"Picker": "选择器",
"Quarter":"季度",
"Switching the picker, the value and default value will be cleared":"切换选择器时,字段的值和默认值将会被清空",
"Stay on the current popup or page":"停留在当前弹窗或页面",
"Return to the previous popup or page":"返回上一层弹窗或页面",
"Action after successful submission":"提交成功后动作"
"Quarter": "季度",
"Switching the picker, the value and default value will be cleared": "切换选择器时,字段的值和默认值将会被清空",
"Stay on the current popup or page": "停留在当前弹窗或页面",
"Return to the previous popup or page": "返回上一层弹窗或页面",
"Action after successful submission": "提交成功后动作",
"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": "清除預設值",
"Open in new window": "新窗口打開",
"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 { PopupAndSubPageWithParams, oneEmptyTableWithUsers, openInNewWidow } from './templates';
import {
PopupAndSubPageWithParams,
URLSearchParamsUseAssociationFieldValue,
oneEmptyTableWithUsers,
openInNewWidow,
} from './templates';
test.describe('Link', () => {
test('basic', async ({ page, mockPage, mockRecords }) => {
@ -140,4 +145,13 @@ test.describe('Link', () => {
await page.reload();
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`);
});
});

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