Merge branch 'next' into F-827

This commit is contained in:
Zeke Zhang 2024-11-07 15:53:07 +08:00
commit c0caf63b97
262 changed files with 4734 additions and 1495 deletions

View File

@ -62,28 +62,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

@ -16,7 +16,7 @@ on:
default: beta
push:
tags:
- 'v*-beta'
- 'v*'
jobs:
write-changelog-and-release:
@ -24,12 +24,15 @@ jobs:
steps:
- name: Get info
id: get-info
shell: bash
run: |
if [[ "${{ inputs.version }}" == "alpha" ]]; then
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
@ -81,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: ${{ 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

@ -1,7 +1,7 @@
name: Manual build pro image
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.pr_number }}-${{ github.event.inputs.nocobase_pr_number }}
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 }}

View File

@ -1,10 +1,10 @@
name: Build pro plugin docker image
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.pro_plugin }}-${{ github.events.inputs.pr_number }}-${{ github.events.inputs.nocobase_pr_number }}
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 }}-${{ github.event.inputs.pro_plugin }}-${{ github.events.inputs.pr_number }}-${{ github.events.inputs.nocobase_pr_number }}
run-name: Build pro plugin image ${{ github.ref }}-${{ inputs.pro_plugin }}-${{ inputs.pr_number }}-${{ inputs.nocobase_pr_number }}
on:
workflow_dispatch:

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

@ -1,160 +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
steps:
- name: Set Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
- 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
ref: next
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true
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,24 +5,38 @@ concurrency:
cancel-in-progress: true
on:
workflow_dispatch:
push:
tags:
- 'v*-beta'
- 'v*'
jobs:
publish-npm:
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,19 @@ 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
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
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,44 @@ 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.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

View File

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

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

@ -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)
@ -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

@ -1,8 +1,10 @@
{
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.2",
"npmClient": "yarn",
"useWorkspaces": true,
"npmClientArgs": ["--ignore-engines"],
"npmClientArgs": [
"--ignore-engines"
],
"command": {
"version": {
"forcePublish": true,

View File

@ -1,13 +1,13 @@
{
"name": "@nocobase/acl",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.2",
"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.2",
"@nocobase/utils": "1.4.0-alpha.2",
"minimatch": "^5.1.1"
},
"repository": {

View File

@ -1,14 +1,14 @@
{
"name": "@nocobase/actions",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.2",
"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.2",
"@nocobase/database": "1.4.0-alpha.2",
"@nocobase/resourcer": "1.4.0-alpha.2"
},
"repository": {
"type": "git",

View File

@ -37,6 +37,7 @@ export default defineConfig({
`,
},
],
cacheDirectoryPath: process.env.APP_CLIENT_CACHE_DIR || `node_modules/.cache`,
outputPath: path.resolve(__dirname, '../dist/client'),
hash: true,
alias: {

View File

@ -1,17 +1,17 @@
{
"name": "@nocobase/app",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.2",
"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.2",
"@nocobase/preset-nocobase": "1.4.0-alpha.2",
"@nocobase/server": "1.4.0-alpha.2"
},
"devDependencies": {
"@nocobase/client": "1.4.0-alpha"
"@nocobase/client": "1.4.0-alpha.2"
},
"repository": {
"type": "git",

View File

@ -1,16 +1,16 @@
{
"name": "@nocobase/auth",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.2",
"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.2",
"@nocobase/cache": "1.4.0-alpha.2",
"@nocobase/database": "1.4.0-alpha.2",
"@nocobase/resourcer": "1.4.0-alpha.2",
"@nocobase/utils": "1.4.0-alpha.2",
"@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);

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/build",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.2",
"description": "Library build tool based on rollup.",
"main": "lib/index.js",
"types": "./lib/index.d.ts",

View File

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

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.2",
"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.2",
"@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.2"
},
"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

@ -350,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

@ -1,6 +1,6 @@
{
"name": "@nocobase/client",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.2",
"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,12 +27,12 @@
"@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.2",
"@nocobase/sdk": "1.4.0-alpha.2",
"@nocobase/utils": "1.4.0-alpha.2",
"ahooks": "^3.7.2",
"antd": "5.12.8",
"antd-style": "3.4.5",
"antd-style": "3.7.1",
"axios": "^1.7.0",
"bignumber.js": "^9.1.2",
"classnames": "^2.3.1",

View File

@ -345,7 +345,7 @@ export const useACLFieldWhitelist = () => {
return {
whitelist,
schemaInWhitelist: useCallback(
(fieldSchema: Schema, isSkip?) => {
(fieldSchema: Schema | any, isSkip?) => {
if (isSkip) {
return true;
}
@ -359,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

@ -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>();
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

@ -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';
@ -1494,14 +1494,25 @@ 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) => {
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 || str;
const prefix = pre || defaultPrefix;
const collectionField = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field'], dataSource);
const isAssociationSubfield = s.name.includes('.');
const isAssociationField =
@ -1534,8 +1545,10 @@ export const useAssociationNames = (dataSource?: string) => {
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;
@ -1550,10 +1563,18 @@ export const useAssociationNames = (dataSource?: string) => {
appends.add(path);
}
}
if (['Nester', 'SubTable', 'PopoverNester'].includes(s['x-component-props']?.mode)) {
if (isSubMode(s)) {
updateAssociationValues.add(path);
const bufPrefix = prefix && prefix !== '' ? prefix + '.' + s.name : s.name;
_getAssociationAppends(s, bufPrefix);
getAppends({
schema: s,
prefix: bufPrefix,
updateAssociationValues,
appends,
getCollectionJoinField,
getCollection,
dataSource,
});
}
} else if (
![
@ -1567,17 +1588,43 @@ export const useAssociationNames = (dataSource?: string) => {
'AssociationField.Selector',
'AssociationField.AddNewer',
'TableField',
'Kanban.CardViewer',
].includes(s['x-component'])
) {
_getAssociationAppends(s, str);
getAppends({
schema: s,
prefix: defaultPrefix,
updateAssociationValues,
appends,
getCollectionJoinField,
getCollection,
dataSource,
});
}
}, str);
};
}, defaultPrefix);
};
export const useAssociationNames = (dataSource?: string) => {
const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated(dataSource);
const fieldSchema = useFieldSchema();
const getAssociationAppends = () => {
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

@ -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

@ -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

@ -1017,5 +1017,13 @@
"Left": "左",
"Center": "居中",
"Right": "右",
"Divider line color": "分割线颜色"
"Divider line color": "分割线颜色",
"Label align": "字段标题对齐方式",
"Label width": "字段标题宽度",
"When the Label exceeds the width": "字段标题超出宽度时",
"Line break": "换行",
"Ellipsis": "省略",
"Set block layout": "设置区块布局",
"Add & Update": "添加 & 更新",
"Table size":"表格大小"
}

View File

@ -53,7 +53,7 @@ export const PopupActionInitializer = (props) => {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'popup:common:addBlock',
'x-initializer': props?.['x-initializer'] || 'popup:common:addBlock',
properties: {},
},
},

View File

@ -16,13 +16,14 @@ import { useDetailsBlockContext } from '../../../../block-provider/DetailsBlockP
import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider';
import { useCollection_deprecated, useSortFields } from '../../../../collection-manager';
import { removeNullCondition, useDesignable } from '../../../../schema-component';
import { SchemaSettingsLinkageRules, schemaSettingsLabelLayout } from '../../../../schema-settings';
import { SchemaSettingsLinkageRules } from '../../../../schema-settings';
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope';
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { setDataLoadingModeSettingsItem } from './setDataLoadingModeSettingsItem';
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
const commonItems: SchemaSettingsItemType[] = [
{
@ -212,7 +213,10 @@ const commonItems: SchemaSettingsItemType[] = [
};
},
},
schemaSettingsLabelLayout,
{
name: 'setBlockLayout',
Component: SchemaSettingsLayoutItem,
},
{
name: 'divider',
type: 'divider',

View File

@ -11,14 +11,12 @@ import { useFieldSchema } from '@formily/react';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { SchemaSettingsItemType } from '../../../../application/schema-settings/types';
import { useCollection } from '../../../../data-source/collection/CollectionProvider';
import {
SchemaSettingsFormItemTemplate,
SchemaSettingsLinkageRules,
schemaSettingsLabelLayout,
} from '../../../../schema-settings';
import { SchemaSettingsFormItemTemplate, SchemaSettingsLinkageRules } from '../../../../schema-settings';
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
const commonItems: SchemaSettingsItemType[] = [
{
name: 'title',
@ -56,7 +54,10 @@ const commonItems: SchemaSettingsItemType[] = [
};
},
},
schemaSettingsLabelLayout,
{
name: 'setBlockLayout',
Component: SchemaSettingsLayoutItem,
},
{
name: 'divider',
type: 'divider',

View File

@ -16,11 +16,11 @@ import {
SchemaSettingsDataTemplates,
SchemaSettingsFormItemTemplate,
SchemaSettingsLinkageRules,
schemaSettingsLabelLayout,
} from '../../../../schema-settings';
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
export const createFormBlockSettings = new SchemaSettings({
name: 'blockSettings:createForm',
@ -77,7 +77,10 @@ export const createFormBlockSettings = new SchemaSettings({
};
},
},
schemaSettingsLabelLayout,
{
name: 'setBlockLayout',
Component: SchemaSettingsLayoutItem,
},
{
name: 'divider2',
type: 'divider',

View File

@ -16,11 +16,11 @@ import {
SchemaSettingsDataTemplates,
SchemaSettingsFormItemTemplate,
SchemaSettingsLinkageRules,
schemaSettingsLabelLayout,
} from '../../../../schema-settings';
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
export const editFormBlockSettings = new SchemaSettings({
name: 'blockSettings:editForm',
@ -77,7 +77,10 @@ export const editFormBlockSettings = new SchemaSettings({
};
},
},
schemaSettingsLabelLayout,
{
name: 'setBlockLayout',
Component: SchemaSettingsLayoutItem,
},
{
name: 'divider2',
type: 'divider',

View File

@ -19,7 +19,7 @@ import { useCollectionManager_deprecated, useCollection_deprecated } from '../..
import { useFieldComponentName } from '../../../../common/useFieldComponentName';
import { useCollection } from '../../../../data-source';
import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem';
import { useDesignable, useValidateSchema } from '../../../../schema-component';
import { useDesignable, useValidateSchema, useCompile } from '../../../../schema-component';
import {
useIsFieldReadPretty,
useIsFormReadPretty,
@ -53,6 +53,7 @@ export const fieldSettingsFormItem = new SchemaSettings({
const { dn } = useDesignable();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const compile = useCompile();
const { getCollectionJoinField } = useCollectionManager_deprecated();
const { getField } = useCollection_deprecated();
const collectionField =
@ -75,16 +76,15 @@ export const fieldSettingsFormItem = new SchemaSettings({
},
} as ISchema,
onSubmit({ title }) {
if (title) {
field.title = title;
fieldSchema.title = title;
const result = title.trim() === '' ? collectionField?.uiSchema?.title : title;
field.title = compile(result);
fieldSchema.title = result;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
title: fieldSchema.title,
},
});
}
dn.refresh();
},
};

View File

@ -7,11 +7,18 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useForm } from '@formily/react';
import { useCollectionRecordData } from '../../../../../data-source/collection-record/CollectionRecordProvider';
import { useSatisfiedActionValues } from '../../../../../schema-settings/LinkageRules/useActionValues';
import { useFormBlockContext } from '../../../../../block-provider';
import { useSubFormValue } from '../../../../../schema-component/antd/association-field/hooks';
export function useDataFormItemProps() {
const data = useCollectionRecordData();
const { valueMap: style } = useSatisfiedActionValues({ category: 'style', formValues: data });
const record = useCollectionRecordData();
const { form } = useFormBlockContext();
const subForm = useSubFormValue();
const { valueMap: style } = useSatisfiedActionValues({
category: 'style',
formValues: subForm?.formValue || form?.values || record,
form,
});
return { wrapperStyle: style };
}

View File

@ -23,7 +23,7 @@ import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettin
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
import { SetTheCountOfColumnsDisplayedInARow } from './SetTheCountOfColumnsDisplayedInARow';
import { schemaSettingsLabelLayout } from '../../../../schema-settings';
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
export const gridCardBlockSettings = new SchemaSettings({
name: 'blockSettings:gridCard',
@ -222,7 +222,10 @@ export const gridCardBlockSettings = new SchemaSettings({
};
},
},
schemaSettingsLabelLayout,
{
name: 'setBlockLayout',
Component: SchemaSettingsLayoutItem,
},
{
name: 'divider',
type: 'divider',

View File

@ -21,7 +21,7 @@ import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSetti
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
import { schemaSettingsLabelLayout } from '../../../../schema-settings';
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
export const listBlockSettings = new SchemaSettings({
name: 'blockSettings:list',
@ -225,7 +225,10 @@ export const listBlockSettings = new SchemaSettings({
};
},
},
schemaSettingsLabelLayout,
{
name: 'setBlockLayout',
Component: SchemaSettingsLayoutItem,
},
{
name: 'divider',
type: 'divider',

View File

@ -103,7 +103,7 @@ test.describe('table data selector schema settings', () => {
await page.getByRole('menuitem', { name: 'Delete' }).hover();
await page.mouse.move(-300, 0);
await page.getByLabel('block-item-CollectionField-').nth(1).click();
await page.getByTestId('select-data-picker').first().click();
// 3. 创建 Table 区块
await page.getByLabel('schema-initializer-Grid-popup').hover();

View File

@ -42,6 +42,19 @@ const commonOptions = {
},
useVisible: () => useActionAvailable('create'),
},
{
type: 'item',
title: "{{t('Popup')}}",
name: 'popup',
Component: 'PopupActionInitializer',
componentProps: {
'x-component': 'Action',
'x-initializer': 'page:addBlock',
},
schema: {
'x-align': 'right',
},
},
{
type: 'item',
title: "{{t('Delete')}}",

View File

@ -936,7 +936,7 @@ test.describe('actions schema settings', () => {
).toBeVisible();
});
test('open mode', async ({ page, mockPage }) => {
test.skip('open mode', async ({ page, mockPage }) => {
const nocoPage = await mockPage(testingOfOpenModeForAddChild).waitForInit();
await nocoPage.goto();

View File

@ -187,6 +187,40 @@ export const tableBlockSettings = new SchemaSettings({
};
},
},
{
name: 'tableSize',
type: 'select',
useComponentProps() {
const field = useField();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { dn } = useDesignable();
return {
title: t('Table size'),
value: field.componentProps?.size || 'middle',
options: [
{ label: t('Large'), value: 'large' },
{ label: t('Middle'), value: 'middle' },
{ label: t('Small'), value: 'small' },
],
onChange: (size) => {
const schema = fieldSchema.reduceProperties((_, s) => {
if (s['x-component'] === 'TableV2') {
return s;
}
}, null);
schema['x-component-props'] = schema['x-component-props'] || {};
schema['x-component-props']['size'] = size;
dn.emit('patch', {
schema: {
['x-uid']: schema['x-uid'],
'x-decorator-props': schema['x-component-props'],
},
});
},
};
},
},
{
name: 'ConnectDataBlocks',
Component: SchemaSettingsConnectDataBlocks,

View File

@ -13,15 +13,12 @@ import { SchemaSettings } from '../../../../application/schema-settings/SchemaSe
import { useCollection_deprecated } from '../../../../collection-manager';
import { useCollection } from '../../../../data-source/collection/CollectionProvider';
import { FilterBlockType } from '../../../../filter-provider';
import {
SchemaSettingsFormItemTemplate,
SchemaSettingsLinkageRules,
schemaSettingsLabelLayout,
} from '../../../../schema-settings';
import { SchemaSettingsFormItemTemplate, SchemaSettingsLinkageRules } from '../../../../schema-settings';
import { SchemaSettingsBlockHeightItem } from '../../../../schema-settings/SchemaSettingsBlockHeightItem';
import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/SchemaSettingsBlockTitleItem';
import { SchemaSettingsConnectDataBlocks } from '../../../../schema-settings/SchemaSettingsConnectDataBlocks';
import { useBlockTemplateContext } from '../../../../schema-templates/BlockTemplateProvider';
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
export const filterFormBlockSettings = new SchemaSettings({
name: 'blockSettings:filterForm',
@ -71,7 +68,10 @@ export const filterFormBlockSettings = new SchemaSettings({
};
},
},
schemaSettingsLabelLayout,
{
name: 'setBlockLayout',
Component: SchemaSettingsLayoutItem,
},
{
name: 'divider',
type: 'divider',

View File

@ -17,7 +17,7 @@ import { SchemaSettings } from '../../../../application/schema-settings/SchemaSe
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../../collection-manager';
import { useFieldComponentName } from '../../../../common/useFieldComponentName';
import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem';
import { EditOperator, useDesignable, useValidateSchema } from '../../../../schema-component';
import { EditOperator, useDesignable, useValidateSchema, useCompile } from '../../../../schema-component';
import { SchemaSettingsDefaultValue } from '../../../../schema-settings/SchemaSettingsDefaultValue';
const fieldComponentNameMap = (name: string) => {
@ -49,6 +49,7 @@ export const filterFormItemFieldSettings = new SchemaSettings({
const { t } = useTranslation();
const { dn } = useDesignable();
const field = useField<Field>();
const compile = useCompile();
const fieldSchema = useFieldSchema();
const { getCollectionJoinField } = useCollectionManager_deprecated();
const { getField } = useCollection_deprecated();
@ -72,16 +73,16 @@ export const filterFormItemFieldSettings = new SchemaSettings({
},
} as ISchema,
onSubmit({ title }) {
if (title) {
field.title = title;
fieldSchema.title = title;
const result = title.trim() === '' ? collectionField?.uiSchema?.title : title;
field.title = compile(result);
fieldSchema.title = result;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
title: fieldSchema.title,
},
});
}
dn.refresh();
},
};

View File

@ -21,7 +21,7 @@ import {
useIsFormReadPretty,
} from '../../../../schema-component/antd/form-item/FormItem.Settings';
import { linkageRules, setDefaultSortingRules } from '../SubTable/subTablePopoverComponentFieldSettings';
import { schemaSettingsLabelLayout } from '../../../../schema-settings';
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
const allowMultiple: any = {
name: 'allowMultiple',
@ -144,6 +144,9 @@ export const subformComponentFieldSettings = new SchemaSettings({
},
setDefaultSortingRules,
linkageRules,
schemaSettingsLabelLayout,
{
name: 'setBlockLayout',
Component: SchemaSettingsLayoutItem,
},
],
});

View File

@ -20,7 +20,7 @@ import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Tabl
import { titleField } from '../Select/selectComponentFieldSettings';
import { linkageRules } from '../SubTable/subTablePopoverComponentFieldSettings';
import { schemaSettingsLabelLayout } from '../../../../schema-settings';
import { SchemaSettingsLayoutItem } from '../../../../schema-settings/SchemaSettingsLayoutItem';
const allowMultiple: any = {
name: 'allowMultiple',
@ -106,5 +106,14 @@ const fieldComponent: any = {
export const subformPopoverComponentFieldSettings = new SchemaSettings({
name: 'fieldSettings:component:PopoverNester',
items: [fieldComponent, allowMultiple, titleField, linkageRules, schemaSettingsLabelLayout],
items: [
fieldComponent,
allowMultiple,
titleField,
linkageRules,
{
name: 'setBlockLayout',
Component: SchemaSettingsLayoutItem,
},
],
});

View File

@ -27,7 +27,7 @@ export const PluginAddModal: FC<IPluginFormProps> = ({ onClose, isShow }) => {
const [type, setType] = useState<'npm' | 'upload' | 'url'>('npm');
return (
<Modal onCancel={() => onClose()} footer={null} destroyOnClose title={t('Add & update')} width={580} open={isShow}>
<Modal onCancel={() => onClose()} footer={null} destroyOnClose title={t('Add & Update')} width={580} open={isShow}>
{/* <label style={{ fontWeight: 'bold' }}>{t('Source')}:</label> */}
<div style={{ marginTop: theme.marginLG, marginBottom: theme.marginLG }}>
<Radio.Group optionType="button" defaultValue={type} onChange={(e) => setType(e.target.value)}>

View File

@ -154,6 +154,7 @@ export const Action: ComposedAction = withDynamicSchemaProps(
setSubmitted={setSubmitted}
getAriaLabel={getAriaLabel}
parentRecordData={parentRecordData}
actionCallback={actionCallback}
{...others}
/>
);

View File

@ -8,6 +8,7 @@
*/
export * from './Action';
export * from './Action.Designer';
export * from './ActionBar';
export * from './context';
export * from './hooks';
@ -15,5 +16,5 @@ export * from './hooks/useGetAriaLabelOfAction';
export * from './hooks/useGetAriaLabelOfDrawer';
export * from './hooks/useGetAriaLabelOfModal';
export * from './hooks/useGetAriaLabelOfPopover';
export * from './Action.Designer';
export * from './types';
export * from './zIndexContext';

View File

@ -9,6 +9,7 @@
import { css, cx } from '@emotion/css';
import { FormLayout } from '@formily/antd-v5';
import { theme } from 'antd';
import { RecursionField, observer, useField, useFieldSchema } from '@formily/react';
import React, { useEffect } from 'react';
import { ACLCollectionProvider, useACLActionParamsContext } from '../../../acl';
@ -45,14 +46,25 @@ export const InternalNester = observer(
const { options: collectionField } = useAssociationFieldContext();
const showTitle = fieldSchema['x-decorator-props']?.showTitle ?? true;
const { actionName } = useACLActionParamsContext();
const { layout = 'vertical' } = fieldSchema?.['x-component-props'] || {};
const { token } = theme.useToken();
const {
layout = 'vertical',
labelAlign = 'left',
labelWidth = 120,
labelWrap = true,
} = fieldSchema?.['x-component-props'] || {};
useEffect(() => {
insertNester(schema.Nester);
}, []);
return (
<CollectionProvider_deprecated name={collectionField.target}>
<ACLCollectionProvider actionPath={`${collectionField.target}:${actionName || 'view'}`}>
<FormLayout layout={layout}>
<FormLayout
layout={layout}
labelAlign={labelAlign}
labelWidth={layout === 'horizontal' ? labelWidth : null}
labelWrap={labelWrap}
>
<div
className={cx(
InternalNesterCss,
@ -62,6 +74,12 @@ export const InternalNester = observer(
css`
.nb-grid-container {
height: 100% !important;
.ant-formily-item-label {
line-height: ${token.controlHeight}px;
}
.ant-formily-item-label label {
white-space: ${labelWrap ? 'break-all' : 'nowrap'};
}
}
`,
)}

View File

@ -165,7 +165,7 @@ export const SubTable: any = observer(
};
const getFilter = () => {
const targetKey = collectionField?.targetKey || 'id';
const list = (field.value || []).map((option) => option[targetKey]).filter(Boolean);
const list = (field.value || []).map((option) => option?.[targetKey]).filter(Boolean);
const filter = list.length ? { $and: [{ [`${targetKey}.$ne`]: list }] } : {};
return filter;
};
@ -235,6 +235,7 @@ export const SubTable: any = observer(
// 计算总页数,并跳转到最后一页
const totalPages = Math.ceil(field.value.length / (field.componentProps?.pageSize || 10));
setCurrentPage(totalPages);
return field.onInput(field.value);
}}
>
{t('Add new')}

View File

@ -45,7 +45,7 @@ describe('CollectionSelect', () => {
expect(container).toMatchInlineSnapshot(`
<div>
<div
class="css-dev-only-do-not-override-wwtqkl ant-app"
class="css-dev-only-do-not-override-11aiz3o ant-app"
style="height: 100%;"
>
<div
@ -54,7 +54,7 @@ describe('CollectionSelect', () => {
role="button"
>
<div
class="css-1yh5po ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-wwtqkl"
class="css-1yh5po ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-11aiz3o"
>
<div
class="ant-formily-item-label"
@ -84,7 +84,7 @@ describe('CollectionSelect', () => {
class="ant-formily-item-control-content-component"
>
<div
class="ant-select css-dev-only-do-not-override-wwtqkl ant-select-focused ant-select-single ant-select-show-arrow ant-select-show-search"
class="ant-select css-dev-only-do-not-override-11aiz3o ant-select-focused ant-select-single ant-select-show-arrow ant-select-show-search"
data-testid="select-collection"
role="button"
>
@ -182,7 +182,7 @@ describe('CollectionSelect', () => {
expect(container).toMatchInlineSnapshot(`
<div>
<div
class="css-dev-only-do-not-override-wwtqkl ant-app"
class="css-dev-only-do-not-override-11aiz3o ant-app"
style="height: 100%;"
>
<div
@ -191,7 +191,7 @@ describe('CollectionSelect', () => {
role="button"
>
<div
class="css-1yh5po ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-wwtqkl"
class="css-1yh5po ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-11aiz3o"
>
<div
class="ant-formily-item-label"
@ -222,7 +222,7 @@ describe('CollectionSelect', () => {
>
<div>
<span
class="ant-tag css-dev-only-do-not-override-wwtqkl"
class="ant-tag css-dev-only-do-not-override-11aiz3o"
>
Users
</span>

View File

@ -26,7 +26,7 @@ describe('ColorPicker', () => {
expect(container).toMatchInlineSnapshot(`
<div>
<div
class="css-dev-only-do-not-override-wwtqkl ant-app"
class="css-dev-only-do-not-override-11aiz3o ant-app"
style="height: 100%;"
>
<div
@ -35,7 +35,7 @@ describe('ColorPicker', () => {
style="display: inline-block;"
>
<div
class="ant-color-picker-trigger css-dev-only-do-not-override-wwtqkl"
class="ant-color-picker-trigger css-dev-only-do-not-override-11aiz3o"
>
<div
class="ant-color-picker-color-block"
@ -90,7 +90,7 @@ describe('ColorPicker', () => {
expect(container).toMatchInlineSnapshot(`
<div>
<div
class="css-dev-only-do-not-override-wwtqkl ant-app"
class="css-dev-only-do-not-override-11aiz3o ant-app"
style="height: 100%;"
>
<div
@ -99,7 +99,7 @@ describe('ColorPicker', () => {
role="button"
>
<div
class="ant-color-picker-trigger ant-color-picker-sm css-dev-only-do-not-override-wwtqkl ant-color-picker-trigger-disabled"
class="ant-color-picker-trigger ant-color-picker-sm css-dev-only-do-not-override-11aiz3o ant-color-picker-trigger-disabled"
>
<div
class="ant-color-picker-color-block"

View File

@ -30,6 +30,7 @@ export const EditTitle = () => {
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { dn } = useDesignable();
const compile = useCompile();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
return collectionField ? (
@ -53,8 +54,8 @@ export const EditTitle = () => {
} as ISchema
}
onSubmit={({ title }) => {
if (title) {
field.title = title;
const result = title.trim() === '' ? collectionField?.uiSchema?.title : title;
field.title = compile(result);
fieldSchema.title = title;
dn.emit('patch', {
schema: {
@ -62,7 +63,7 @@ export const EditTitle = () => {
title: fieldSchema.title,
},
});
}
dn.refresh();
}}
/>

View File

@ -40,11 +40,22 @@ const FormComponent: React.FC<FormProps> = (props) => {
const f = useAttach(form.createVoidField({ ...field.props, basePath: '' }));
const height = useFormBlockHeight();
const { token } = theme.useToken();
const { layout = 'vertical' } = cardItemSchema?.['x-component-props'] || {};
const {
layout = 'vertical',
labelAlign = 'left',
labelWidth = 120,
labelWrap = true,
} = cardItemSchema?.['x-component-props'] || {};
return (
<FieldContext.Provider value={undefined}>
<FormContext.Provider value={form}>
<FormLayout layout={layout} {...others}>
<FormLayout
layout={layout}
{...others}
labelAlign={labelAlign}
labelWidth={layout === 'horizontal' ? labelWidth : null}
labelWrap={labelWrap}
>
<div
className={css`
.nb-grid-container {

View File

@ -162,7 +162,12 @@ const InternalGridCard = (props: GridCardProps) => {
onChange: onPaginationChange,
};
const cardItemSchema = getCardItemSchema?.(fieldSchema);
const { layout = 'vertical' } = cardItemSchema?.['x-component-props'] || {};
const {
layout = 'vertical',
labelAlign = 'left',
labelWidth = 120,
labelWrap = true,
} = cardItemSchema?.['x-component-props'] || {};
return (
<SchemaComponentOptions
@ -187,7 +192,12 @@ const InternalGridCard = (props: GridCardProps) => {
`,
)}
>
<FormLayout layout={layout}>
<FormLayout
layout={layout}
labelAlign={labelAlign}
labelWidth={layout === 'horizontal' ? labelWidth : null}
labelWrap={labelWrap}
>
<AntdList
pagination={
!meta || meta.count <= meta.pageSize

View File

@ -500,7 +500,7 @@ Grid.Col = observer(
width = `calc(${w}% - ${token.marginBlock}px * ${(showDivider ? cols.length + 1 : 0) / cols.length})`;
}
return { width };
}, [cols?.length, schema?.['x-component-props']?.['width']]);
}, [cols?.length, schema?.['x-component-props']?.['width'], token.marginBlock]);
const { isOver, setNodeRef } = useDroppable({
id: field.address.toString(),
data: {

View File

@ -67,7 +67,12 @@ const InternalList = (props) => {
[run, params],
);
const cardItemSchema = getCardItemSchema?.(fieldSchema);
const { layout = 'vertical' } = cardItemSchema?.['x-component-props'] || {};
const {
layout = 'vertical',
labelAlign = 'left',
labelWidth = 120,
labelWrap = true,
} = cardItemSchema?.['x-component-props'] || {};
const usePagination = () => {
if (!count) {
return {
@ -138,7 +143,12 @@ const InternalList = (props) => {
)}
>
<div className="nb-list-container">
<FormLayout layout={layout}>
<FormLayout
layout={layout}
labelAlign={labelAlign}
labelWidth={layout === 'horizontal' ? labelWidth : null}
labelWrap={labelWrap}
>
<AntdList
{...props}
pagination={!meta || !field.value?.length ? false : paginationProps}

View File

@ -21,12 +21,12 @@ describe('Pagination', () => {
expect(container).toMatchInlineSnapshot(`
<div>
<div
class="css-dev-only-do-not-override-wwtqkl ant-app"
class="css-dev-only-do-not-override-11aiz3o ant-app"
style="height: 100%;"
>
<div>
<ul
class="ant-pagination css-dev-only-do-not-override-wwtqkl"
class="ant-pagination css-dev-only-do-not-override-11aiz3o"
>
<li
aria-disabled="true"
@ -131,7 +131,7 @@ describe('Pagination', () => {
expect(container).toMatchInlineSnapshot(`
<div>
<div
class="css-dev-only-do-not-override-wwtqkl ant-app"
class="css-dev-only-do-not-override-11aiz3o ant-app"
style="height: 100%;"
/>
</div>

View File

@ -86,7 +86,8 @@ const InternalRemoteSelect = withDynamicSchemaProps(
const operator = useMemo(() => {
if (targetField?.interface) {
return getInterface(targetField.interface)?.filterable?.operators[0].value || '$includes';
const initialOperator = getInterface(targetField.interface)?.filterable?.operators[0].value || '$includes';
return initialOperator !== '$eq' ? initialOperator : '$includes';
}
return '$includes';
}, [targetField]);

View File

@ -105,7 +105,7 @@ const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }, paginat
return css`
.nb-action-link {
margin: -${token.paddingContentVerticalLG}px -${token.marginSM}px;
padding: ${token.paddingContentVerticalLG}px ${token.margin}px;
padding: ${token.paddingContentVerticalLG}px ${token.paddingSM + 4}px;
}
`;
}, [token.paddingContentVerticalLG, token.marginSM]);
@ -196,6 +196,9 @@ const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }, paginat
deleteCount: 1,
});
field.value.splice(fieldIndex, 1);
setTimeout(() => {
field.value[field.value.length] = null;
});
return field.onInput(field.value);
});
}}
@ -232,8 +235,8 @@ const SortableRow = (props: {
const { ref, inView } = useInView({
threshold: 0,
triggerOnce: true,
initialInView: !!process.env.__E2E__ || isInSubTable || props.rowIndex < INITIAL_ROWS_NUMBER,
skip: !!process.env.__E2E__ || isInSubTable || props.rowIndex < INITIAL_ROWS_NUMBER,
initialInView: !!process.env.__E2E__ || isInSubTable || (props.rowIndex || 0) < INITIAL_ROWS_NUMBER,
skip: !!process.env.__E2E__ || isInSubTable,
});
const classObj = useMemo(() => {
@ -617,6 +620,7 @@ export const Table: any = withDynamicSchemaProps(
} = { ...others1, ...others2 } as any;
const field = useArrayField(others);
const schema = useFieldSchema();
const { size = 'middle' } = schema?.['x-component-props'] || {};
const collection = useCollection();
const isTableSelector = schema?.parent?.['x-decorator'] === 'TableSelectorProvider';
const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext();
@ -889,7 +893,6 @@ export const Table: any = withDynamicSchemaProps(
expandedRowKeys: expandedKeys,
};
}, [expandedKeys, onExpandValue]);
return (
// If spinning is set to undefined, it will cause the subtable to always display loading, so we need to convert it here
<Spin spinning={!!loading}>
@ -910,6 +913,7 @@ export const Table: any = withDynamicSchemaProps(
columns={columns}
expandable={expandable}
field={field}
size={size}
/>
</Spin>
);

View File

@ -303,6 +303,30 @@ export const TableBlockDesigner = () => {
});
}}
/>
<SchemaSettingsSelectItem
title={t('Table size')}
value={field.componentProps?.size || 'middle'}
options={[
{ label: t('Large'), value: 'large' },
{ label: t('Middle'), value: 'middle' },
{ label: t('Small'), value: 'small' },
]}
onChange={(size) => {
const schema = fieldSchema.reduceProperties((_, s) => {
if (s['x-component'] === 'TableV2') {
return s;
}
}, null);
schema['x-component-props'] = schema['x-component-props'] || {};
schema['x-component-props']['size'] = size;
dn.emit('patch', {
schema: {
['x-uid']: schema['x-uid'],
'x-decorator-props': schema['x-component-props'],
},
});
}}
/>
<SchemaSettingsConnectDataBlocks type={FilterBlockType.TABLE} emptyDescription={t('No blocks to connect')} />
{supportTemplate && <SchemaSettingsDivider />}
{supportTemplate && (

View File

@ -219,6 +219,21 @@ describe('Table.settings', () => {
},
],
},
{
title: 'Table size',
type: 'select',
options: [
{
label: 'Large',
},
{
label: 'Middle',
},
{
label: 'Small',
},
],
},
{
title: 'Save as template',
type: 'modal',

View File

@ -20,11 +20,11 @@ describe('UnixTimestamp', () => {
expect(container).toMatchInlineSnapshot(`
<div>
<div
class="css-dev-only-do-not-override-wwtqkl ant-app"
class="css-dev-only-do-not-override-11aiz3o ant-app"
style="height: 100%;"
>
<div
class="ant-picker css-dev-only-do-not-override-wwtqkl"
class="ant-picker css-dev-only-do-not-override-11aiz3o"
>
<div
class="ant-picker-input"
@ -77,7 +77,7 @@ describe('UnixTimestamp', () => {
expect(container).toMatchInlineSnapshot(`
<div>
<div
class="css-dev-only-do-not-override-wwtqkl ant-app"
class="css-dev-only-do-not-override-11aiz3o ant-app"
style="height: 100%;"
>
<div

View File

@ -446,14 +446,7 @@ export function Uploader({ rules, ...props }: UploadProps) {
</Tooltip>
</div>
{selectable && QRCodeUploader && (
<QRCodeUploader
value={value}
onChange={(value) => {
// TODO
console.log(value);
// onChange(value);
}}
/>
<QRCodeUploader disabled={disabled} multiple={multiple} value={value} onChange={onChange} />
)}
</>
);

View File

@ -7,11 +7,8 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import helpers from '@budibase/handlebars-helpers';
import { dayjs, getPickerFormat } from '@nocobase/utils/client';
import Handlebars from 'handlebars';
import { dayjs, getPickerFormat, Handlebars } from '@nocobase/utils/client';
import _, { every, findIndex, some } from 'lodash';
import url from 'url';
import { replaceVariableValue } from '../../../block-provider/hooks';
import { VariableOption, VariablesContextType } from '../../../variables/types';
import { isVariable } from '../../../variables/utils/isVariable';
@ -170,36 +167,6 @@ const getVariablesData = (localVariables) => {
});
return data;
};
const allHelpers = helpers();
//遍历所有 helper 并手动注册到 Handlebars
Object.keys(allHelpers).forEach(function (helperName) {
Handlebars.registerHelper(helperName, allHelpers[helperName]);
});
// 自定义 helper 来处理对象
Handlebars.registerHelper('json', function (context) {
return JSON.stringify(context);
});
//重写urlParse
Handlebars.registerHelper('urlParse', function (str) {
try {
return JSON.stringify(url.parse(str));
} catch (error) {
return `Invalid URL: ${str}`;
}
});
Handlebars.registerHelper('dateFormat', (date, format, tz) => {
if (typeof tz === 'string') {
return dayjs(date).tz(tz).format(format);
}
return dayjs(date).format(format);
});
Handlebars.registerHelper('isNull', (value) => {
return _.isNull(value);
});
export async function getRenderContent(templateEngine, content, variables, localVariables, defaultParse) {
if (content && templateEngine === 'handlebars') {

View File

@ -13,7 +13,7 @@ import _ from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useCollection_deprecated, useCollectionManager_deprecated } from '../collection-manager';
import { useDesignable } from '../schema-component';
import { useDesignable, useCompile } from '../schema-component';
import { SchemaSettingsModalItem, SchemaSettingsSwitchItem } from '../schema-settings';
import { getTempFieldState } from '../schema-settings/LinkageRules/bindLinkageRulesToFiled';
@ -28,6 +28,7 @@ export const GeneralSchemaItems: React.FC<{
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { dn, refresh } = useDesignable();
const compile = useCompile();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
return (
<>
@ -52,8 +53,8 @@ export const GeneralSchemaItems: React.FC<{
} as ISchema
}
onSubmit={({ title }) => {
if (title) {
field.title = title;
const result = title.trim() === '' ? collectionField?.uiSchema?.title : title;
field.title = compile(result);
fieldSchema.title = title;
dn.emit('patch', {
schema: {
@ -61,7 +62,7 @@ export const GeneralSchemaItems: React.FC<{
title: fieldSchema.title,
},
});
}
dn.refresh();
}}
/>

View File

@ -15,7 +15,7 @@ import { SchemaSettingOptions } from '../application';
import { useSchemaToolbar } from '../application/schema-toolbar';
import { useCollection_deprecated, useCollectionManager_deprecated } from '../collection-manager';
import { SchemaSettingsLinkageRules } from '../schema-settings';
import { useIsFieldReadPretty } from '../schema-component';
import { useIsFieldReadPretty, useCompile } from '../schema-component';
import { useCollection } from '../data-source';
export const generalSettingsItems: SchemaSettingOptions['items'] = [
@ -25,6 +25,7 @@ export const generalSettingsItems: SchemaSettingOptions['items'] = [
useComponentProps() {
const { t } = useTranslation();
const { dn } = useDesignable();
const compile = useCompile();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { getCollectionJoinField } = useCollectionManager_deprecated();
@ -49,8 +50,8 @@ export const generalSettingsItems: SchemaSettingOptions['items'] = [
},
} as ISchema,
onSubmit({ title }) {
if (title) {
field.title = title;
const result = title.trim() === '' ? collectionField?.uiSchema?.title : title;
field.title = compile(result);
fieldSchema.title = title;
dn.emit('patch', {
schema: {
@ -58,7 +59,7 @@ export const generalSettingsItems: SchemaSettingOptions['items'] = [
title: fieldSchema.title,
},
});
}
dn.refresh();
},
};

View File

@ -7,7 +7,9 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { uid } from '@formily/shared';
import { Form, onFormValuesChange } from '@formily/core';
import { useVariables, useLocalVariables } from '../../variables';
import { useFieldSchema } from '@formily/react';
import { LinkageRuleCategory, LinkageRuleDataKeyMap } from './type';
@ -18,11 +20,13 @@ export function useSatisfiedActionValues({
category = 'default',
rules,
schema,
form,
}: {
category: `${LinkageRuleCategory}`;
formValues: Record<string, any>;
rules?: any;
schema?: any;
form?: Form;
}) {
const [valueMap, setValueMap] = useState({});
const fieldSchema = useFieldSchema();
@ -30,8 +34,7 @@ export function useSatisfiedActionValues({
const localVariables = useLocalVariables({ currentForm: { values: formValues } as any });
const localSchema = schema ?? fieldSchema;
const linkageRules = rules ?? localSchema[LinkageRuleDataKeyMap[category]];
useEffect(() => {
const compute = useCallback(() => {
if (linkageRules && formValues) {
getSatisfiedValueMap({ rules: linkageRules, variables, localVariables })
.then((valueMap) => {
@ -43,6 +46,22 @@ export function useSatisfiedActionValues({
throw new Error(err.message);
});
}
}, [variables, localVariables, formValues, linkageRules]);
}, [variables, localVariables, linkageRules, formValues]);
useEffect(() => {
compute();
}, [compute]);
useEffect(() => {
if (form) {
const id = uid();
form.addEffects(id, () => {
onFormValuesChange(() => {
compute();
});
});
return () => {
form.removeEffects(id);
};
}
}, [form, compute]);
return { valueMap };
}

View File

@ -0,0 +1,134 @@
/**
* 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 { ISchema, useField, useFieldSchema } from '@formily/react';
import _ from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SchemaSettingsModalItem } from './SchemaSettings';
import { useDesignable } from '../schema-component/hooks/useDesignable';
export const Layout = {
VERTICAL: 'vertical',
HORIZONTAL: 'horizontal',
};
export const SchemaSettingsLayoutItem = function LayoutItem() {
const field = useField();
const fieldSchema = useFieldSchema();
const { dn } = useDesignable();
const { t } = useTranslation();
return (
<SchemaSettingsModalItem
title={t('Layout')}
schema={
{
type: 'object',
title: t('Set block layout'),
properties: {
layout: {
type: 'string',
enum: [
{ label: t('Vertical'), value: Layout.VERTICAL },
{ label: t('Horizontal'), value: Layout.HORIZONTAL },
],
required: true,
default: fieldSchema?.['x-component-props']?.layout || Layout.VERTICAL,
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
},
labelAlign: {
title: t('Label align'),
type: 'string',
default: fieldSchema?.['x-component-props']?.['labelAlign'] || 'left',
'x-decorator': 'FormItem',
'x-component': 'Select',
enum: [
{ label: "{{t('Right')}}", value: 'right' },
{ label: "{{t('Left')}}", value: 'left' },
],
'x-reactions': {
dependencies: ['layout'],
fulfill: {
state: {
visible: '{{ $deps[0]==="horizontal"}}',
},
},
},
},
labelWidth: {
title: t('Label width'),
type: 'string',
default: fieldSchema?.['x-component-props']?.['labelWidth'] || 120,
required: true,
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
'x-component-props': {
addonAfter: 'px',
},
'x-validator': [
{
minimum: 50,
},
],
'x-reactions': {
dependencies: ['layout'],
fulfill: {
state: {
visible: '{{ $deps[0]==="horizontal"}}',
},
},
},
},
labelWrap: {
type: 'string',
title: t('When the Label exceeds the width'),
enum: [
{ label: t('Line break'), value: true },
{ label: t('Ellipsis'), value: false },
],
default: fieldSchema?.['x-component-props']?.labelWrap !== false,
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-reactions': {
dependencies: ['layout'],
fulfill: {
state: {
visible: '{{ $deps[0]==="horizontal"}}',
},
},
},
},
},
} as ISchema
}
onSubmit={({ layout, labelAlign, labelWidth, labelWrap }) => {
const componentProps = fieldSchema['x-component-props'] || {};
componentProps.layout = layout;
componentProps.labelAlign = labelAlign;
componentProps.labelWidth = layout === 'horizontal' ? labelWidth : null;
componentProps.labelWrap = labelWrap;
fieldSchema['x-component-props'] = componentProps;
field.componentProps.layout = layout;
field.componentProps.labelAlign = labelAlign;
field.componentProps.labelWidth = labelWidth;
field.componentProps.labelWrap = labelWrap;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-component-props': fieldSchema['x-component-props'],
},
});
dn.refresh();
}}
/>
);
};

View File

@ -25,6 +25,7 @@ export * from './setTheDataScopeSchemaSettingsItem';
export * from './SchemaSettingsRenderEngine';
export * from './hooks/useGetAriaLabelOfDesigner';
export * from './hooks/useIsAllowToSetDefaultValue';
export * from './SchemaSettingsLayoutItem';
export { default as useParseDataScopeFilter } from './hooks/useParseDataScopeFilter';
export * from './isPatternDisabled';
export { SchemaSettingsPlugin } from './SchemaSettingsPlugin';

View File

@ -139,8 +139,12 @@ export const SettingsMenu: React.FC<{
key: 'signout',
label: t('Sign out'),
onClick: async () => {
await api.auth.signOut();
const { data } = await api.auth.signOut();
if (data?.data?.redirect) {
window.location.href = data.data.redirect;
} else {
navigate(`/signin?redirect=${encodeURIComponent(redirectUrl)}`);
}
},
},
];

View File

@ -1,6 +1,6 @@
{
"name": "create-nocobase-app",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.2",
"main": "src/index.js",
"license": "AGPL-3.0",
"dependencies": {

View File

@ -1,16 +1,16 @@
{
"name": "@nocobase/data-source-manager",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.2",
"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.2",
"@nocobase/cache": "1.4.0-alpha.2",
"@nocobase/database": "1.4.0-alpha.2",
"@nocobase/resourcer": "1.4.0-alpha.2",
"@nocobase/utils": "1.4.0-alpha.2",
"@types/jsonwebtoken": "^8.5.8",
"jsonwebtoken": "^8.5.1"
},

View File

@ -84,17 +84,20 @@ export class DataSourceManager {
}
middleware() {
return async (ctx, next) => {
const self = this;
return async function dataSourceManager(ctx, next) {
const name = ctx.get('x-data-source') || 'main';
if (!this.dataSources.has(name)) {
if (!self.dataSources.has(name)) {
ctx.throw(`data source ${name} does not exist`);
}
const ds = this.dataSources.get(name);
const ds = self.dataSources.get(name);
ctx.dataSource = ds;
return ds.middleware(this.middlewares)(ctx, next);
const composedFn = ds.middleware(self.middlewares);
return composedFn(ctx, next);
};
}

View File

@ -14,6 +14,7 @@ import compose from 'koa-compose';
import { loadDefaultActions } from './load-default-actions';
import { ICollectionManager } from './types';
import { Logger } from '@nocobase/logger';
import { wrapMiddlewareWithLogging } from '@nocobase/utils';
export type DataSourceOptions = any;
@ -79,6 +80,7 @@ export abstract class DataSource extends EventEmitter {
for (const [fn, options] of middlewares) {
this.resourceManager.use(fn, options);
}
this['_used'] = true;
}
@ -91,7 +93,9 @@ export abstract class DataSource extends EventEmitter {
return this.collectionManager.getRepository(resourceName, resourceOf);
};
return compose([this.collectionToResourceMiddleware(), this.resourceManager.middleware()])(ctx, next);
const middlewares = [this.collectionToResourceMiddleware(), this.resourceManager.middleware()];
return compose(middlewares.map((fn) => wrapMiddlewareWithLogging(fn)))(ctx, next);
};
}
@ -117,15 +121,16 @@ export abstract class DataSource extends EventEmitter {
abstract createCollectionManager(options?: any): ICollectionManager;
protected collectionToResourceMiddleware() {
return async (ctx, next) => {
const self = this;
return async function collectionToResource(ctx, next) {
const params = parseRequest(
{
path: ctx.request.path,
method: ctx.request.method,
},
{
prefix: this.resourceManager.options.prefix,
accessors: this.resourceManager.options.accessors,
prefix: self.resourceManager.options.prefix,
accessors: self.resourceManager.options.accessors,
},
);
if (!params) {
@ -133,7 +138,7 @@ export abstract class DataSource extends EventEmitter {
}
const resourceName = getNameByParams(params);
// 如果资源名称未被定义
if (this.resourceManager.isDefined(resourceName)) {
if (self.resourceManager.isDefined(resourceName)) {
return next();
}
@ -141,11 +146,11 @@ export abstract class DataSource extends EventEmitter {
const collectionName = splitResult[0];
if (!this.collectionManager.hasCollection(collectionName)) {
if (!self.collectionManager.hasCollection(collectionName)) {
return next();
}
this.resourceManager.define({
self.resourceManager.define({
name: resourceName,
});

View File

@ -1,13 +1,13 @@
{
"name": "@nocobase/database",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.2",
"description": "",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"license": "AGPL-3.0",
"dependencies": {
"@nocobase/logger": "1.4.0-alpha",
"@nocobase/utils": "1.4.0-alpha",
"@nocobase/logger": "1.4.0-alpha.2",
"@nocobase/utils": "1.4.0-alpha.2",
"async-mutex": "^0.3.2",
"chalk": "^4.1.1",
"cron-parser": "4.4.0",

View File

@ -189,10 +189,6 @@ export class Collection<
return this.model.primaryKeyAttribute;
}
isMultiFilterTargetKey() {
return Array.isArray(this.filterTargetKey) && this.filterTargetKey.length > 1;
}
get name() {
return this.options.name;
}
@ -225,6 +221,10 @@ export class Collection<
}
}
isMultiFilterTargetKey() {
return Array.isArray(this.filterTargetKey) && this.filterTargetKey.length > 1;
}
tableName() {
const { name, tableName } = this.options;
const tName = tableName || name;
@ -313,6 +313,20 @@ export class Collection<
},
});
Object.defineProperty(this.model, 'primaryKeyField', {
get: function () {
if (this.primaryKeyAttribute) {
return this.rawAttributes[this.primaryKeyAttribute].field || this.primaryKeyAttribute;
}
return null;
}.bind(this.model),
set(val) {
this._primaryKeyField = val;
},
});
this.model.init(null, this.sequelizeModelOptions());
this.model.options.modelName = this.options.name;
@ -639,9 +653,14 @@ export class Collection<
updateOptions(options: CollectionOptions, mergeOptions?: any) {
let newOptions = lodash.cloneDeep(options);
newOptions = merge(this.options, newOptions, mergeOptions);
this.context.database.emit('beforeUpdateCollection', this, newOptions);
this.options = newOptions;
if (options.filterTargetKey) {
newOptions.filterTargetKey = options.filterTargetKey;
}
this.context.database.emit('beforeUpdateCollection', this, newOptions);
this.options = newOptions;
this.setFields(options.fields, false);
if (options.repository) {
this.setRepository(options.repository);
@ -909,10 +928,6 @@ export class Collection<
}
unavailableActions() {
if (this.options.template === 'file') {
return ['create', 'update', 'destroy'];
}
return [];
}

View File

@ -234,7 +234,12 @@ export class Database extends EventEmitter implements AsyncEmitter {
});
}
if (options.logging && process.env['DB_SQL_BENCHMARK'] == 'true') {
opts.benchmark = true;
}
this.options = opts;
this.logger.debug(
`create database instance: ${safeJsonStringify(
// remove sensitive information

View File

@ -40,7 +40,12 @@ export class BooleanInterface extends BaseInterface {
const option = enumConfig.find((item) => item.value === value);
return option?.label;
} else {
return value ? '是' : value === null || value === undefined ? '' : '否';
const label = value ? 'True' : value === null || value === undefined ? '' : 'False';
if (ctx?.t) {
return ctx.t(label, { ns: 'action-export' });
}
return label;
}
}
}

View File

@ -36,7 +36,16 @@ export class MultipleSelectInterface extends BaseInterface {
.castArray(value)
.map((value) => {
const option = enumConfig.find((item) => item.value === value);
return option ? option.label : value;
if (option) {
if (ctx?.t) {
return ctx.t(option.label, { ns: 'lm-collections' });
}
return option.label;
}
return value;
})
.join(',');
}

View File

@ -32,6 +32,15 @@ export class SelectInterface extends BaseInterface {
toString(value: any, ctx?: any) {
const enumConfig = this.options.uiSchema?.enum || [];
const option = enumConfig.find((item) => item.value === value);
return option?.label || value;
if (option) {
if (ctx?.t) {
return ctx.t(option.label, { ns: 'lm-collections' });
}
return option.label;
}
return value;
}
}

View File

@ -19,7 +19,7 @@ export class ToOneInterface extends BaseInterface {
return null;
}
const { filterKey, targetCollection, transaction } = ctx;
const { filterKey, associationField, targetCollection, transaction } = ctx;
const targetInstance = await targetCollection.repository.findOne({
filter: {
@ -31,8 +31,9 @@ export class ToOneInterface extends BaseInterface {
if (!targetInstance) {
throw new Error(`"${str}" not found in ${targetCollection.model.name} ${filterKey}`);
}
const primaryKeyAttribute = targetCollection.model.primaryKeyAttribute;
return targetInstance[primaryKeyAttribute];
const targetKey = associationField.targetKey || targetCollection.model.primaryKeyAttribute;
return targetInstance[targetKey];
}
}

View File

@ -182,8 +182,13 @@ export class OptionsParser {
sortField.push(direction);
if (this.database.isMySQLCompatibleDialect()) {
const fieldName = sortField[0];
// @ts-ignore
if (this.model.fieldRawAttributesMap[fieldName]) {
orderParams.push([Sequelize.fn('ISNULL', Sequelize.col(`${this.model.name}.${sortField[0]}`))]);
}
}
orderParams.push(sortField);
}

View File

@ -1,13 +1,13 @@
{
"name": "@nocobase/devtools",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./src/index.js",
"dependencies": {
"@nocobase/build": "1.4.0-alpha",
"@nocobase/client": "1.4.0-alpha",
"@nocobase/test": "1.4.0-alpha",
"@nocobase/build": "1.4.0-alpha.2",
"@nocobase/client": "1.4.0-alpha.2",
"@nocobase/test": "1.4.0-alpha.2",
"@types/koa": "^2.13.4",
"@types/koa-bodyparser": "^4.3.4",
"@types/lodash": "^4.14.177",

View File

@ -1,13 +1,13 @@
{
"name": "@nocobase/evaluators",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.2",
"description": "",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"license": "AGPL-3.0",
"dependencies": {
"@formulajs/formulajs": "4.2.0",
"@nocobase/utils": "1.4.0-alpha",
"@nocobase/utils": "1.4.0-alpha.2",
"mathjs": "^10.6.0"
},
"repository": {

View File

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

View File

@ -34,7 +34,7 @@ export interface RequestLoggerOptions extends LoggerOptions {
}
export const requestLogger = (appName: string, requestLogger: Logger, options?: RequestLoggerOptions) => {
return async (ctx, next) => {
return async function requestLoggerMiddleware(ctx, next) {
const reqId = ctx.reqId;
const path = /^\/api\/(.+):(.+)/.exec(ctx.path);
const contextLogger = ctx.app.log.child({ reqId, module: path?.[1], submodule: path?.[2] });
@ -71,6 +71,7 @@ export const requestLogger = (appName: string, requestLogger: Logger, options?:
cost,
app: appName,
reqId,
bodySize: ctx.response.length,
};
if (Math.floor(status / 100) == 5) {
requestLogger.error({ ...info, res: ctx.body?.['errors'] || ctx.body });

View File

@ -1,12 +1,12 @@
{
"name": "@nocobase/resourcer",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.2",
"description": "",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"license": "AGPL-3.0",
"dependencies": {
"@nocobase/utils": "1.4.0-alpha",
"@nocobase/utils": "1.4.0-alpha.2",
"deepmerge": "^4.2.2",
"koa-compose": "^4.1.0",
"lodash": "^4.17.21",

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { assign, MergeStrategies, requireModule } from '@nocobase/utils';
import { assign, MergeStrategies, requireModule, wrapMiddlewareWithLogging } from '@nocobase/utils';
import compose from 'koa-compose';
import _ from 'lodash';
import Middleware, { MiddlewareType } from './middleware';
@ -375,9 +375,7 @@ export class Action {
this.getHandler(),
].filter(Boolean);
// handlers = handlers.map((handler) => prePerfHooksWrap(handler));
return handlers;
return handlers.map((fn) => wrapMiddlewareWithLogging(fn));
}
/**

View File

@ -306,7 +306,9 @@ export class ResourceManager {
}
middleware({ prefix, accessors, skipIfDataSourceExists = false }: KoaMiddlewareOptions = {}) {
return async (ctx: ResourcerContext, next: () => Promise<any>) => {
const self = this;
return async function resourcerMiddleware(ctx: ResourcerContext, next: () => Promise<any>) {
if (skipIfDataSourceExists) {
const dataSource = ctx.get('x-data-source');
if (dataSource) {
@ -314,7 +316,7 @@ export class ResourceManager {
}
}
ctx.resourcer = this;
ctx.resourcer = self;
let params = parseRequest(
{
@ -322,8 +324,8 @@ export class ResourceManager {
method: ctx.request.method,
},
{
prefix: this.options.prefix || prefix,
accessors: this.options.accessors || accessors,
prefix: self.options.prefix || prefix,
accessors: self.options.accessors || accessors,
},
);
@ -332,7 +334,7 @@ export class ResourceManager {
}
try {
const resource = this.getResource(getNameByParams(params));
const resource = self.getResource(getNameByParams(params));
// 为关系资源时,暂时需要再执行一遍 parseRequest
if (resource.options.type && resource.options.type !== 'single') {
@ -343,8 +345,8 @@ export class ResourceManager {
type: resource.options.type,
},
{
prefix: this.options.prefix || prefix,
accessors: this.options.accessors || accessors,
prefix: self.options.prefix || prefix,
accessors: self.options.accessors || accessors,
},
);
@ -354,7 +356,7 @@ export class ResourceManager {
}
// action 需要 clone 之后再赋给 ctx
ctx.action = this.getAction(getNameByParams(params), params.actionName).clone();
ctx.action = self.getAction(getNameByParams(params), params.actionName).clone();
ctx.action.setContext(ctx);
ctx.action.actionName = params.actionName;

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/sdk",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.2",
"license": "AGPL-3.0",
"main": "lib/index.js",
"types": "lib/index.d.ts",

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/server",
"version": "1.4.0-alpha",
"version": "1.4.0-alpha.2",
"main": "lib/index.js",
"types": "./lib/index.d.ts",
"license": "AGPL-3.0",
@ -10,18 +10,18 @@
"@koa/cors": "^3.1.0",
"@koa/multer": "^3.0.2",
"@koa/router": "^9.4.0",
"@nocobase/acl": "1.4.0-alpha",
"@nocobase/actions": "1.4.0-alpha",
"@nocobase/auth": "1.4.0-alpha",
"@nocobase/cache": "1.4.0-alpha",
"@nocobase/data-source-manager": "1.4.0-alpha",
"@nocobase/database": "1.4.0-alpha",
"@nocobase/evaluators": "1.4.0-alpha",
"@nocobase/logger": "1.4.0-alpha",
"@nocobase/resourcer": "1.4.0-alpha",
"@nocobase/sdk": "1.4.0-alpha",
"@nocobase/telemetry": "1.4.0-alpha",
"@nocobase/utils": "1.4.0-alpha",
"@nocobase/acl": "1.4.0-alpha.2",
"@nocobase/actions": "1.4.0-alpha.2",
"@nocobase/auth": "1.4.0-alpha.2",
"@nocobase/cache": "1.4.0-alpha.2",
"@nocobase/data-source-manager": "1.4.0-alpha.2",
"@nocobase/database": "1.4.0-alpha.2",
"@nocobase/evaluators": "1.4.0-alpha.2",
"@nocobase/logger": "1.4.0-alpha.2",
"@nocobase/resourcer": "1.4.0-alpha.2",
"@nocobase/sdk": "1.4.0-alpha.2",
"@nocobase/telemetry": "1.4.0-alpha.2",
"@nocobase/utils": "1.4.0-alpha.2",
"@types/decompress": "4.2.7",
"@types/ini": "^1.3.31",
"@types/koa-send": "^4.1.3",

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