mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
Merge branch 'develop' into F-1952
This commit is contained in:
commit
b9aa843a69
119
.github/workflows/build-internal-image.yml
vendored
Normal file
119
.github/workflows/build-internal-image.yml
vendored
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
name: Build Image (Internal)
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
ref_name:
|
||||||
|
description: 'Branch or tag name to release'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
get-plugins:
|
||||||
|
uses: nocobase/nocobase/.github/workflows/get-plugins.yml@main
|
||||||
|
secrets: inherit
|
||||||
|
push-docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: get-plugins
|
||||||
|
services:
|
||||||
|
verdaccio:
|
||||||
|
image: verdaccio/verdaccio:5
|
||||||
|
ports:
|
||||||
|
- 4873:4873
|
||||||
|
steps:
|
||||||
|
- name: Set Node.js 20
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
- name: Get info
|
||||||
|
id: get-info
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [[ "${{ inputs.ref_name || github.ref_name }}" =~ "beta" ]]; then
|
||||||
|
echo "defaultTag=$(echo 'beta')" >> $GITHUB_OUTPUT
|
||||||
|
echo "proRepos=$(echo '${{ needs.get-plugins.outputs.beta-plugins }}')" >> $GITHUB_OUTPUT
|
||||||
|
elif [[ "${{ inputs.ref_name || github.ref_name }}" =~ "alpha" ]]; then
|
||||||
|
echo "defaultTag=$(echo 'alpha')" >> $GITHUB_OUTPUT
|
||||||
|
echo "proRepos=$(echo '${{ needs.get-plugins.outputs.alpha-plugins }}')" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
# rc
|
||||||
|
echo "defaultTag=$(echo 'latest')" >> $GITHUB_OUTPUT
|
||||||
|
echo "proRepos=$(echo '${{ needs.get-plugins.outputs.rc-plugins }}')" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
- uses: actions/create-github-app-token@v1
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.NOCOBASE_APP_ID }}
|
||||||
|
private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
|
||||||
|
repositories: nocobase,pro-plugins,${{ join(fromJSON(steps.get-info.outputs.proRepos), ',') }},${{ join(fromJSON(needs.get-plugins.outputs.custom-plugins), ',') }}
|
||||||
|
skip-token-revoke: true
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref_name || github.ref_name }}
|
||||||
|
- name: yarn install
|
||||||
|
run: |
|
||||||
|
yarn install
|
||||||
|
- name: Checkout pro-plugins
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
repository: nocobase/pro-plugins
|
||||||
|
path: packages/pro-plugins
|
||||||
|
ref: ${{ inputs.ref_name || github.ref_name }}
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
- name: Clone pro repos
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
for repo in ${{ join(fromJSON(steps.get-info.outputs.proRepos), ' ') }} ${{ join(fromJSON(needs.get-plugins.outputs.custom-plugins), ' ') }}
|
||||||
|
do
|
||||||
|
git clone -b ${{ inputs.ref_name || github.ref_name }} https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
|
||||||
|
done
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
with:
|
||||||
|
driver-opts: network=host
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
nocobase/nocobase
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
- name: Login to Aliyun Container Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ secrets.ALI_DOCKER_REGISTRY }}
|
||||||
|
username: ${{ secrets.ALI_DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.ALI_DOCKER_PASSWORD }}
|
||||||
|
- name: Set variables
|
||||||
|
run: |
|
||||||
|
target_directory="./packages/pro-plugins/@nocobase"
|
||||||
|
subdirectories=$(find "$target_directory" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | tr '\n' ' ')
|
||||||
|
trimmed_variable=$(echo "$subdirectories" | xargs)
|
||||||
|
packageNames="@nocobase/${trimmed_variable// / @nocobase/}"
|
||||||
|
pluginNames="${trimmed_variable//plugin-/}"
|
||||||
|
BEFORE_PACK_NOCOBASE="yarn add @nocobase/plugin-notifications @nocobase/plugin-disable-pm-add $packageNames -W --production"
|
||||||
|
APPEND_PRESET_LOCAL_PLUGINS="notifications,disable-pm-add,${pluginNames// /,}"
|
||||||
|
echo "var1=$BEFORE_PACK_NOCOBASE" >> $GITHUB_OUTPUT
|
||||||
|
echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
build-args: |
|
||||||
|
VERDACCIO_URL=http://localhost:4873/
|
||||||
|
COMMIT_HASH=${GITHUB_SHA}
|
||||||
|
PLUGINS_DIRS=pro-plugins
|
||||||
|
BEFORE_PACK_NOCOBASE=${{ steps.vars.outputs.var1 }}
|
||||||
|
APPEND_PRESET_LOCAL_PLUGINS=${{ steps.vars.outputs.var2 }}
|
||||||
|
push: true
|
||||||
|
tags: ${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/nocobase/nocobase:${{ steps.get-info.outputs.defaultTag }},${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/${{ steps.meta.outputs.tags }}
|
66
.github/workflows/nocobase-test-backend.yml
vendored
66
.github/workflows/nocobase-test-backend.yml
vendored
@ -5,38 +5,40 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
branches:
|
|
||||||
- main
|
# push:
|
||||||
- next
|
# branches:
|
||||||
- develop
|
# - main
|
||||||
paths:
|
# - next
|
||||||
- 'package.json'
|
# - develop
|
||||||
- '**/yarn.lock'
|
# paths:
|
||||||
- 'packages/core/acl/**'
|
# - 'package.json'
|
||||||
- 'packages/core/auth/**'
|
# - '**/yarn.lock'
|
||||||
- 'packages/core/actions/**'
|
# - 'packages/core/acl/**'
|
||||||
- 'packages/core/database/**'
|
# - 'packages/core/auth/**'
|
||||||
- 'packages/core/resourcer/**'
|
# - 'packages/core/actions/**'
|
||||||
- 'packages/core/data-source-manager/**'
|
# - 'packages/core/database/**'
|
||||||
- 'packages/core/server/**'
|
# - 'packages/core/resourcer/**'
|
||||||
- 'packages/core/utils/**'
|
# - 'packages/core/data-source-manager/**'
|
||||||
- 'packages/plugins/**/src/server/**'
|
# - 'packages/core/server/**'
|
||||||
- '.github/workflows/nocobase-test-backend.yml'
|
# - 'packages/core/utils/**'
|
||||||
pull_request:
|
# - 'packages/plugins/**/src/server/**'
|
||||||
paths:
|
# - '.github/workflows/nocobase-test-backend.yml'
|
||||||
- 'package.json'
|
# pull_request:
|
||||||
- '**/yarn.lock'
|
# paths:
|
||||||
- 'packages/core/acl/**'
|
# - 'package.json'
|
||||||
- 'packages/core/auth/**'
|
# - '**/yarn.lock'
|
||||||
- 'packages/core/actions/**'
|
# - 'packages/core/acl/**'
|
||||||
- 'packages/core/database/**'
|
# - 'packages/core/auth/**'
|
||||||
- 'packages/core/resourcer/**'
|
# - 'packages/core/actions/**'
|
||||||
- 'packages/core/data-source-manager/**'
|
# - 'packages/core/database/**'
|
||||||
- 'packages/core/server/**'
|
# - 'packages/core/resourcer/**'
|
||||||
- 'packages/core/utils/**'
|
# - 'packages/core/data-source-manager/**'
|
||||||
- 'packages/plugins/**/src/server/**'
|
# - 'packages/core/server/**'
|
||||||
- '.github/workflows/nocobase-test-backend.yml'
|
# - 'packages/core/utils/**'
|
||||||
|
# - 'packages/plugins/**/src/server/**'
|
||||||
|
# - '.github/workflows/nocobase-test-backend.yml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sqlite-test:
|
sqlite-test:
|
||||||
|
214
CHANGELOG.md
214
CHANGELOG.md
@ -5,6 +5,220 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [v1.6.21](https://github.com/nocobase/nocobase/compare/v1.6.20...v1.6.21) - 2025-04-17
|
||||||
|
|
||||||
|
### 🚀 Improvements
|
||||||
|
|
||||||
|
- **[client]** Add delay API for scenarios which open without delay ([#6681](https://github.com/nocobase/nocobase/pull/6681)) by @mytharcher
|
||||||
|
|
||||||
|
- **[create-nocobase-app]** Upgrade some dependencies to latest versions ([#6673](https://github.com/nocobase/nocobase/pull/6673)) by @chenos
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- Fix error thrown when mouse hover on referenced template block in approval node configuration ([#6691](https://github.com/nocobase/nocobase/pull/6691)) by @mytharcher
|
||||||
|
|
||||||
|
- custom association field not displaying field component settings ([#6692](https://github.com/nocobase/nocobase/pull/6692)) by @katherinehhh
|
||||||
|
|
||||||
|
- Fix locale for upload component ([#6682](https://github.com/nocobase/nocobase/pull/6682)) by @mytharcher
|
||||||
|
|
||||||
|
- lazy load missing ui component will cause render error ([#6683](https://github.com/nocobase/nocobase/pull/6683)) by @gchust
|
||||||
|
|
||||||
|
- Add native Password component to HoC Input ([#6679](https://github.com/nocobase/nocobase/pull/6679)) by @mytharcher
|
||||||
|
|
||||||
|
- inherited fields shown in current collection field assignment list ([#6666](https://github.com/nocobase/nocobase/pull/6666)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[database]** Fixed ci build error ([#6687](https://github.com/nocobase/nocobase/pull/6687)) by @aaaaaajie
|
||||||
|
|
||||||
|
- **[build]** build output is incorrect when plugin depends on some AMD libraries ([#6665](https://github.com/nocobase/nocobase/pull/6665)) by @gchust
|
||||||
|
|
||||||
|
- **[Action: Import records]** fixed an error importing xlsx time field ([#6672](https://github.com/nocobase/nocobase/pull/6672)) by @aaaaaajie
|
||||||
|
|
||||||
|
- **[Workflow: Manual node]** Fix manual task status constant ([#6676](https://github.com/nocobase/nocobase/pull/6676)) by @mytharcher
|
||||||
|
|
||||||
|
- **[Block: iframe]** vertical scrollbar appears when iframe block is set to full height ([#6675](https://github.com/nocobase/nocobase/pull/6675)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[Workflow: Custom action event]** Fix test cases by @mytharcher
|
||||||
|
|
||||||
|
- **[Backup manager]** timeout error occurs when trying to restore an unecrypted backup with a password by @gchust
|
||||||
|
|
||||||
|
## [v1.6.20](https://github.com/nocobase/nocobase/compare/v1.6.19...v1.6.20) - 2025-04-14
|
||||||
|
|
||||||
|
### 🎉 New Features
|
||||||
|
|
||||||
|
- **[Departments]** Make Department, Attachment URL, and Workflow response message plugins free ([#6663](https://github.com/nocobase/nocobase/pull/6663)) by @chenos
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- The filter form should not display the "Unsaved changes" prompt ([#6657](https://github.com/nocobase/nocobase/pull/6657)) by @zhangzhonghe
|
||||||
|
|
||||||
|
- "allow multiple" option not working for relation field ([#6661](https://github.com/nocobase/nocobase/pull/6661)) by @katherinehhh
|
||||||
|
|
||||||
|
- In the filter form, when the filter button is clicked, if there are fields that have not passed validation, the filtering is still triggered ([#6659](https://github.com/nocobase/nocobase/pull/6659)) by @zhangzhonghe
|
||||||
|
|
||||||
|
- Switching to the group menu should not jump to a page that has already been hidden in menu ([#6654](https://github.com/nocobase/nocobase/pull/6654)) by @zhangzhonghe
|
||||||
|
|
||||||
|
- **[File storage: S3(Pro)]**
|
||||||
|
- Organize language by @jiannx
|
||||||
|
|
||||||
|
- Individual baseurl and public settings, improve S3 pro storage config UX by @jiannx
|
||||||
|
|
||||||
|
- **[Migration manager]** the skip auto backup option becomes invalid if environment variable popup appears during migration by @gchust
|
||||||
|
|
||||||
|
## [v1.6.19](https://github.com/nocobase/nocobase/compare/v1.6.18...v1.6.19) - 2025-04-14
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- Fix the issue of preview images being obscured ([#6651](https://github.com/nocobase/nocobase/pull/6651)) by @zhangzhonghe
|
||||||
|
|
||||||
|
- In the form block, the default value of the field configuration will first be displayed as the original variable string and then disappear ([#6649](https://github.com/nocobase/nocobase/pull/6649)) by @zhangzhonghe
|
||||||
|
|
||||||
|
## [v1.6.18](https://github.com/nocobase/nocobase/compare/v1.6.17...v1.6.18) - 2025-04-11
|
||||||
|
|
||||||
|
### 🚀 Improvements
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- Add default type fallback API for `Variable.Input` ([#6644](https://github.com/nocobase/nocobase/pull/6644)) by @mytharcher
|
||||||
|
|
||||||
|
- Optimize prompts for unconfigured pages ([#6641](https://github.com/nocobase/nocobase/pull/6641)) by @zhangzhonghe
|
||||||
|
|
||||||
|
- **[Workflow: Delay node]** Support to use variable for duration ([#6621](https://github.com/nocobase/nocobase/pull/6621)) by @mytharcher
|
||||||
|
|
||||||
|
- **[Workflow: Custom action event]** Add refresh settings for trigger workflow button by @mytharcher
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- subtable description overlapping with add new button ([#6646](https://github.com/nocobase/nocobase/pull/6646)) by @katherinehhh
|
||||||
|
|
||||||
|
- dashed underline caused by horizontal form layout in modal ([#6639](https://github.com/nocobase/nocobase/pull/6639)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[File storage: S3(Pro)]** Fix missing await for next call. by @jiannx
|
||||||
|
|
||||||
|
- **[Email manager]** Fix missing await for next call. by @jiannx
|
||||||
|
|
||||||
|
## [v1.6.17](https://github.com/nocobase/nocobase/compare/v1.6.16...v1.6.17) - 2025-04-09
|
||||||
|
|
||||||
|
### 🚀 Improvements
|
||||||
|
|
||||||
|
- **[utils]** Add duration extension for dayjs ([#6630](https://github.com/nocobase/nocobase/pull/6630)) by @mytharcher
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- Support to search field in Filter component ([#6627](https://github.com/nocobase/nocobase/pull/6627)) by @mytharcher
|
||||||
|
|
||||||
|
- Add `trim` API for `Input` and `Variable.TextArea` ([#6624](https://github.com/nocobase/nocobase/pull/6624)) by @mytharcher
|
||||||
|
|
||||||
|
- **[Error handler]** Support custom title in AppError component. ([#6409](https://github.com/nocobase/nocobase/pull/6409)) by @sheldon66
|
||||||
|
|
||||||
|
- **[IP restriction]** Update IP restriction message content. by @sheldon66
|
||||||
|
|
||||||
|
- **[File storage: S3(Pro)]** Support global variables in storage configuration by @mytharcher
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- rule with 'any' condition does not take effect when condition list is empty ([#6628](https://github.com/nocobase/nocobase/pull/6628)) by @katherinehhh
|
||||||
|
|
||||||
|
- data issue with Gantt block in tree collection ([#6617](https://github.com/nocobase/nocobase/pull/6617)) by @katherinehhh
|
||||||
|
|
||||||
|
- The relationship fields in the filter form report an error after the page is refreshed because x-data-source is not carried ([#6619](https://github.com/nocobase/nocobase/pull/6619)) by @zhangzhonghe
|
||||||
|
|
||||||
|
- variable parse failure when URL parameters contain Chinese characters ([#6618](https://github.com/nocobase/nocobase/pull/6618)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[Users]** Issue with parsing the user profile form schema ([#6635](https://github.com/nocobase/nocobase/pull/6635)) by @2013xile
|
||||||
|
|
||||||
|
- **[Mobile]** single-select field with 'contains' filter on mobile does not support multiple selection ([#6629](https://github.com/nocobase/nocobase/pull/6629)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[Action: Export records]** missing filter params when exporting data after changing pagination ([#6633](https://github.com/nocobase/nocobase/pull/6633)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[Email manager]** fix email management permission cannot view email list by @jiannx
|
||||||
|
|
||||||
|
- **[File storage: S3(Pro)]** Throw error to user when upload logo to S3 Pro storage (set to default) by @mytharcher
|
||||||
|
|
||||||
|
- **[Workflow: Approval]** Fix `updatedAt` changed after migration by @mytharcher
|
||||||
|
|
||||||
|
- **[Migration manager]** migration log creation time is displayed incorrectly in some environments by @gchust
|
||||||
|
|
||||||
|
## [v1.6.16](https://github.com/nocobase/nocobase/compare/v1.6.15...v1.6.16) - 2025-04-03
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- x-disabled property not taking effect on form fields ([#6610](https://github.com/nocobase/nocobase/pull/6610)) by @katherinehhh
|
||||||
|
|
||||||
|
- field label display issue to prevent truncation by colon ([#6599](https://github.com/nocobase/nocobase/pull/6599)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[database]** When deleting one-to-many records, both `filter` and `filterByTk` are passed and `filter` includes an association field, the `filterByTk` is ignored ([#6606](https://github.com/nocobase/nocobase/pull/6606)) by @2013xile
|
||||||
|
|
||||||
|
## [v1.6.15](https://github.com/nocobase/nocobase/compare/v1.6.14...v1.6.15) - 2025-04-01
|
||||||
|
|
||||||
|
### 🚀 Improvements
|
||||||
|
|
||||||
|
- **[database]**
|
||||||
|
- Add trim option for text field ([#6603](https://github.com/nocobase/nocobase/pull/6603)) by @mytharcher
|
||||||
|
|
||||||
|
- Add trim option for string field ([#6565](https://github.com/nocobase/nocobase/pull/6565)) by @mytharcher
|
||||||
|
|
||||||
|
- **[File manager]** Add trim option for text fields of storages collection ([#6604](https://github.com/nocobase/nocobase/pull/6604)) by @mytharcher
|
||||||
|
|
||||||
|
- **[Workflow]** Improve code ([#6589](https://github.com/nocobase/nocobase/pull/6589)) by @mytharcher
|
||||||
|
|
||||||
|
- **[Workflow: Approval]** Support to use block template for approval process form by @mytharcher
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **[database]** Avoid "datetimeNoTz" field changes when value not changed in updating record ([#6588](https://github.com/nocobase/nocobase/pull/6588)) by @mytharcher
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- association field (select) displaying N/A when exposing related collection fields ([#6582](https://github.com/nocobase/nocobase/pull/6582)) by @katherinehhh
|
||||||
|
|
||||||
|
- Fix `disabled` property not works when `SchemaInitializerItem` has `items` ([#6597](https://github.com/nocobase/nocobase/pull/6597)) by @mytharcher
|
||||||
|
|
||||||
|
- cascade issue: 'The value of xxx cannot be in array format' when deleting and re-selecting ([#6585](https://github.com/nocobase/nocobase/pull/6585)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[Collection field: Many to many (array)]** Issue of filtering by fields in an association collection with a many to many (array) field ([#6596](https://github.com/nocobase/nocobase/pull/6596)) by @2013xile
|
||||||
|
|
||||||
|
- **[Public forms]** View permissions include list and get ([#6607](https://github.com/nocobase/nocobase/pull/6607)) by @chenos
|
||||||
|
|
||||||
|
- **[Authentication]** token assignment in `AuthProvider` ([#6593](https://github.com/nocobase/nocobase/pull/6593)) by @2013xile
|
||||||
|
|
||||||
|
- **[Workflow]** Fix sync option display incorrectly ([#6595](https://github.com/nocobase/nocobase/pull/6595)) by @mytharcher
|
||||||
|
|
||||||
|
- **[Block: Map]** map management validation should not pass with space input ([#6575](https://github.com/nocobase/nocobase/pull/6575)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[Workflow: Approval]**
|
||||||
|
- Fix client variables to use in approval form by @mytharcher
|
||||||
|
|
||||||
|
- Fix branch mode when `endOnReject` configured as `true` by @mytharcher
|
||||||
|
|
||||||
|
## [v1.6.14](https://github.com/nocobase/nocobase/compare/v1.6.13...v1.6.14) - 2025-03-29
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **[Calendar]** missing data on boundary dates in weekly calendar view ([#6587](https://github.com/nocobase/nocobase/pull/6587)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[Auth: OIDC]** Incorrect redirection occurs when the callback path is the string 'null' by @2013xile
|
||||||
|
|
||||||
|
- **[Workflow: Approval]** Fix approval node configuration is incorrect after schema changed by @mytharcher
|
||||||
|
|
||||||
|
## [v1.6.13](https://github.com/nocobase/nocobase/compare/v1.6.12...v1.6.13) - 2025-03-28
|
||||||
|
|
||||||
|
### 🚀 Improvements
|
||||||
|
|
||||||
|
- **[Async task manager]** optimize import/export buttons in Pro ([#6531](https://github.com/nocobase/nocobase/pull/6531)) by @chenos
|
||||||
|
|
||||||
|
- **[Action: Export records Pro]** optimize import/export buttons in Pro by @katherinehhh
|
||||||
|
|
||||||
|
- **[Migration manager]** allow skip automatic backup and restore for migration by @gchust
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **[client]** linkage conflict between same-named association fields in different sub-tables within the same form ([#6577](https://github.com/nocobase/nocobase/pull/6577)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[Action: Batch edit]** Click the batch edit button, configure the pop-up window, and then open it again, the pop-up window is blank ([#6578](https://github.com/nocobase/nocobase/pull/6578)) by @zhangzhonghe
|
||||||
|
|
||||||
## [v1.6.12](https://github.com/nocobase/nocobase/compare/v1.6.11...v1.6.12) - 2025-03-27
|
## [v1.6.12](https://github.com/nocobase/nocobase/compare/v1.6.11...v1.6.12) - 2025-03-27
|
||||||
|
|
||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
@ -5,6 +5,220 @@
|
|||||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
||||||
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
|
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
|
||||||
|
|
||||||
|
## [v1.6.21](https://github.com/nocobase/nocobase/compare/v1.6.20...v1.6.21) - 2025-04-17
|
||||||
|
|
||||||
|
### 🚀 优化
|
||||||
|
|
||||||
|
- **[client]** 为弹窗组件增加 delay API ([#6681](https://github.com/nocobase/nocobase/pull/6681)) by @mytharcher
|
||||||
|
|
||||||
|
- **[create-nocobase-app]** 升级部分依赖的版本 ([#6673](https://github.com/nocobase/nocobase/pull/6673)) by @chenos
|
||||||
|
|
||||||
|
### 🐛 修复
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- 修复审批节点配置中引用模板区块的添加按钮报错问题 ([#6691](https://github.com/nocobase/nocobase/pull/6691)) by @mytharcher
|
||||||
|
|
||||||
|
- 自定义的关系字段没有显示关系字段组件 ([#6692](https://github.com/nocobase/nocobase/pull/6692)) by @katherinehhh
|
||||||
|
|
||||||
|
- 修复上传组件语言问题 ([#6682](https://github.com/nocobase/nocobase/pull/6682)) by @mytharcher
|
||||||
|
|
||||||
|
- 懒加载组件不存在时界面报错 ([#6683](https://github.com/nocobase/nocobase/pull/6683)) by @gchust
|
||||||
|
|
||||||
|
- 补全原生的 Password 组件到封装过的输入组件 ([#6679](https://github.com/nocobase/nocobase/pull/6679)) by @mytharcher
|
||||||
|
|
||||||
|
- 字段赋值本表字段列表中显示了继承表字段,应只显示本表字段 ([#6666](https://github.com/nocobase/nocobase/pull/6666)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[database]** 修复 CI 编译错误 ([#6687](https://github.com/nocobase/nocobase/pull/6687)) by @aaaaaajie
|
||||||
|
|
||||||
|
- **[build]** 插件依赖 AMD 库时构建产物不正确 ([#6665](https://github.com/nocobase/nocobase/pull/6665)) by @gchust
|
||||||
|
|
||||||
|
- **[操作:导入记录]** 修复导入包含时间字段的 xlsx 错误 ([#6672](https://github.com/nocobase/nocobase/pull/6672)) by @aaaaaajie
|
||||||
|
|
||||||
|
- **[工作流:人工处理节点]** 修复人工节点任务状态常量 ([#6676](https://github.com/nocobase/nocobase/pull/6676)) by @mytharcher
|
||||||
|
|
||||||
|
- **[区块:iframe]** iframe 区块设置全高时页面出现滚动条 ([#6675](https://github.com/nocobase/nocobase/pull/6675)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[工作流:自定义操作事件]** 修复测试用例 by @mytharcher
|
||||||
|
|
||||||
|
- **[备份管理器]** 还原时若备份未设置密码,但用户输入了密码,还原会出现超时报错 by @gchust
|
||||||
|
|
||||||
|
## [v1.6.20](https://github.com/nocobase/nocobase/compare/v1.6.19...v1.6.20) - 2025-04-14
|
||||||
|
|
||||||
|
### 🎉 新特性
|
||||||
|
|
||||||
|
- **[部门]** 商业插件部门、附件 URL、工作流响应消息改为免费提供 ([#6663](https://github.com/nocobase/nocobase/pull/6663)) by @chenos
|
||||||
|
|
||||||
|
### 🐛 修复
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- 筛选表单不应该显示“未保存修改”提示 ([#6657](https://github.com/nocobase/nocobase/pull/6657)) by @zhangzhonghe
|
||||||
|
|
||||||
|
- 筛选表单中关系字段的“允许多选”设置项不生效 ([#6661](https://github.com/nocobase/nocobase/pull/6661)) by @katherinehhh
|
||||||
|
|
||||||
|
- 筛选表单中,当点击筛选按钮时,如果有字段未校验通过,依然会触发筛选的问题 ([#6659](https://github.com/nocobase/nocobase/pull/6659)) by @zhangzhonghe
|
||||||
|
|
||||||
|
- 切换到分组菜单时,不应该跳转到已经在菜单中被隐藏的页面 ([#6654](https://github.com/nocobase/nocobase/pull/6654)) by @zhangzhonghe
|
||||||
|
|
||||||
|
- **[文件存储:S3 (Pro)]**
|
||||||
|
- 整理语言文案 by @jiannx
|
||||||
|
|
||||||
|
- baseurl 和 public 设置不再互相关联,改进 S3 pro 存储的配置交互体验 by @jiannx
|
||||||
|
|
||||||
|
- **[迁移管理]** 迁移时若弹出环境变量弹窗,跳过自动备份选项会失效 by @gchust
|
||||||
|
|
||||||
|
## [v1.6.19](https://github.com/nocobase/nocobase/compare/v1.6.18...v1.6.19) - 2025-04-14
|
||||||
|
|
||||||
|
### 🐛 修复
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- 修复预览图片被遮挡的问题 ([#6651](https://github.com/nocobase/nocobase/pull/6651)) by @zhangzhonghe
|
||||||
|
|
||||||
|
- 表单区块中,字段配置的默认值会先显示为原始变量字符串然后再消失 ([#6649](https://github.com/nocobase/nocobase/pull/6649)) by @zhangzhonghe
|
||||||
|
|
||||||
|
## [v1.6.18](https://github.com/nocobase/nocobase/compare/v1.6.17...v1.6.18) - 2025-04-11
|
||||||
|
|
||||||
|
### 🚀 优化
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- 为 `Variable.Input` 组件增加默认退避类型的 API ([#6644](https://github.com/nocobase/nocobase/pull/6644)) by @mytharcher
|
||||||
|
|
||||||
|
- 优化未配置页面时的提示 ([#6641](https://github.com/nocobase/nocobase/pull/6641)) by @zhangzhonghe
|
||||||
|
|
||||||
|
- **[工作流:延时节点]** 支持延迟时间使用变量 ([#6621](https://github.com/nocobase/nocobase/pull/6621)) by @mytharcher
|
||||||
|
|
||||||
|
- **[工作流:自定义操作事件]** 为触发工作流按钮增加刷新配置项 by @mytharcher
|
||||||
|
|
||||||
|
### 🐛 修复
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- 子表格中描述信息与操作按钮遮挡 ([#6646](https://github.com/nocobase/nocobase/pull/6646)) by @katherinehhh
|
||||||
|
|
||||||
|
- 弹窗表单在 horizontal 布局下初始宽度计算错误,导致出现提示和 下划虚线 ([#6639](https://github.com/nocobase/nocobase/pull/6639)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[文件存储:S3 (Pro)]** 修复next调用缺少await by @jiannx
|
||||||
|
|
||||||
|
- **[邮件管理]** 修复next调用缺少await by @jiannx
|
||||||
|
|
||||||
|
## [v1.6.17](https://github.com/nocobase/nocobase/compare/v1.6.16...v1.6.17) - 2025-04-09
|
||||||
|
|
||||||
|
### 🚀 优化
|
||||||
|
|
||||||
|
- **[utils]** 为 dayjs 包增加时长扩展 ([#6630](https://github.com/nocobase/nocobase/pull/6630)) by @mytharcher
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- 支持筛选组件中对字段进行搜索 ([#6627](https://github.com/nocobase/nocobase/pull/6627)) by @mytharcher
|
||||||
|
|
||||||
|
- 为 `Input` 和 `Variable.TextArea` 组件增加 `trim` API ([#6624](https://github.com/nocobase/nocobase/pull/6624)) by @mytharcher
|
||||||
|
|
||||||
|
- **[错误处理器]** 在 AppError 组件中支持自定义标题。 ([#6409](https://github.com/nocobase/nocobase/pull/6409)) by @sheldon66
|
||||||
|
|
||||||
|
- **[IP 限制]** 更新 IP 限制消息内容。 by @sheldon66
|
||||||
|
|
||||||
|
- **[文件存储:S3 (Pro)]** 支持存储引擎的配置中使用全局变量 by @mytharcher
|
||||||
|
|
||||||
|
### 🐛 修复
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- 联动规则条件设置为任意且无条件内容时属性设置不生效 ([#6628](https://github.com/nocobase/nocobase/pull/6628)) by @katherinehhh
|
||||||
|
|
||||||
|
- 树表使用甘特图区块时数据显示异常 ([#6617](https://github.com/nocobase/nocobase/pull/6617)) by @katherinehhh
|
||||||
|
|
||||||
|
- 筛选表单中的关系字段在刷新页面后,由于没有携带 x-data-source 而报错 ([#6619](https://github.com/nocobase/nocobase/pull/6619)) by @zhangzhonghe
|
||||||
|
|
||||||
|
- 链接中中文参数变量值解析失败 ([#6618](https://github.com/nocobase/nocobase/pull/6618)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[用户]** 用户个人资料表单 schema 的解析问题 ([#6635](https://github.com/nocobase/nocobase/pull/6635)) by @2013xile
|
||||||
|
|
||||||
|
- **[移动端]** 下拉单选字段在移动端设置筛选符为包含时组件未支持多选 ([#6629](https://github.com/nocobase/nocobase/pull/6629)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[操作:导出记录]** 筛选数据后切换分页再导出时筛选参数丢失 ([#6633](https://github.com/nocobase/nocobase/pull/6633)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[邮件管理]** 邮件管理权限无法查看邮件列表 by @jiannx
|
||||||
|
|
||||||
|
- **[文件存储:S3 (Pro)]** 当用户上传 logo 失败时提示错误(设置为默认存储的 S3 Pro) by @mytharcher
|
||||||
|
|
||||||
|
- **[工作流:审批]** 修复更新时间在迁移后变化 by @mytharcher
|
||||||
|
|
||||||
|
- **[迁移管理]** 部分服务器环境下迁移日志创建日期显示不正确 by @gchust
|
||||||
|
|
||||||
|
## [v1.6.16](https://github.com/nocobase/nocobase/compare/v1.6.15...v1.6.16) - 2025-04-03
|
||||||
|
|
||||||
|
### 🐛 修复
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- 表单字段设置不可编辑不起作用 ([#6610](https://github.com/nocobase/nocobase/pull/6610)) by @katherinehhh
|
||||||
|
|
||||||
|
- 表单字段标题因冒号导致的截断问题 ([#6599](https://github.com/nocobase/nocobase/pull/6599)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[database]** 删除一对多记录时,同时传递 `filter` 和 `filterByTk` 参数,`filter` 包含关系字段时,`filterByTk` 参数失效 ([#6606](https://github.com/nocobase/nocobase/pull/6606)) by @2013xile
|
||||||
|
|
||||||
|
## [v1.6.15](https://github.com/nocobase/nocobase/compare/v1.6.14...v1.6.15) - 2025-04-01
|
||||||
|
|
||||||
|
### 🚀 优化
|
||||||
|
|
||||||
|
- **[database]**
|
||||||
|
- 为多行文本类型字段增加去除首尾空白字符的选项 ([#6603](https://github.com/nocobase/nocobase/pull/6603)) by @mytharcher
|
||||||
|
|
||||||
|
- 为单行文本增加自动去除首尾空白字符的选项 ([#6565](https://github.com/nocobase/nocobase/pull/6565)) by @mytharcher
|
||||||
|
|
||||||
|
- **[文件管理器]** 为存储引擎表的文本字段增加去除首尾空白字符的选项 ([#6604](https://github.com/nocobase/nocobase/pull/6604)) by @mytharcher
|
||||||
|
|
||||||
|
- **[工作流]** 优化代码 ([#6589](https://github.com/nocobase/nocobase/pull/6589)) by @mytharcher
|
||||||
|
|
||||||
|
- **[工作流:审批]** 支持审批表单使用区块模板 by @mytharcher
|
||||||
|
|
||||||
|
### 🐛 修复
|
||||||
|
|
||||||
|
- **[database]** 避免“日期时间(无时区)”字段在值未变动的更新时触发值改变 ([#6588](https://github.com/nocobase/nocobase/pull/6588)) by @mytharcher
|
||||||
|
|
||||||
|
- **[client]**
|
||||||
|
- 关系字段(select)放出关系表字段时默认显示 N/A ([#6582](https://github.com/nocobase/nocobase/pull/6582)) by @katherinehhh
|
||||||
|
|
||||||
|
- 修复 `SchemaInitializerItem` 配置了 `items` 时 `disabled` 属性无效的问题 ([#6597](https://github.com/nocobase/nocobase/pull/6597)) by @mytharcher
|
||||||
|
|
||||||
|
- 级联组件删除后重新选择时出现 'The value of xxx cannot be in array format' ([#6585](https://github.com/nocobase/nocobase/pull/6585)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[数据表字段:多对多 (数组)]** 主表筛选带有多对多(数组)字段的关联表中的字段报错的问题 ([#6596](https://github.com/nocobase/nocobase/pull/6596)) by @2013xile
|
||||||
|
|
||||||
|
- **[公开表单]** 查看权限包括 list 和 get ([#6607](https://github.com/nocobase/nocobase/pull/6607)) by @chenos
|
||||||
|
|
||||||
|
- **[用户认证]** `AuthProvider` 中的 token 赋值 ([#6593](https://github.com/nocobase/nocobase/pull/6593)) by @2013xile
|
||||||
|
|
||||||
|
- **[工作流]** 修复同步选项展示问题 ([#6595](https://github.com/nocobase/nocobase/pull/6595)) by @mytharcher
|
||||||
|
|
||||||
|
- **[区块:地图]** 地图管理必填校验不应通过空格输入 ([#6575](https://github.com/nocobase/nocobase/pull/6575)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[工作流:审批]**
|
||||||
|
- 修复审批表单中的前端变量 by @mytharcher
|
||||||
|
|
||||||
|
- 修复分支模式下配置拒绝则结束时的流程问题 by @mytharcher
|
||||||
|
|
||||||
|
## [v1.6.14](https://github.com/nocobase/nocobase/compare/v1.6.13...v1.6.14) - 2025-03-29
|
||||||
|
|
||||||
|
### 🐛 修复
|
||||||
|
|
||||||
|
- **[日历]** 日历区块以周为视图时,边界日期不显示数据 ([#6587](https://github.com/nocobase/nocobase/pull/6587)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[认证:OIDC]** 回调路径是字符串'null'时导致跳转不正确 by @2013xile
|
||||||
|
|
||||||
|
- **[工作流:审批]** 修复审批节点界面配置变更后数据未同步的问题 by @mytharcher
|
||||||
|
|
||||||
|
## [v1.6.13](https://github.com/nocobase/nocobase/compare/v1.6.12...v1.6.13) - 2025-03-28
|
||||||
|
|
||||||
|
### 🚀 优化
|
||||||
|
|
||||||
|
- **[异步任务管理器]** 优化 Pro 导入导出按钮异步逻辑 ([#6531](https://github.com/nocobase/nocobase/pull/6531)) by @chenos
|
||||||
|
|
||||||
|
- **[操作:导出记录 Pro]** 优化 Pro 导入导出按钮 by @katherinehhh
|
||||||
|
|
||||||
|
- **[迁移管理]** 允许执行迁移时跳过自动备份还原 by @gchust
|
||||||
|
|
||||||
|
### 🐛 修复
|
||||||
|
|
||||||
|
- **[client]** 同一表单中不同关系字段的同名关系字段的联动互相影响 ([#6577](https://github.com/nocobase/nocobase/pull/6577)) by @katherinehhh
|
||||||
|
|
||||||
|
- **[操作:批量编辑]** 点击批量编辑按钮,配置完弹窗再打开,弹窗是空白的 ([#6578](https://github.com/nocobase/nocobase/pull/6578)) by @zhangzhonghe
|
||||||
|
|
||||||
## [v1.6.12](https://github.com/nocobase/nocobase/compare/v1.6.11...v1.6.12) - 2025-03-27
|
## [v1.6.12](https://github.com/nocobase/nocobase/compare/v1.6.11...v1.6.12) - 2025-03-27
|
||||||
|
|
||||||
### 🐛 修复
|
### 🐛 修复
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Updated Date: February 20, 2025
|
Updated Date: April 1, 2025
|
||||||
|
|
||||||
NocoBase License Agreement
|
NocoBase License Agreement
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr
|
|||||||
|
|
||||||
6.6 Can sell plugins developed for Software in the Marketplace.
|
6.6 Can sell plugins developed for Software in the Marketplace.
|
||||||
|
|
||||||
6.7 The User with an Enterprise Edition License can sell Upper Layer Application to their clients.
|
6.7 The User with a Professional or Enterprise Edition License can sell Upper Layer Application to their clients.
|
||||||
|
|
||||||
6.8 Not restricted by the AGPL-3.0 agreement.
|
6.8 Not restricted by the AGPL-3.0 agreement.
|
||||||
|
|
||||||
@ -106,9 +106,9 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr
|
|||||||
|
|
||||||
7.4 It is not allowed to provide any form of no-code, zero-code, low-code platform SaaS products to the public using the original or modified Software.
|
7.4 It is not allowed to provide any form of no-code, zero-code, low-code platform SaaS products to the public using the original or modified Software.
|
||||||
|
|
||||||
7.5 It is not allowed for the User withot an Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license.
|
7.5 It is not allowed for the User withot a Professional or Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license.
|
||||||
|
|
||||||
7.6 It is not allowed for the User with an Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license with access to further development and configuration.
|
7.6 It is not allowed for the User with a Professional or Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license with access to further development and configuration.
|
||||||
|
|
||||||
7.7 It is not allowed to publicly sell plugins developed for Software outside of the Marketplace.
|
7.7 It is not allowed to publicly sell plugins developed for Software outside of the Marketplace.
|
||||||
|
|
||||||
|
@ -2,14 +2,10 @@
|
|||||||
|
|
||||||
https://github.com/user-attachments/assets/cf08bfe5-e6e6-453c-8b96-350a6a8bed17
|
https://github.com/user-attachments/assets/cf08bfe5-e6e6-453c-8b96-350a6a8bed17
|
||||||
|
|
||||||
## ご協力ありがとうございます!
|
<p align="center">
|
||||||
<a href="https://trendshift.io/repositories/4112" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4112" alt="nocobase%2Fnocobase | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/4112" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4112" alt="nocobase%2Fnocobase | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
<a href="https://www.producthunt.com/posts/nocobase?embed=true&utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-nocobase" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=456520&theme=light&period=weekly&topic_id=267" alt="NocoBase - Scalability-first, open-source no-code platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/posts/nocobase?embed=true&utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-nocobase" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=456520&theme=light&period=weekly&topic_id=267" alt="NocoBase - Scalability-first, open-source no-code platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
</p>
|
||||||
## リリースノート
|
|
||||||
|
|
||||||
リリースノートは[ブログ](https://www.nocobase.com/ja/blog/timeline)で随時更新され、週ごとにまとめて公開しています。
|
|
||||||
|
|
||||||
## NocoBaseはなに?
|
## NocoBaseはなに?
|
||||||
|
|
||||||
@ -28,6 +24,16 @@ https://docs-cn.nocobase.com/
|
|||||||
コミュニティ:
|
コミュニティ:
|
||||||
https://forum.nocobase.com/
|
https://forum.nocobase.com/
|
||||||
|
|
||||||
|
チュートリアル:
|
||||||
|
https://www.nocobase.com/ja/tutorials
|
||||||
|
|
||||||
|
顧客のストーリー:
|
||||||
|
https://www.nocobase.com/ja/blog/tags/customer-stories
|
||||||
|
|
||||||
|
## リリースノート
|
||||||
|
|
||||||
|
リリースノートは[ブログ](https://www.nocobase.com/ja/blog/timeline)で随時更新され、週ごとにまとめて公開しています。
|
||||||
|
|
||||||
## 他の製品との違い
|
## 他の製品との違い
|
||||||
|
|
||||||
### 1. データモデル駆動
|
### 1. データモデル駆動
|
||||||
|
22
README.md
22
README.md
@ -2,19 +2,14 @@ English | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md)
|
|||||||
|
|
||||||
https://github.com/user-attachments/assets/a50c100a-4561-4e06-b2d2-d48098659ec0
|
https://github.com/user-attachments/assets/a50c100a-4561-4e06-b2d2-d48098659ec0
|
||||||
|
|
||||||
## We'd love your support!
|
<p align="center">
|
||||||
|
|
||||||
<a href="https://trendshift.io/repositories/4112" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4112" alt="nocobase%2Fnocobase | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/4112" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4112" alt="nocobase%2Fnocobase | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
<a href="https://www.producthunt.com/posts/nocobase?embed=true&utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-nocobase" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=456520&theme=light&period=weekly&topic_id=267" alt="NocoBase - Scalability-first, open-source no-code platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/posts/nocobase?embed=true&utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-nocobase" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=456520&theme=light&period=weekly&topic_id=267" alt="NocoBase - Scalability-first, open-source no-code platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
</p>
|
||||||
## Release Notes
|
|
||||||
|
|
||||||
Our [blog](https://www.nocobase.com/en/blog/timeline) is regularly updated with release notes and provides a weekly summary.
|
|
||||||
|
|
||||||
## What is NocoBase
|
## What is NocoBase
|
||||||
|
|
||||||
NocoBase is a scalability-first, open-source no-code development platform.
|
NocoBase is an extensibility-first, open-source no-code development platform.
|
||||||
Instead of investing years of time and millions of dollars in research and development, deploy NocoBase in a few minutes and you'll have a private, controllable, and extremely scalable no-code development platform!
|
Instead of investing years of time and millions of dollars in research and development, deploy NocoBase in a few minutes and you'll have a private, controllable, and extremely scalable no-code development platform!
|
||||||
|
|
||||||
Homepage:
|
Homepage:
|
||||||
@ -29,6 +24,17 @@ https://docs.nocobase.com/
|
|||||||
Forum:
|
Forum:
|
||||||
https://forum.nocobase.com/
|
https://forum.nocobase.com/
|
||||||
|
|
||||||
|
Tutorials:
|
||||||
|
https://www.nocobase.com/en/tutorials
|
||||||
|
|
||||||
|
Use Cases:
|
||||||
|
https://www.nocobase.com/en/blog/tags/customer-stories
|
||||||
|
|
||||||
|
|
||||||
|
## Release Notes
|
||||||
|
|
||||||
|
Our [blog](https://www.nocobase.com/en/blog/timeline) is regularly updated with release notes and provides a weekly summary.
|
||||||
|
|
||||||
## Distinctive features
|
## Distinctive features
|
||||||
|
|
||||||
### 1. Data model-driven
|
### 1. Data model-driven
|
||||||
|
@ -2,13 +2,10 @@
|
|||||||
|
|
||||||
https://github.com/nocobase/nocobase/assets/1267426/29623e45-9a48-4598-bb9e-9dd173ade553
|
https://github.com/nocobase/nocobase/assets/1267426/29623e45-9a48-4598-bb9e-9dd173ade553
|
||||||
|
|
||||||
## 感谢支持
|
<p align="center">
|
||||||
<a href="https://trendshift.io/repositories/4112" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4112" alt="nocobase%2Fnocobase | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/4112" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4112" alt="nocobase%2Fnocobase | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
<a href="https://www.producthunt.com/posts/nocobase?embed=true&utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-nocobase" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=456520&theme=light&period=weekly&topic_id=267" alt="NocoBase - Scalability-first, open-source no-code platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/posts/nocobase?embed=true&utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-nocobase" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=456520&theme=light&period=weekly&topic_id=267" alt="NocoBase - Scalability-first, open-source no-code platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
</p>
|
||||||
## 发布日志
|
|
||||||
我们的[博客](https://www.nocobase.com/cn/blog/timeline)会及时更新发布日志,并每周进行汇总。
|
|
||||||
|
|
||||||
## NocoBase 是什么
|
## NocoBase 是什么
|
||||||
|
|
||||||
@ -27,6 +24,15 @@ https://docs-cn.nocobase.com/
|
|||||||
社区:
|
社区:
|
||||||
https://forum.nocobase.com/
|
https://forum.nocobase.com/
|
||||||
|
|
||||||
|
教程:
|
||||||
|
https://www.nocobase.com/cn/tutorials
|
||||||
|
|
||||||
|
用户故事:
|
||||||
|
https://www.nocobase.com/cn/blog/tags/customer-stories
|
||||||
|
|
||||||
|
## 发布日志
|
||||||
|
我们的[博客](https://www.nocobase.com/cn/blog/timeline)会及时更新发布日志,并每周进行汇总。
|
||||||
|
|
||||||
## 与众不同之处
|
## 与众不同之处
|
||||||
|
|
||||||
### 1. 数据模型驱动
|
### 1. 数据模型驱动
|
||||||
|
@ -6,7 +6,7 @@ WORKDIR /app
|
|||||||
|
|
||||||
RUN cd /app \
|
RUN cd /app \
|
||||||
&& yarn config set network-timeout 600000 -g \
|
&& yarn config set network-timeout 600000 -g \
|
||||||
&& npx -y create-nocobase-app@${CNA_VERSION} my-nocobase-app -a -e APP_ENV=production \
|
&& npx -y create-nocobase-app@${CNA_VERSION} my-nocobase-app --skip-dev-dependencies -a -e APP_ENV=production \
|
||||||
&& cd /app/my-nocobase-app \
|
&& cd /app/my-nocobase-app \
|
||||||
&& yarn install --production
|
&& yarn install --production
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.7.0-alpha.10",
|
"version": "1.7.0-alpha.11",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"useWorkspaces": true,
|
"useWorkspaces": true,
|
||||||
"npmClientArgs": ["--ignore-engines"],
|
"npmClientArgs": ["--ignore-engines"],
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "@nocobase/acl",
|
"name": "@nocobase/acl",
|
||||||
"version": "1.7.0-alpha.10",
|
"version": "1.7.0-alpha.11",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
"types": "./lib/index.d.ts",
|
"types": "./lib/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nocobase/resourcer": "1.7.0-alpha.10",
|
"@nocobase/resourcer": "1.7.0-alpha.11",
|
||||||
"@nocobase/utils": "1.7.0-alpha.10",
|
"@nocobase/utils": "1.7.0-alpha.11",
|
||||||
"minimatch": "^5.1.1"
|
"minimatch": "^5.1.1"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -30,9 +30,65 @@ export function mergeRole(roles: ACLRole[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.snippets = mergeRoleSnippets(allSnippets);
|
result.snippets = mergeRoleSnippets(allSnippets);
|
||||||
|
adjustActionByStrategy(roles, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When merging permissions from multiple roles, if strategy.actions allows certain actions, then those actions have higher priority.
|
||||||
|
* For example, [
|
||||||
|
* {
|
||||||
|
* actions: {
|
||||||
|
* 'users:view': {...},
|
||||||
|
* 'users:create': {...}
|
||||||
|
* },
|
||||||
|
* strategy: {
|
||||||
|
* actions: ['view']
|
||||||
|
* }
|
||||||
|
* }]
|
||||||
|
* finally result: [{
|
||||||
|
* actions: {
|
||||||
|
* 'users:create': {...},
|
||||||
|
* 'users:view': {} // all view
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* strategy: {
|
||||||
|
* actions: ['view']
|
||||||
|
* }]
|
||||||
|
**/
|
||||||
|
function adjustActionByStrategy(
|
||||||
|
roles,
|
||||||
|
result: {
|
||||||
|
actions?: Record<string, object>;
|
||||||
|
strategy?: { actions?: string[] };
|
||||||
|
resources?: string[];
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const { actions, strategy } = result;
|
||||||
|
const actionSet = getAdjustActions(roles);
|
||||||
|
if (!_.isEmpty(actions) && !_.isEmpty(strategy?.actions) && !_.isEmpty(result.resources)) {
|
||||||
|
for (const resource of result.resources) {
|
||||||
|
for (const action of strategy.actions) {
|
||||||
|
if (actionSet.has(action)) {
|
||||||
|
actions[`${resource}:${action}`] = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAdjustActions(roles: ACLRole[]) {
|
||||||
|
const actionSet = new Set<string>();
|
||||||
|
for (const role of roles) {
|
||||||
|
const jsonRole = role.toJSON();
|
||||||
|
// Within the same role, actions have higher priority than strategy.actions.
|
||||||
|
if (!_.isEmpty(jsonRole.strategy?.['actions']) && _.isEmpty(jsonRole.actions)) {
|
||||||
|
jsonRole.strategy['actions'].forEach((x) => !x.includes('own') && actionSet.add(x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return actionSet;
|
||||||
|
}
|
||||||
|
|
||||||
function mergeRoleNames(sourceRoleNames, newRoleName) {
|
function mergeRoleNames(sourceRoleNames, newRoleName) {
|
||||||
return newRoleName ? sourceRoleNames.concat(newRoleName) : sourceRoleNames;
|
return newRoleName ? sourceRoleNames.concat(newRoleName) : sourceRoleNames;
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "@nocobase/actions",
|
"name": "@nocobase/actions",
|
||||||
"version": "1.7.0-alpha.10",
|
"version": "1.7.0-alpha.11",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
"types": "./lib/index.d.ts",
|
"types": "./lib/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nocobase/cache": "1.7.0-alpha.10",
|
"@nocobase/cache": "1.7.0-alpha.11",
|
||||||
"@nocobase/database": "1.7.0-alpha.10",
|
"@nocobase/database": "1.7.0-alpha.11",
|
||||||
"@nocobase/resourcer": "1.7.0-alpha.10"
|
"@nocobase/resourcer": "1.7.0-alpha.11"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "@nocobase/app",
|
"name": "@nocobase/app",
|
||||||
"version": "1.7.0-alpha.10",
|
"version": "1.7.0-alpha.11",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
"types": "./lib/index.d.ts",
|
"types": "./lib/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nocobase/database": "1.7.0-alpha.10",
|
"@nocobase/database": "1.7.0-alpha.11",
|
||||||
"@nocobase/preset-nocobase": "1.7.0-alpha.10",
|
"@nocobase/preset-nocobase": "1.7.0-alpha.11",
|
||||||
"@nocobase/server": "1.7.0-alpha.10"
|
"@nocobase/server": "1.7.0-alpha.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nocobase/client": "1.7.0-alpha.10"
|
"@nocobase/client": "1.7.0-alpha.11"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "@nocobase/auth",
|
"name": "@nocobase/auth",
|
||||||
"version": "1.7.0-alpha.10",
|
"version": "1.7.0-alpha.11",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
"types": "./lib/index.d.ts",
|
"types": "./lib/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nocobase/actions": "1.7.0-alpha.10",
|
"@nocobase/actions": "1.7.0-alpha.11",
|
||||||
"@nocobase/cache": "1.7.0-alpha.10",
|
"@nocobase/cache": "1.7.0-alpha.11",
|
||||||
"@nocobase/database": "1.7.0-alpha.10",
|
"@nocobase/database": "1.7.0-alpha.11",
|
||||||
"@nocobase/resourcer": "1.7.0-alpha.10",
|
"@nocobase/resourcer": "1.7.0-alpha.11",
|
||||||
"@nocobase/utils": "1.7.0-alpha.10",
|
"@nocobase/utils": "1.7.0-alpha.11",
|
||||||
"@types/jsonwebtoken": "^8.5.8",
|
"@types/jsonwebtoken": "^8.5.8",
|
||||||
"jsonwebtoken": "^8.5.1"
|
"jsonwebtoken": "^8.5.1"
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nocobase/build",
|
"name": "@nocobase/build",
|
||||||
"version": "1.7.0-alpha.10",
|
"version": "1.7.0-alpha.11",
|
||||||
"description": "Library build tool based on rollup.",
|
"description": "Library build tool based on rollup.",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"types": "./lib/index.d.ts",
|
"types": "./lib/index.d.ts",
|
||||||
@ -17,7 +17,7 @@
|
|||||||
"@lerna/project": "4.0.0",
|
"@lerna/project": "4.0.0",
|
||||||
"@rsbuild/plugin-babel": "^1.0.3",
|
"@rsbuild/plugin-babel": "^1.0.3",
|
||||||
"@rsdoctor/rspack-plugin": "^0.4.8",
|
"@rsdoctor/rspack-plugin": "^0.4.8",
|
||||||
"@rspack/core": "1.1.1",
|
"@rspack/core": "1.3.2",
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@types/gulp": "^4.0.13",
|
"@types/gulp": "^4.0.13",
|
||||||
"@types/lerna__package": "5.1.0",
|
"@types/lerna__package": "5.1.0",
|
||||||
@ -39,7 +39,7 @@
|
|||||||
"postcss-preset-env": "^9.1.2",
|
"postcss-preset-env": "^9.1.2",
|
||||||
"react-imported-component": "^6.5.4",
|
"react-imported-component": "^6.5.4",
|
||||||
"style-loader": "^3.3.3",
|
"style-loader": "^3.3.3",
|
||||||
"tar": "^6.2.0",
|
"tar": "^7.4.3",
|
||||||
"tsup": "8.2.4",
|
"tsup": "8.2.4",
|
||||||
"typescript": "5.1.3",
|
"typescript": "5.1.3",
|
||||||
"update-notifier": "3.0.0",
|
"update-notifier": "3.0.0",
|
||||||
|
@ -347,6 +347,7 @@ export async function buildPluginClient(cwd: string, userConfig: UserConfig, sou
|
|||||||
umdNamedDefine: true,
|
umdNamedDefine: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
amd: {},
|
||||||
resolve: {
|
resolve: {
|
||||||
tsConfig: path.join(process.cwd(), 'tsconfig.json'),
|
tsConfig: path.join(process.cwd(), 'tsconfig.json'),
|
||||||
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.less', '.css'],
|
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.less', '.css'],
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import tar from 'tar';
|
import { create } from 'tar';
|
||||||
import fg from 'fast-glob';
|
import fg from 'fast-glob';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
|
|
||||||
@ -38,5 +38,5 @@ export function tarPlugin(cwd: string, log: PkgLog) {
|
|||||||
|
|
||||||
fs.mkdirpSync(path.dirname(tarball));
|
fs.mkdirpSync(path.dirname(tarball));
|
||||||
fs.rmSync(tarball, { force: true });
|
fs.rmSync(tarball, { force: true });
|
||||||
return tar.c({ gzip: true, file: tarball, cwd }, tarFiles);
|
return create({ gzip: true, file: tarball, cwd }, tarFiles);
|
||||||
}
|
}
|
||||||
|
4
packages/core/cache/package.json
vendored
4
packages/core/cache/package.json
vendored
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@nocobase/cache",
|
"name": "@nocobase/cache",
|
||||||
"version": "1.7.0-alpha.10",
|
"version": "1.7.0-alpha.11",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
"types": "./lib/index.d.ts",
|
"types": "./lib/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nocobase/lock-manager": "1.7.0-alpha.10",
|
"@nocobase/lock-manager": "1.7.0-alpha.11",
|
||||||
"bloom-filters": "^3.0.1",
|
"bloom-filters": "^3.0.1",
|
||||||
"cache-manager": "^5.2.4",
|
"cache-manager": "^5.2.4",
|
||||||
"cache-manager-redis-yet": "^4.1.2"
|
"cache-manager-redis-yet": "^4.1.2"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nocobase/cli",
|
"name": "@nocobase/cli",
|
||||||
"version": "1.7.0-alpha.10",
|
"version": "1.7.0-alpha.11",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "./src/index.js",
|
"main": "./src/index.js",
|
||||||
@ -8,7 +8,7 @@
|
|||||||
"nocobase": "./bin/index.js"
|
"nocobase": "./bin/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nocobase/app": "1.7.0-alpha.10",
|
"@nocobase/app": "1.7.0-alpha.11",
|
||||||
"@types/fs-extra": "^11.0.1",
|
"@types/fs-extra": "^11.0.1",
|
||||||
"@umijs/utils": "3.5.20",
|
"@umijs/utils": "3.5.20",
|
||||||
"chalk": "^4.1.1",
|
"chalk": "^4.1.1",
|
||||||
@ -18,14 +18,14 @@
|
|||||||
"fast-glob": "^3.3.1",
|
"fast-glob": "^3.3.1",
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^11.1.1",
|
||||||
"p-all": "3.0.0",
|
"p-all": "3.0.0",
|
||||||
"pm2": "^5.2.0",
|
"pm2": "^6.0.5",
|
||||||
"portfinder": "^1.0.28",
|
"portfinder": "^1.0.28",
|
||||||
"serve": "^13.0.2",
|
"tar": "^7.4.3",
|
||||||
"tree-kill": "^1.2.2",
|
"tree-kill": "^1.2.2",
|
||||||
"tsx": "^4.19.0"
|
"tsx": "^4.19.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nocobase/devtools": "1.7.0-alpha.10"
|
"@nocobase/devtools": "1.7.0-alpha.11"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -460,8 +460,16 @@ exports.initEnv = function initEnv() {
|
|||||||
process.env.SOCKET_PATH = generateGatewayPath();
|
process.env.SOCKET_PATH = generateGatewayPath();
|
||||||
fs.mkdirpSync(dirname(process.env.SOCKET_PATH), { force: true, recursive: true });
|
fs.mkdirpSync(dirname(process.env.SOCKET_PATH), { force: true, recursive: true });
|
||||||
fs.mkdirpSync(process.env.PM2_HOME, { force: true, recursive: true });
|
fs.mkdirpSync(process.env.PM2_HOME, { force: true, recursive: true });
|
||||||
const pkgDir = resolve(process.cwd(), 'storage/plugins', '@nocobase/plugin-multi-app-manager');
|
const pkgs = [
|
||||||
fs.existsSync(pkgDir) && fs.rmdirSync(pkgDir, { force: true });
|
'@nocobase/plugin-multi-app-manager',
|
||||||
|
'@nocobase/plugin-departments',
|
||||||
|
'@nocobase/plugin-field-attachment-url',
|
||||||
|
'@nocobase/plugin-workflow-response-message',
|
||||||
|
];
|
||||||
|
for (const pkg of pkgs) {
|
||||||
|
const pkgDir = resolve(process.cwd(), 'storage/plugins', pkg);
|
||||||
|
fs.existsSync(pkgDir) && fs.rmdirSync(pkgDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.generatePlugins = function () {
|
exports.generatePlugins = function () {
|
||||||
|
@ -234,6 +234,10 @@ export default defineConfig({
|
|||||||
"title": "Filter",
|
"title": "Filter",
|
||||||
"link": "/components/filter"
|
"link": "/components/filter"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "LinkageFilter",
|
||||||
|
"link": "/components/linkage-filter"
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nocobase/client",
|
"name": "@nocobase/client",
|
||||||
"version": "1.7.0-alpha.10",
|
"version": "1.7.0-alpha.11",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"module": "es/index.mjs",
|
"module": "es/index.mjs",
|
||||||
@ -27,10 +27,10 @@
|
|||||||
"@formily/reactive-react": "^2.2.27",
|
"@formily/reactive-react": "^2.2.27",
|
||||||
"@formily/shared": "^2.2.27",
|
"@formily/shared": "^2.2.27",
|
||||||
"@formily/validator": "^2.2.27",
|
"@formily/validator": "^2.2.27",
|
||||||
"@nocobase/evaluators": "1.7.0-alpha.10",
|
"@nocobase/evaluators": "1.7.0-alpha.11",
|
||||||
"@nocobase/sdk": "1.7.0-alpha.10",
|
"@nocobase/sdk": "1.7.0-alpha.11",
|
||||||
"@nocobase/utils": "1.7.0-alpha.10",
|
"@nocobase/utils": "1.7.0-alpha.11",
|
||||||
"@nocobase/json-template-parser": "1.7.0-alpha.10",
|
"@nocobase/json-template-parser": "1.7.0-alpha.11",
|
||||||
"ahooks": "^3.7.2",
|
"ahooks": "^3.7.2",
|
||||||
"antd": "5.24.2",
|
"antd": "5.24.2",
|
||||||
"antd-style": "3.7.1",
|
"antd-style": "3.7.1",
|
||||||
|
@ -314,15 +314,15 @@ export const ACLActionProvider = (props) => {
|
|||||||
const schema = useFieldSchema();
|
const schema = useFieldSchema();
|
||||||
const currentUid = schema['x-uid'];
|
const currentUid = schema['x-uid'];
|
||||||
let actionPath = schema['x-acl-action'];
|
let actionPath = schema['x-acl-action'];
|
||||||
const editablePath = ['create', 'update', 'destroy', 'importXlsx'];
|
// 只兼容这些数据表资源按钮
|
||||||
|
const resourceActionPath = ['create', 'update', 'destroy', 'importXlsx', 'export'];
|
||||||
|
|
||||||
if (!actionPath && resource && schema['x-action'] && editablePath.includes(schema['x-action'])) {
|
if (!actionPath && resource && schema['x-action'] && resourceActionPath.includes(schema['x-action'])) {
|
||||||
actionPath = `${resource}:${schema['x-action']}`;
|
actionPath = `${resource}:${schema['x-action']}`;
|
||||||
}
|
}
|
||||||
if (actionPath && !actionPath?.includes(':')) {
|
if (actionPath && !actionPath?.includes(':')) {
|
||||||
actionPath = `${resource}:${actionPath}`;
|
actionPath = `${resource}:${actionPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = useMemo(
|
const params = useMemo(
|
||||||
() => actionPath && parseAction(actionPath, { schema, recordPkValue }),
|
() => actionPath && parseAction(actionPath, { schema, recordPkValue }),
|
||||||
[parseAction, actionPath, schema, recordPkValue],
|
[parseAction, actionPath, schema, recordPkValue],
|
||||||
@ -340,7 +340,7 @@ export const ACLActionProvider = (props) => {
|
|||||||
return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
|
return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
|
||||||
}
|
}
|
||||||
//视图表无编辑权限时不显示
|
//视图表无编辑权限时不显示
|
||||||
if (editablePath.includes(actionPath) || editablePath.includes(actionPath?.split(':')[1])) {
|
if (resourceActionPath.includes(actionPath) || resourceActionPath.includes(actionPath?.split(':')[1])) {
|
||||||
if ((collection && collection.template !== 'view') || collection?.writableView) {
|
if ((collection && collection.template !== 'view') || collection?.writableView) {
|
||||||
return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
|
return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
|
||||||
}
|
}
|
||||||
|
@ -150,6 +150,9 @@ export class APIClient extends APIClientSDK {
|
|||||||
}
|
}
|
||||||
return [{ message }];
|
return [{ message }];
|
||||||
}
|
}
|
||||||
|
if (error?.response?.data?.error) {
|
||||||
|
return [error?.response?.data?.error];
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
error?.response?.data?.errors ||
|
error?.response?.data?.errors ||
|
||||||
error?.response?.data?.messages ||
|
error?.response?.data?.messages ||
|
||||||
|
@ -11,10 +11,11 @@ import React, { FC } from 'react';
|
|||||||
import { MainComponent } from './MainComponent';
|
import { MainComponent } from './MainComponent';
|
||||||
|
|
||||||
const Loading: FC = () => <div>Loading...</div>;
|
const Loading: FC = () => <div>Loading...</div>;
|
||||||
const AppError: FC<{ error: Error }> = ({ error }) => {
|
const AppError: FC<{ error: Error & { title?: string } }> = ({ error }) => {
|
||||||
|
const title = error?.title || 'App Error';
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>App Error</div>
|
<div>{title}</div>
|
||||||
{error?.message}
|
{error?.message}
|
||||||
{process.env.__TEST__ && error?.stack}
|
{process.env.__TEST__ && error?.stack}
|
||||||
</div>
|
</div>
|
||||||
|
@ -63,7 +63,7 @@ export const SchemaInitializerItem = memo(
|
|||||||
className: className,
|
className: className,
|
||||||
label: children || compile(title),
|
label: children || compile(title),
|
||||||
onClick: (info) => {
|
onClick: (info) => {
|
||||||
if (info.key !== name) return;
|
if (disabled || info.key !== name) return;
|
||||||
if (closeInitializerMenuWhenClick) {
|
if (closeInitializerMenuWhenClick) {
|
||||||
setVisible?.(false);
|
setVisible?.(false);
|
||||||
}
|
}
|
||||||
@ -73,10 +73,10 @@ export const SchemaInitializerItem = memo(
|
|||||||
children: childrenItems,
|
children: childrenItems,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [name, style, className, children, title, onClick, icon, childrenItems]);
|
}, [name, disabled, style, className, children, title, onClick, icon, childrenItems]);
|
||||||
|
|
||||||
if (items && items.length > 0) {
|
if (items && items.length > 0) {
|
||||||
return <SchemaInitializerMenu items={menuItems}></SchemaInitializerMenu>;
|
return <SchemaInitializerMenu disabled={disabled} items={menuItems}></SchemaInitializerMenu>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -10,11 +10,11 @@
|
|||||||
import { useFieldSchema } from '@formily/react';
|
import { useFieldSchema } from '@formily/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps';
|
import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps';
|
||||||
import { DatePickerProvider, ActionBarProvider, SchemaComponentOptions } from '../schema-component';
|
import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField';
|
||||||
|
import { ActionBarProvider, DatePickerProvider, SchemaComponentOptions } from '../schema-component';
|
||||||
import { DefaultValueProvider } from '../schema-settings';
|
import { DefaultValueProvider } from '../schema-settings';
|
||||||
import { CollectOperators } from './CollectOperators';
|
import { CollectOperators } from './CollectOperators';
|
||||||
import { FormBlockProvider } from './FormBlockProvider';
|
import { FormBlockProvider } from './FormBlockProvider';
|
||||||
import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField';
|
|
||||||
|
|
||||||
export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
|
export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
|
||||||
const filedSchema = useFieldSchema();
|
const filedSchema = useFieldSchema();
|
||||||
@ -35,7 +35,7 @@ export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DefaultValueProvider isAllowToSetDefaultValue={() => false}>
|
<DefaultValueProvider isAllowToSetDefaultValue={() => false}>
|
||||||
<FormBlockProvider name="filter-form" {...props}></FormBlockProvider>
|
<FormBlockProvider name="filter-form" {...props} confirmBeforeClose={false}></FormBlockProvider>
|
||||||
</DefaultValueProvider>
|
</DefaultValueProvider>
|
||||||
</ActionBarProvider>
|
</ActionBarProvider>
|
||||||
</DatePickerProvider>
|
</DatePickerProvider>
|
||||||
|
@ -546,9 +546,11 @@ export const useFilterBlockActionProps = () => {
|
|||||||
const { doFilter } = useDoFilter();
|
const { doFilter } = useDoFilter();
|
||||||
const actionField = useField();
|
const actionField = useField();
|
||||||
actionField.data = actionField.data || {};
|
actionField.data = actionField.data || {};
|
||||||
|
const form = useForm();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async onClick() {
|
async onClick() {
|
||||||
|
await form.submit();
|
||||||
actionField.data.loading = true;
|
actionField.data.loading = true;
|
||||||
await doFilter();
|
await doFilter();
|
||||||
actionField.data.loading = false;
|
actionField.data.loading = false;
|
||||||
@ -1580,7 +1582,7 @@ export const getAppends = ({
|
|||||||
const fieldNames = getTargetField(item);
|
const fieldNames = getTargetField(item);
|
||||||
|
|
||||||
// 只应该收集关系字段,只有大于 1 的时候才是关系字段
|
// 只应该收集关系字段,只有大于 1 的时候才是关系字段
|
||||||
if (fieldNames.length > 1) {
|
if (fieldNames.length > 1 && !item.op) {
|
||||||
appends.add(fieldNames.join('.'));
|
appends.add(fieldNames.join('.'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -62,6 +62,12 @@ export class InputFieldInterface extends CollectionFieldInterface {
|
|||||||
hasDefaultValue = true;
|
hasDefaultValue = true;
|
||||||
properties = {
|
properties = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
|
trim: {
|
||||||
|
type: 'boolean',
|
||||||
|
'x-content': '{{t("Automatically remove heading and tailing spaces")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Checkbox',
|
||||||
|
},
|
||||||
layout: {
|
layout: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{t("Index")}}',
|
title: '{{t("Index")}}',
|
||||||
|
@ -129,12 +129,12 @@ export const enumType = [
|
|||||||
label: '{{t("is")}}',
|
label: '{{t("is")}}',
|
||||||
value: '$eq',
|
value: '$eq',
|
||||||
selected: true,
|
selected: true,
|
||||||
schema: { 'x-component': 'Select' },
|
schema: { 'x-component': 'Select', 'x-component-props': { mode: null } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '{{t("is not")}}',
|
label: '{{t("is not")}}',
|
||||||
value: '$ne',
|
value: '$ne',
|
||||||
schema: { 'x-component': 'Select' },
|
schema: { 'x-component': 'Select', 'x-component-props': { mode: null } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '{{t("is any of")}}',
|
label: '{{t("is any of")}}',
|
||||||
|
@ -31,6 +31,12 @@ export class TextareaFieldInterface extends CollectionFieldInterface {
|
|||||||
titleUsable = true;
|
titleUsable = true;
|
||||||
properties = {
|
properties = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
|
trim: {
|
||||||
|
type: 'boolean',
|
||||||
|
'x-content': '{{t("Automatically remove heading and tailing spaces")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Checkbox',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
schemaInitialize(schema: ISchema, { block }) {
|
schemaInitialize(schema: ISchema, { block }) {
|
||||||
if (['Table', 'Kanban'].includes(block)) {
|
if (['Table', 'Kanban'].includes(block)) {
|
||||||
|
@ -7,9 +7,9 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { filter, unionBy, uniq } from 'lodash';
|
||||||
import type { CollectionFieldOptions, GetCollectionFieldPredicate } from '../../data-source';
|
import type { CollectionFieldOptions, GetCollectionFieldPredicate } from '../../data-source';
|
||||||
import { Collection } from '../../data-source/collection/Collection';
|
import { Collection } from '../../data-source/collection/Collection';
|
||||||
import _, { filter, unionBy, uniq } from 'lodash';
|
|
||||||
|
|
||||||
export class InheritanceCollectionMixin extends Collection {
|
export class InheritanceCollectionMixin extends Collection {
|
||||||
protected parentCollectionsName: string[];
|
protected parentCollectionsName: string[];
|
||||||
@ -22,6 +22,7 @@ export class InheritanceCollectionMixin extends Collection {
|
|||||||
protected parentCollectionFields: Record<string, CollectionFieldOptions[]> = {};
|
protected parentCollectionFields: Record<string, CollectionFieldOptions[]> = {};
|
||||||
protected allCollectionsInheritChain: string[];
|
protected allCollectionsInheritChain: string[];
|
||||||
protected inheritCollectionsChain: string[];
|
protected inheritCollectionsChain: string[];
|
||||||
|
protected inheritChain: string[];
|
||||||
protected foreignKeyFields: CollectionFieldOptions[];
|
protected foreignKeyFields: CollectionFieldOptions[];
|
||||||
|
|
||||||
getParentCollectionsName() {
|
getParentCollectionsName() {
|
||||||
@ -233,6 +234,43 @@ export class InheritanceCollectionMixin extends Collection {
|
|||||||
return this.inheritCollectionsChain;
|
return this.inheritCollectionsChain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有祖先数据表和后代数据表,不包括兄弟表。用于下面这些地方:
|
||||||
|
* - 筛选区块链接数据区块时使用
|
||||||
|
*/
|
||||||
|
getInheritChain() {
|
||||||
|
if (this.inheritChain) {
|
||||||
|
return this.inheritChain.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ancestorChain = this.getInheritCollectionsChain();
|
||||||
|
const descendantNames = this.getChildrenCollectionsName();
|
||||||
|
|
||||||
|
// 构建最终的链,首先包含祖先链(包括自身)
|
||||||
|
const inheritChain = [...ancestorChain];
|
||||||
|
|
||||||
|
// 再添加直接后代及其后代,但不包括兄弟表
|
||||||
|
const addDescendants = (names: string[]) => {
|
||||||
|
for (const name of names) {
|
||||||
|
if (!inheritChain.includes(name)) {
|
||||||
|
inheritChain.push(name);
|
||||||
|
const childCollection = this.collectionManager.getCollection<InheritanceCollectionMixin>(name);
|
||||||
|
if (childCollection) {
|
||||||
|
// 递归添加每个后代的后代
|
||||||
|
const childrenNames = childCollection.getChildrenCollectionsName();
|
||||||
|
addDescendants(childrenNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从当前集合的直接后代开始添加
|
||||||
|
addDescendants(descendantNames);
|
||||||
|
|
||||||
|
this.inheritChain = inheritChain;
|
||||||
|
return this.inheritChain;
|
||||||
|
}
|
||||||
|
|
||||||
getAllFields(predicate?: GetCollectionFieldPredicate) {
|
getAllFields(predicate?: GetCollectionFieldPredicate) {
|
||||||
if (this.allFields) {
|
if (this.allFields) {
|
||||||
return this.allFields.slice();
|
return this.allFields.slice();
|
||||||
|
@ -0,0 +1,189 @@
|
|||||||
|
import { Application } from '@nocobase/client';
|
||||||
|
import { CollectionManager } from '../../../data-source/collection/CollectionManager';
|
||||||
|
import { InheritanceCollectionMixin } from '../InheritanceCollectionMixin';
|
||||||
|
|
||||||
|
describe('InheritanceCollectionMixin', () => {
|
||||||
|
let app: Application;
|
||||||
|
let collectionManager: CollectionManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
app = new Application({
|
||||||
|
dataSourceManager: {
|
||||||
|
collectionMixins: [InheritanceCollectionMixin],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
collectionManager = app.getCollectionManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getInheritChain', () => {
|
||||||
|
it('should return itself when there are no ancestors or descendants', () => {
|
||||||
|
const options = {
|
||||||
|
name: 'test',
|
||||||
|
fields: [{ name: 'field1', interface: 'input' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
collectionManager.addCollections([options]);
|
||||||
|
const collection = collectionManager.getCollection<InheritanceCollectionMixin>('test');
|
||||||
|
|
||||||
|
const inheritChain = collection.getInheritChain();
|
||||||
|
expect(inheritChain).toEqual(['test']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a chain including all ancestor tables', () => {
|
||||||
|
// 创建三代数据表结构:grandparent -> parent -> child
|
||||||
|
const grandparentOptions = {
|
||||||
|
name: 'grandparent',
|
||||||
|
fields: [{ name: 'field1', interface: 'input' }],
|
||||||
|
};
|
||||||
|
const parentOptions = {
|
||||||
|
name: 'parent',
|
||||||
|
inherits: ['grandparent'],
|
||||||
|
fields: [{ name: 'field2', interface: 'input' }],
|
||||||
|
};
|
||||||
|
const childOptions = {
|
||||||
|
name: 'child',
|
||||||
|
inherits: ['parent'],
|
||||||
|
fields: [{ name: 'field3', interface: 'input' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 先将所有集合添加到 collectionManager
|
||||||
|
collectionManager.addCollections([grandparentOptions, parentOptions, childOptions]);
|
||||||
|
|
||||||
|
// 获取最终的集合实例以调用方法
|
||||||
|
const child = collectionManager.getCollection<InheritanceCollectionMixin>('child');
|
||||||
|
|
||||||
|
// 测试 child 的继承链包含所有祖先表
|
||||||
|
const inheritChain = child.getInheritChain();
|
||||||
|
expect(inheritChain).toContain('child');
|
||||||
|
expect(inheritChain).toContain('parent');
|
||||||
|
expect(inheritChain).toContain('grandparent');
|
||||||
|
expect(inheritChain.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include all descendant tables, but not sibling tables', () => {
|
||||||
|
// 创建具有兄弟和后代关系的数据表结构
|
||||||
|
// parent (祖先表)
|
||||||
|
// |-- child1 (子表)
|
||||||
|
// | |-- grandChild1 (孙表1)
|
||||||
|
// | |-- grandChild2 (孙表2)
|
||||||
|
// |-- child2 (兄弟表)
|
||||||
|
// |-- grandChild3 (兄弟的子表,不应该包括在测试集合的继承链中)
|
||||||
|
|
||||||
|
const collections = [
|
||||||
|
{
|
||||||
|
name: 'parent',
|
||||||
|
fields: [{ name: 'parentField', interface: 'input' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'child1',
|
||||||
|
inherits: ['parent'],
|
||||||
|
fields: [{ name: 'child1Field', interface: 'input' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'child2',
|
||||||
|
inherits: ['parent'],
|
||||||
|
fields: [{ name: 'child2Field', interface: 'input' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'grandChild1',
|
||||||
|
inherits: ['child1'],
|
||||||
|
fields: [{ name: 'grandChild1Field', interface: 'input' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'grandChild2',
|
||||||
|
inherits: ['child1'],
|
||||||
|
fields: [{ name: 'grandChild2Field', interface: 'input' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'grandChild3',
|
||||||
|
inherits: ['child2'],
|
||||||
|
fields: [{ name: 'grandChild3Field', interface: 'input' }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 一次性添加所有集合
|
||||||
|
collectionManager.addCollections(collections);
|
||||||
|
|
||||||
|
// 获取要测试的集合实例
|
||||||
|
const child1 = collectionManager.getCollection<InheritanceCollectionMixin>('child1');
|
||||||
|
|
||||||
|
// 测试 child1 的继承链
|
||||||
|
const child1InheritChain = child1.getInheritChain();
|
||||||
|
|
||||||
|
// 应该包含自身、父表和子表
|
||||||
|
expect(child1InheritChain).toContain('child1');
|
||||||
|
expect(child1InheritChain).toContain('parent');
|
||||||
|
expect(child1InheritChain).toContain('grandChild1');
|
||||||
|
expect(child1InheritChain).toContain('grandChild2');
|
||||||
|
|
||||||
|
// 不应该包含兄弟表及其子表
|
||||||
|
expect(child1InheritChain).not.toContain('child2');
|
||||||
|
expect(child1InheritChain).not.toContain('grandChild3');
|
||||||
|
|
||||||
|
// 检查总数量是否正确 (parent, child1, grandChild1, grandChild2)
|
||||||
|
expect(child1InheritChain.length).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly handle multiple inheritance', () => {
|
||||||
|
// 创建多重继承的数据表结构
|
||||||
|
// parent1 parent2
|
||||||
|
// \ /
|
||||||
|
// \ /
|
||||||
|
// child
|
||||||
|
// |
|
||||||
|
// grandChild
|
||||||
|
|
||||||
|
const collections = [
|
||||||
|
{
|
||||||
|
name: 'parent1',
|
||||||
|
fields: [{ name: 'parent1Field', interface: 'input' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parent2',
|
||||||
|
fields: [{ name: 'parent2Field', interface: 'input' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'child',
|
||||||
|
inherits: ['parent1', 'parent2'],
|
||||||
|
fields: [{ name: 'childField', interface: 'input' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'grandChild',
|
||||||
|
inherits: ['child'],
|
||||||
|
fields: [{ name: 'grandChildField', interface: 'input' }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 一次性添加所有集合
|
||||||
|
collectionManager.addCollections(collections);
|
||||||
|
|
||||||
|
// 获取要测试的集合实例
|
||||||
|
const child = collectionManager.getCollection<InheritanceCollectionMixin>('child');
|
||||||
|
const grandChild = collectionManager.getCollection<InheritanceCollectionMixin>('grandChild');
|
||||||
|
|
||||||
|
// 测试 child 的继承链
|
||||||
|
const childInheritChain = child.getInheritChain();
|
||||||
|
|
||||||
|
// 应该包含自身、两个父表和子表
|
||||||
|
expect(childInheritChain).toContain('child');
|
||||||
|
expect(childInheritChain).toContain('parent1');
|
||||||
|
expect(childInheritChain).toContain('parent2');
|
||||||
|
expect(childInheritChain).toContain('grandChild');
|
||||||
|
|
||||||
|
// 检查总数量是否正确 (child, parent1, parent2, grandChild)
|
||||||
|
expect(childInheritChain.length).toBe(4);
|
||||||
|
|
||||||
|
// 测试 grandChild 的继承链
|
||||||
|
const grandChildInheritChain = grandChild.getInheritChain();
|
||||||
|
|
||||||
|
// 应该包含自身及所有祖先表
|
||||||
|
expect(grandChildInheritChain).toContain('grandChild');
|
||||||
|
expect(grandChildInheritChain).toContain('child');
|
||||||
|
expect(grandChildInheritChain).toContain('parent1');
|
||||||
|
expect(grandChildInheritChain).toContain('parent2');
|
||||||
|
|
||||||
|
// 检查总数量是否正确 (grandChild, child, parent1, parent2)
|
||||||
|
expect(grandChildInheritChain.length).toBe(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -18,7 +18,14 @@ export interface SelectWithTitleProps {
|
|||||||
onChange?: (...args: any[]) => void;
|
onChange?: (...args: any[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectWithTitle({ title, defaultValue, onChange, options, fieldNames }: SelectWithTitleProps) {
|
export function SelectWithTitle({
|
||||||
|
title,
|
||||||
|
defaultValue,
|
||||||
|
onChange,
|
||||||
|
options,
|
||||||
|
fieldNames,
|
||||||
|
...others
|
||||||
|
}: SelectWithTitleProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const timerRef = useRef<any>(null);
|
const timerRef = useRef<any>(null);
|
||||||
return (
|
return (
|
||||||
@ -36,6 +43,7 @@ export function SelectWithTitle({ title, defaultValue, onChange, options, fieldN
|
|||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
<Select
|
<Select
|
||||||
|
{...others}
|
||||||
open={open}
|
open={open}
|
||||||
data-testid={`select-${title}`}
|
data-testid={`select-${title}`}
|
||||||
popupMatchSelectWidth={false}
|
popupMatchSelectWidth={false}
|
||||||
|
@ -18,6 +18,7 @@ import { useCollectionFieldUISchema, useIsInNocoBaseRecursionFieldContext } from
|
|||||||
import { useDynamicComponentProps } from '../../hoc/withDynamicSchemaProps';
|
import { useDynamicComponentProps } from '../../hoc/withDynamicSchemaProps';
|
||||||
import { useCompile, useComponent } from '../../schema-component';
|
import { useCompile, useComponent } from '../../schema-component';
|
||||||
import { useIsAllowToSetDefaultValue } from '../../schema-settings/hooks/useIsAllowToSetDefaultValue';
|
import { useIsAllowToSetDefaultValue } from '../../schema-settings/hooks/useIsAllowToSetDefaultValue';
|
||||||
|
import { isVariable } from '../../variables/utils/isVariable';
|
||||||
import { CollectionFieldProvider, useCollectionField } from './CollectionFieldProvider';
|
import { CollectionFieldProvider, useCollectionField } from './CollectionFieldProvider';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -104,19 +105,45 @@ const CollectionFieldInternalField = (props) => {
|
|||||||
const dynamicProps = useDynamicComponentProps(uiSchema?.['x-use-component-props'], props);
|
const dynamicProps = useDynamicComponentProps(uiSchema?.['x-use-component-props'], props);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// There seems to be a bug in formily where after setting a field to readPretty, switching to editable,
|
/**
|
||||||
// then back to readPretty, and refreshing the page, the field remains in editable state. The expected state is readPretty.
|
* There seems to be a bug in formily where after setting a field to readPretty, switching to editable,
|
||||||
// This code is meant to fix this issue.
|
* then back to readPretty, and refreshing the page, the field remains in editable state. The expected state is readPretty.
|
||||||
|
* This code is meant to fix this issue.
|
||||||
|
*/
|
||||||
if (fieldSchema['x-read-pretty'] === true && !field.readPretty) {
|
if (fieldSchema['x-read-pretty'] === true && !field.readPretty) {
|
||||||
field.readPretty = true;
|
field.readPretty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This solves the issue: After creating a form and setting a field to "read-only", the field remains editable when refreshing the page and reopening the dialog.
|
||||||
|
*
|
||||||
|
* Note: This might be a bug in Formily
|
||||||
|
* When both x-disabled and x-read-pretty exist in the Schema:
|
||||||
|
* - If x-disabled appears before x-read-pretty in the Schema JSON, the disabled state becomes ineffective
|
||||||
|
* - The reason is that during field instance initialization, field.disabled is set before field.readPretty, which causes the pattern value to be changed to 'editable'
|
||||||
|
* - This issue is related to the order of JSON fields, which might return different orders in different environments (databases), thus making the issue inconsistent to reproduce
|
||||||
|
*
|
||||||
|
* Reference to Formily source code:
|
||||||
|
* 1. Setting readPretty may cause pattern to be changed to 'editable': https://github.com/alibaba/formily/blob/d4bb96c40e7918210b1bd7d57b8fadee0cfe4b26/packages/core/src/models/BaseField.ts#L208-L224
|
||||||
|
* 2. The execution order of the each method depends on the order of JSON fields: https://github.com/alibaba/formily/blob/123d536b6076196e00b4e02ee160d72480359f54/packages/json-schema/src/schema.ts#L486-L519
|
||||||
|
*/
|
||||||
|
if (fieldSchema['x-disabled'] === true) {
|
||||||
|
field.disabled = true;
|
||||||
|
}
|
||||||
field.data = field.data || {};
|
field.data = field.data || {};
|
||||||
field.data.dataSource = uiSchema?.enum;
|
field.data.dataSource = uiSchema?.enum;
|
||||||
}, [field, fieldSchema]);
|
}, [field, fieldSchema]);
|
||||||
|
|
||||||
if (!uiSchema) return null;
|
if (!uiSchema) return null;
|
||||||
|
|
||||||
return <Component {...props} {...dynamicProps} />;
|
const mergedProps = { ...props, ...dynamicProps };
|
||||||
|
|
||||||
|
// Prevent displaying the variable string first, then the variable value
|
||||||
|
if (isVariable(mergedProps.value) && mergedProps.value === fieldSchema.default) {
|
||||||
|
mergedProps.value = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Component {...mergedProps} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CollectionField = connect((props) => {
|
export const CollectionField = connect((props) => {
|
||||||
|
@ -29,7 +29,7 @@ export function useDataSourceManager() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前 collection 继承链路上的所有 collection
|
* 获取当前 collection 继承链路上的所有 collection(不包括兄弟表)
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useAllCollectionsInheritChainGetter() {
|
export function useAllCollectionsInheritChainGetter() {
|
||||||
@ -39,7 +39,7 @@ export function useAllCollectionsInheritChainGetter() {
|
|||||||
return dm
|
return dm
|
||||||
?.getDataSource(customDataSource)
|
?.getDataSource(customDataSource)
|
||||||
?.collectionManager?.getCollection<InheritanceCollectionMixin>(collectionName)
|
?.collectionManager?.getCollection<InheritanceCollectionMixin>(collectionName)
|
||||||
?.getAllCollectionsInheritChain();
|
?.getInheritChain();
|
||||||
},
|
},
|
||||||
[dm],
|
[dm],
|
||||||
);
|
);
|
||||||
|
@ -19,6 +19,7 @@ import { mergeFilter, useAssociatedFields } from './utils';
|
|||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import React, { createContext, useCallback, useEffect, useMemo, useRef } from 'react';
|
import React, { createContext, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { useAllDataBlocks } from '../schema-component/antd/page/AllDataBlocksProvider';
|
||||||
|
|
||||||
enum FILTER_OPERATOR {
|
enum FILTER_OPERATOR {
|
||||||
AND = '$and',
|
AND = '$and',
|
||||||
@ -71,6 +72,10 @@ export interface DataBlock {
|
|||||||
* manual: 只有当点击了筛选按钮,才会请求数据
|
* manual: 只有当点击了筛选按钮,才会请求数据
|
||||||
*/
|
*/
|
||||||
dataLoadingMode?: 'auto' | 'manual';
|
dataLoadingMode?: 'auto' | 'manual';
|
||||||
|
/** 让整个区块悬浮起来 */
|
||||||
|
highlightBlock: () => void;
|
||||||
|
/** 取消悬浮 */
|
||||||
|
unhighlightBlock: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FilterContextValue {
|
interface FilterContextValue {
|
||||||
@ -124,7 +129,7 @@ export const DataBlockCollector = ({
|
|||||||
const field = useField();
|
const field = useField();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const associatedFields = useAssociatedFields();
|
const associatedFields = useAssociatedFields();
|
||||||
const container = useRef(null);
|
const container = useRef<HTMLDivElement | null>(null);
|
||||||
const dataLoadingMode = useDataLoadingMode();
|
const dataLoadingMode = useDataLoadingMode();
|
||||||
|
|
||||||
const shouldApplyFilter =
|
const shouldApplyFilter =
|
||||||
@ -172,6 +177,34 @@ export const DataBlockCollector = ({
|
|||||||
field.data?.clearSelectedRowKeys?.();
|
field.data?.clearSelectedRowKeys?.();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
highlightBlock() {
|
||||||
|
const dom = container.current;
|
||||||
|
|
||||||
|
if (!dom) return;
|
||||||
|
|
||||||
|
const designer = dom.querySelector('.ant-nb-schema-toolbar');
|
||||||
|
if (designer) {
|
||||||
|
designer.classList.remove(process.env.__E2E__ ? 'hidden-e2e' : 'hidden');
|
||||||
|
}
|
||||||
|
dom.style.boxShadow = '0 3px 12px rgba(0, 0, 0, 0.15)';
|
||||||
|
dom.style.transition = 'box-shadow 0.3s ease, transform 0.2s ease';
|
||||||
|
dom.scrollIntoView?.({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
unhighlightBlock() {
|
||||||
|
const dom = container.current;
|
||||||
|
|
||||||
|
if (!dom) return;
|
||||||
|
|
||||||
|
const designer = dom.querySelector('.ant-nb-schema-toolbar');
|
||||||
|
if (designer) {
|
||||||
|
designer.classList.add(process.env.__E2E__ ? 'hidden-e2e' : 'hidden');
|
||||||
|
}
|
||||||
|
dom.style.boxShadow = 'none';
|
||||||
|
dom.style.transition = 'box-shadow 0.3s ease, transform 0.2s ease';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
associatedFields,
|
associatedFields,
|
||||||
@ -197,12 +230,14 @@ export const DataBlockCollector = ({
|
|||||||
*/
|
*/
|
||||||
export const useFilterBlock = () => {
|
export const useFilterBlock = () => {
|
||||||
const ctx = React.useContext(FilterContext);
|
const ctx = React.useContext(FilterContext);
|
||||||
|
const allDataBlocksCtx = useAllDataBlocks();
|
||||||
|
|
||||||
// 有可能存在页面没有提供 FilterBlockProvider 的情况,比如内部使用的数据表管理页面
|
// 有可能存在页面没有提供 FilterBlockProvider 的情况,比如内部使用的数据表管理页面
|
||||||
const getDataBlocks = useCallback<() => DataBlock[]>(() => ctx?.getDataBlocks() || [], [ctx]);
|
const getDataBlocks = useCallback<() => DataBlock[]>(() => ctx?.getDataBlocks() || [], [ctx]);
|
||||||
|
|
||||||
const recordDataBlocks = useCallback(
|
const recordDataBlocks = useCallback(
|
||||||
(block: DataBlock) => {
|
(block: DataBlock) => {
|
||||||
|
allDataBlocksCtx.recordDataBlocks(block);
|
||||||
const existingBlock = ctx?.getDataBlocks().find((item) => item.uid === block.uid);
|
const existingBlock = ctx?.getDataBlocks().find((item) => item.uid === block.uid);
|
||||||
|
|
||||||
if (existingBlock) {
|
if (existingBlock) {
|
||||||
@ -218,6 +253,7 @@ export const useFilterBlock = () => {
|
|||||||
|
|
||||||
const removeDataBlock = useCallback(
|
const removeDataBlock = useCallback(
|
||||||
(uid: string) => {
|
(uid: string) => {
|
||||||
|
allDataBlocksCtx.removeDataBlock(uid);
|
||||||
if (ctx?.getDataBlocks().every((item) => item.uid !== uid)) return;
|
if (ctx?.getDataBlocks().every((item) => item.uid !== uid)) return;
|
||||||
ctx?.setDataBlocks((prev) => prev.filter((item) => item.uid !== uid));
|
ctx?.setDataBlocks((prev) => prev.filter((item) => item.uid !== uid));
|
||||||
},
|
},
|
||||||
|
@ -67,6 +67,108 @@ describe('getSupportFieldsByAssociation', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getSupportFieldsByForeignKey', () => {
|
describe('getSupportFieldsByForeignKey', () => {
|
||||||
|
it('should return foreign key fields matching both name and target collection', () => {
|
||||||
|
const filterBlockCollection = {
|
||||||
|
fields: [
|
||||||
|
{ id: 1, name: 'field1', type: 'hasMany', foreignKey: 'fk1', target: 'collection1' },
|
||||||
|
{ id: 2, name: 'field2', type: 'hasMany', foreignKey: 'fk2', target: 'collection2' },
|
||||||
|
{ id: 3, name: 'field3', type: 'hasMany', foreignKey: 'fk3', target: 'collection3' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const block = {
|
||||||
|
foreignKeyFields: [
|
||||||
|
{ id: 1, name: 'fk1', collectionName: 'collection1' },
|
||||||
|
{ id: 2, name: 'fk2', collectionName: 'collection2' },
|
||||||
|
{ id: 3, name: 'fk3', collectionName: 'collection3' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ id: 1, name: 'fk1', collectionName: 'collection1' },
|
||||||
|
{ id: 2, name: 'fk2', collectionName: 'collection2' },
|
||||||
|
{ id: 3, name: 'fk3', collectionName: 'collection3' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not return foreign key fields when target collection doesn't match", () => {
|
||||||
|
const filterBlockCollection = {
|
||||||
|
fields: [
|
||||||
|
{ id: 1, name: 'field1', type: 'hasMany', foreignKey: 'fk1', target: 'collection1' },
|
||||||
|
{ id: 2, name: 'field2', type: 'hasMany', foreignKey: 'fk2', target: 'collectionX' }, // target不匹配
|
||||||
|
{ id: 3, name: 'field3', type: 'hasMany', foreignKey: 'fk3', target: 'collection3' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const block = {
|
||||||
|
foreignKeyFields: [
|
||||||
|
{ id: 1, name: 'fk1', collectionName: 'collection1' },
|
||||||
|
{ id: 2, name: 'fk2', collectionName: 'collection2' }, // 与field2的target不匹配
|
||||||
|
{ id: 3, name: 'fk3', collectionName: 'collection3' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ id: 1, name: 'fk1', collectionName: 'collection1' },
|
||||||
|
{ id: 3, name: 'fk3', collectionName: 'collection3' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter out belongsTo type fields', () => {
|
||||||
|
const filterBlockCollection = {
|
||||||
|
fields: [
|
||||||
|
{ id: 1, name: 'field1', type: 'hasMany', foreignKey: 'fk1', target: 'collection1' },
|
||||||
|
{ id: 2, name: 'field2', type: 'belongsTo', foreignKey: 'fk2', target: 'collection2' }, // belongsTo类型
|
||||||
|
{ id: 3, name: 'field3', type: 'hasMany', foreignKey: 'fk3', target: 'collection3' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const block = {
|
||||||
|
foreignKeyFields: [
|
||||||
|
{ id: 1, name: 'fk1', collectionName: 'collection1' },
|
||||||
|
{ id: 2, name: 'fk2', collectionName: 'collection2' },
|
||||||
|
{ id: 3, name: 'fk3', collectionName: 'collection3' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ id: 1, name: 'fk1', collectionName: 'collection1' },
|
||||||
|
{ id: 3, name: 'fk3', collectionName: 'collection3' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle when both name and target collection match', () => {
|
||||||
|
const filterBlockCollection = {
|
||||||
|
fields: [
|
||||||
|
{ id: 1, name: 'field1', type: 'hasMany', foreignKey: 'fk1', target: 'collection1' },
|
||||||
|
{ id: 2, name: 'field2', type: 'hasOne', foreignKey: 'fk2', target: 'collection2' },
|
||||||
|
{ id: 3, name: 'field3', type: 'hasMany', foreignKey: 'fk3', target: 'wrongCollection' }, // 目标表不匹配
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const block = {
|
||||||
|
foreignKeyFields: [
|
||||||
|
{ id: 1, name: 'fk1', collectionName: 'collection1' },
|
||||||
|
{ id: 2, name: 'fk2', collectionName: 'collection2' },
|
||||||
|
{ id: 3, name: 'fk3', collectionName: 'collection3' }, // 与field3的target不匹配
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ id: 1, name: 'fk1', collectionName: 'collection1' },
|
||||||
|
{ id: 2, name: 'fk2', collectionName: 'collection2' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保留原有的通用测试用例
|
||||||
it("should return all foreign key fields matching the filter block collection's foreign key properties", () => {
|
it("should return all foreign key fields matching the filter block collection's foreign key properties", () => {
|
||||||
const filterBlockCollection = {
|
const filterBlockCollection = {
|
||||||
fields: [
|
fields: [
|
||||||
|
47
packages/core/client/src/filter-provider/highlightBlock.ts
Normal file
47
packages/core/client/src/filter-provider/highlightBlock.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
let container: HTMLElement | null = null;
|
||||||
|
|
||||||
|
export const highlightBlock = (clonedBlockDom: HTMLElement, boxRect: DOMRect) => {
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
container.style.position = 'absolute';
|
||||||
|
container.style.transition = 'opacity 0.3s ease';
|
||||||
|
container.style.pointerEvents = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(clonedBlockDom);
|
||||||
|
container.style.opacity = '1';
|
||||||
|
container.style.width = `${boxRect.width}px`;
|
||||||
|
container.style.height = `${boxRect.height}px`;
|
||||||
|
container.style.top = `${boxRect.top}px`;
|
||||||
|
container.style.left = `${boxRect.left}px`;
|
||||||
|
container.style.zIndex = '2000';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unhighlightBlock = () => {
|
||||||
|
if (container) {
|
||||||
|
container.style.opacity = '0';
|
||||||
|
container.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const startScrollEndTracking = (dom: HTMLElement & { _prevRect?: DOMRect; _timer?: any }, callback: () => void) => {
|
||||||
|
dom._timer = setInterval(() => {
|
||||||
|
const prevRect = dom._prevRect;
|
||||||
|
const currentRect = dom.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (!prevRect || currentRect.top !== prevRect.top) {
|
||||||
|
dom._prevRect = currentRect;
|
||||||
|
} else {
|
||||||
|
clearInterval(dom._timer);
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const stopScrollEndTracking = (dom: HTMLElement & { _timer?: any }) => {
|
||||||
|
if (dom._timer) {
|
||||||
|
clearInterval(dom._timer);
|
||||||
|
dom._timer = null;
|
||||||
|
}
|
||||||
|
}
|
@ -49,9 +49,13 @@ export const getSupportFieldsByAssociation = (inheritCollectionsChain: string[],
|
|||||||
|
|
||||||
export const getSupportFieldsByForeignKey = (filterBlockCollection: Collection, block: DataBlock) => {
|
export const getSupportFieldsByForeignKey = (filterBlockCollection: Collection, block: DataBlock) => {
|
||||||
return block.foreignKeyFields?.filter((foreignKeyField) => {
|
return block.foreignKeyFields?.filter((foreignKeyField) => {
|
||||||
return filterBlockCollection.fields.some(
|
return filterBlockCollection.fields.some((field) => {
|
||||||
(field) => field.type !== 'belongsTo' && field.foreignKey === foreignKeyField.name,
|
return (
|
||||||
);
|
field.type !== 'belongsTo' &&
|
||||||
|
field.foreignKey === foreignKeyField.name && // 1. 外键字段的 name 要一致
|
||||||
|
field.target === foreignKeyField.collectionName // 2. 关系字段的目标表要和外键的数据表一致
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -193,19 +197,21 @@ export const useFilterAPI = () => {
|
|||||||
|
|
||||||
const doFilter = useCallback(
|
const doFilter = useCallback(
|
||||||
(
|
(
|
||||||
value: any | ((target: FilterTarget['targets'][0], block: DataBlock) => any),
|
value: any | ((target: FilterTarget['targets'][0], block: DataBlock, sourceKey?: string) => any),
|
||||||
field: string | ((target: FilterTarget['targets'][0], block: DataBlock) => string) = 'id',
|
field: string | ((target: FilterTarget['targets'][0], block: DataBlock) => string) = 'id',
|
||||||
operator: string | ((target: FilterTarget['targets'][0]) => string) = '$eq',
|
operator: string | ((target: FilterTarget['targets'][0]) => string) = '$eq',
|
||||||
) => {
|
) => {
|
||||||
|
const currentBlock = dataBlocks.find((block) => block.uid === fieldSchema.parent['x-uid']);
|
||||||
dataBlocks.forEach((block) => {
|
dataBlocks.forEach((block) => {
|
||||||
|
let key = field as string;
|
||||||
const target = targets.find((target) => target.uid === block.uid);
|
const target = targets.find((target) => target.uid === block.uid);
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
if (_.isFunction(value)) {
|
if (_.isFunction(value)) {
|
||||||
value = value(target, block);
|
value = value(target, block, getSourceKey(currentBlock, target.field));
|
||||||
}
|
}
|
||||||
if (_.isFunction(field)) {
|
if (_.isFunction(field)) {
|
||||||
field = field(target, block);
|
key = field(target, block);
|
||||||
}
|
}
|
||||||
if (_.isFunction(operator)) {
|
if (_.isFunction(operator)) {
|
||||||
operator = operator(target);
|
operator = operator(target);
|
||||||
@ -219,7 +225,7 @@ export const useFilterAPI = () => {
|
|||||||
storedFilter[uid] = {
|
storedFilter[uid] = {
|
||||||
$and: [
|
$and: [
|
||||||
{
|
{
|
||||||
[field]: {
|
[key]: {
|
||||||
[operator]: value,
|
[operator]: value,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -248,7 +254,7 @@ export const useFilterAPI = () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[dataBlocks, targets, uid],
|
[dataBlocks, targets, uid, fieldSchema],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -268,3 +274,8 @@ export const isInFilterFormBlock = (fieldSchema: Schema) => {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getSourceKey(currentBlock: DataBlock, field: string) {
|
||||||
|
const associationField = currentBlock?.associatedFields?.find((item) => item.foreignKey === field);
|
||||||
|
return associationField?.sourceKey || field?.split?.('.')?.[1];
|
||||||
|
}
|
||||||
|
@ -49,6 +49,8 @@ export interface CustomToken extends AliasToken {
|
|||||||
marginBlock: number;
|
marginBlock: number;
|
||||||
/** 区块的圆角 */
|
/** 区块的圆角 */
|
||||||
borderRadiusBlock: number;
|
borderRadiusBlock: number;
|
||||||
|
|
||||||
|
siderWidth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThemeConfig extends _ThemeConfig {
|
export interface ThemeConfig extends _ThemeConfig {
|
||||||
|
12
packages/core/client/src/i18n/constant.ts
Normal file
12
packages/core/client/src/i18n/constant.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const NAMESPACE_UI_SCHEMA = 'ui-schema-storage';
|
||||||
|
|
||||||
|
export { NAMESPACE_UI_SCHEMA };
|
@ -8,3 +8,4 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './i18n';
|
export * from './i18n';
|
||||||
|
export * from './constant';
|
||||||
|
@ -884,5 +884,10 @@
|
|||||||
"If selected, the page will display Tab pages.": "Wenn ausgewählt, zeigt die Seite Tab-Seiten an.",
|
"If selected, the page will display Tab pages.": "Wenn ausgewählt, zeigt die Seite Tab-Seiten an.",
|
||||||
"If selected, the route will be displayed in the menu.": "Wenn ausgewählt, wird die Route im Menü angezeigt.",
|
"If selected, the route will be displayed in the menu.": "Wenn ausgewählt, wird die Route im Menü angezeigt.",
|
||||||
"Are you sure you want to hide this tab?": "Sind Sie sicher, dass Sie diesen Tab ausblenden möchten?",
|
"Are you sure you want to hide this tab?": "Sind Sie sicher, dass Sie diesen Tab ausblenden möchten?",
|
||||||
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Nach dem Ausblenden wird dieser Tab nicht mehr in der Tableiste angezeigt. Um ihn wieder anzuzeigen, müssen Sie zur Routenverwaltungsseite gehen, um ihn einzustellen."
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Nach dem Ausblenden wird dieser Tab nicht mehr in der Tableiste angezeigt. Um ihn wieder anzuzeigen, müssen Sie zur Routenverwaltungsseite gehen, um ihn einzustellen.",
|
||||||
|
"No pages yet, please configure first": "Noch keine Seiten, bitte zuerst konfigurieren",
|
||||||
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Klicken Sie auf das \"UI-Editor\"-Symbol in der oberen rechten Ecke, um den UI-Editor-Modus zu betreten",
|
||||||
|
"Refresh data blocks": "Aktualisieren Sie die Datenblöcke",
|
||||||
|
"Select data blocks to refresh": "Wählen Sie die Datenblöcke aus, die aktualisiert werden sollen.",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Nach erfolgreicher Übermittlung werden die ausgewählten Datenblöcke automatisch aktualisiert."
|
||||||
}
|
}
|
||||||
|
@ -888,8 +888,14 @@
|
|||||||
"If selected, the route will be displayed in the menu.": "If selected, the route will be displayed in the menu.",
|
"If selected, the route will be displayed in the menu.": "If selected, the route will be displayed in the menu.",
|
||||||
"Are you sure you want to hide this tab?": "Are you sure you want to hide this tab?",
|
"Are you sure you want to hide this tab?": "Are you sure you want to hide this tab?",
|
||||||
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.",
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.",
|
||||||
|
"No pages yet, please configure first": "No pages yet, please configure first",
|
||||||
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode",
|
||||||
"Deprecated": "Deprecated",
|
"Deprecated": "Deprecated",
|
||||||
"The following old template features have been deprecated and will be removed in next version.": "The following old template features have been deprecated and will be removed in next version.",
|
"The following old template features have been deprecated and will be removed in next version.": "The following old template features have been deprecated and will be removed in next version.",
|
||||||
"Array": "Array",
|
"Array": "Array",
|
||||||
"Full permissions": "Full permissions"
|
"Full permissions": "Full permissions",
|
||||||
|
"Refresh data blocks": "Refresh data blocks",
|
||||||
|
"Select data blocks to refresh": "Select data blocks to refresh",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "After successful submission, the selected data blocks will be automatically refreshed."
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -803,7 +803,12 @@
|
|||||||
"If selected, the route will be displayed in the menu.": "Si se selecciona, la ruta se mostrará en el menú.",
|
"If selected, the route will be displayed in the menu.": "Si se selecciona, la ruta se mostrará en el menú.",
|
||||||
"Are you sure you want to hide this tab?": "¿Estás seguro de que quieres ocultar esta pestaña?",
|
"Are you sure you want to hide this tab?": "¿Estás seguro de que quieres ocultar esta pestaña?",
|
||||||
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Después de ocultar, esta pestaña ya no aparecerá en la barra de pestañas. Para mostrarla de nuevo, deberás ir a la página de gestión de rutas para configurarla.",
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Después de ocultar, esta pestaña ya no aparecerá en la barra de pestañas. Para mostrarla de nuevo, deberás ir a la página de gestión de rutas para configurarla.",
|
||||||
|
"No pages yet, please configure first": "Aún no hay páginas, por favor configura primero",
|
||||||
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Haga clic en el icono \"Editor de UI\" en la esquina superior derecha para entrar en el modo de Editor de UI.",
|
||||||
"Deprecated": "Obsoleto",
|
"Deprecated": "Obsoleto",
|
||||||
"The following old template features have been deprecated and will be removed in next version.": "Las siguientes características de plantilla antigua han quedado obsoletas y se eliminarán en la próxima versión.",
|
"The following old template features have been deprecated and will be removed in next version.": "Las siguientes características de plantilla antigua han quedado obsoletas y se eliminarán en la próxima versión.",
|
||||||
"Full permissions": "Todos los derechos"
|
"Full permissions": "Todos los derechos",
|
||||||
|
"Refresh data blocks": "Actualizar bloques de datos",
|
||||||
|
"Select data blocks to refresh": "Actualizar bloques de datos",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Después de enviar correctamente, los bloques de datos seleccionados se actualizarán automáticamente."
|
||||||
}
|
}
|
||||||
|
@ -823,7 +823,12 @@
|
|||||||
"If selected, the route will be displayed in the menu.": "Si sélectionné, la route sera affichée dans le menu.",
|
"If selected, the route will be displayed in the menu.": "Si sélectionné, la route sera affichée dans le menu.",
|
||||||
"Are you sure you want to hide this tab?": "Êtes-vous sûr de vouloir masquer cet onglet ?",
|
"Are you sure you want to hide this tab?": "Êtes-vous sûr de vouloir masquer cet onglet ?",
|
||||||
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Après avoir masqué, cette tab ne sera plus affichée dans la barre de tab. Pour la montrer à nouveau, vous devez vous rendre sur la page de gestion des routes pour la configurer.",
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Après avoir masqué, cette tab ne sera plus affichée dans la barre de tab. Pour la montrer à nouveau, vous devez vous rendre sur la page de gestion des routes pour la configurer.",
|
||||||
|
"No pages yet, please configure first": "Pas encore de pages, veuillez configurer d'abord",
|
||||||
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur",
|
||||||
"Deprecated": "Déprécié",
|
"Deprecated": "Déprécié",
|
||||||
"The following old template features have been deprecated and will be removed in next version.": "Les fonctionnalités des anciens modèles ont été dépréciées et seront supprimées dans la prochaine version.",
|
"The following old template features have been deprecated and will be removed in next version.": "Les fonctionnalités des anciens modèles ont été dépréciées et seront supprimées dans la prochaine version.",
|
||||||
"Full permissions": "Tous les droits"
|
"Full permissions": "Tous les droits",
|
||||||
|
"Refresh data blocks": "Actualiser les blocs de données",
|
||||||
|
"Select data blocks to refresh": "Actualiser les blocs de données",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Après une soumission réussie, les blocs de données sélectionnés seront automatiquement actualisés."
|
||||||
}
|
}
|
||||||
|
@ -1080,5 +1080,13 @@
|
|||||||
"If selected, the page will display Tab pages.": "Se selezionato, la pagina visualizzerà le pagine schede.",
|
"If selected, the page will display Tab pages.": "Se selezionato, la pagina visualizzerà le pagine schede.",
|
||||||
"If selected, the route will be displayed in the menu.": "Se selezionato, il percorso verrà visualizzato nel menu.",
|
"If selected, the route will be displayed in the menu.": "Se selezionato, il percorso verrà visualizzato nel menu.",
|
||||||
"Are you sure you want to hide this tab?": "Sei sicuro di voler nascondere questa scheda?",
|
"Are you sure you want to hide this tab?": "Sei sicuro di voler nascondere questa scheda?",
|
||||||
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Dopo averla nascosta, questa scheda non apparirà più nella barra delle schede. Per mostrarla di nuovo, devi andare alla pagina di gestione dei percorsi per configurarlo."
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Dopo averla nascosta, questa scheda non apparirà più nella barra delle schede. Per mostrarla di nuovo, devi andare alla pagina di gestione dei percorsi per configurarlo.",
|
||||||
|
"Refresh data blocks": "Aggiorna blocchi di dati",
|
||||||
|
"Select data blocks to refresh": "Aggiorna blocchi di dati",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Dopo una soumission réussie, les blocs de données sélectionnés seront automatiquement actualisés.",
|
||||||
|
"No pages yet, please configure first": "Nessuna pagina ancora, si prega di configurare prima",
|
||||||
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur",
|
||||||
|
"Refresh data blocks": "Aggiorna blocchi di dati",
|
||||||
|
"Select data blocks to refresh": "Aggiorna blocchi di dati",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Dopo una soumission réussie, les blocs de données sélectionnés seront automatiquement actualisés."
|
||||||
}
|
}
|
||||||
|
@ -1041,7 +1041,12 @@
|
|||||||
"If selected, the route will be displayed in the menu.": "選択されている場合、ルートはメニューに表示されます。",
|
"If selected, the route will be displayed in the menu.": "選択されている場合、ルートはメニューに表示されます。",
|
||||||
"Are you sure you want to hide this tab?": "このタブを非表示にしますか?",
|
"Are you sure you want to hide this tab?": "このタブを非表示にしますか?",
|
||||||
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "非表示にすると、このタブはタブバーに表示されなくなります。再表示するには、ルート管理ページで設定する必要があります。",
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "非表示にすると、このタブはタブバーに表示されなくなります。再表示するには、ルート管理ページで設定する必要があります。",
|
||||||
|
"No pages yet, please configure first": "まだページがありません。最初に設定してください",
|
||||||
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "ユーザーインターフェースエディターモードに入るには、右上隅の「UIエディタ」アイコンをクリックしてください",
|
||||||
"Deprecated": "非推奨",
|
"Deprecated": "非推奨",
|
||||||
"The following old template features have been deprecated and will be removed in next version.": "次の古いテンプレート機能は非推奨になり、次のバージョンで削除されます。",
|
"The following old template features have been deprecated and will be removed in next version.": "次の古いテンプレート機能は非推奨になり、次のバージョンで削除されます。",
|
||||||
"Full permissions": "すべての権限"
|
"Full permissions": "すべての権限",
|
||||||
|
"Refresh data blocks": "データブロックを更新",
|
||||||
|
"Select data blocks to refresh": "データブロックを選択して更新",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "送信後、選択したデータブロックが自動的に更新されます。"
|
||||||
}
|
}
|
||||||
|
@ -914,7 +914,12 @@
|
|||||||
"If selected, the route will be displayed in the menu.": "선택되면 라우트는 메뉴에 표시됩니다.",
|
"If selected, the route will be displayed in the menu.": "선택되면 라우트는 메뉴에 표시됩니다.",
|
||||||
"Are you sure you want to hide this tab?": "이 탭을 숨기시겠습니까?",
|
"Are you sure you want to hide this tab?": "이 탭을 숨기시겠습니까?",
|
||||||
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "숨기면 이 탭은 탭 바에 더 이상 표시되지 않습니다. 다시 표시하려면 라우트 관리 페이지에서 설정해야 합니다.",
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "숨기면 이 탭은 탭 바에 더 이상 표시되지 않습니다. 다시 표시하려면 라우트 관리 페이지에서 설정해야 합니다.",
|
||||||
|
"No pages yet, please configure first": "아직 페이지가 없습니다. 먼저 설정하십시오",
|
||||||
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "사용자 인터페이스 편집기 모드에 들어가려면 오른쪽 상단의 \"UI 편집기\" 아이콘을 클릭하십시오",
|
||||||
"Deprecated": "사용 중단됨",
|
"Deprecated": "사용 중단됨",
|
||||||
"The following old template features have been deprecated and will be removed in next version.": "다음 오래된 템플릿 기능은 사용 중단되었으며 다음 버전에서 제거될 것입니다.",
|
"The following old template features have been deprecated and will be removed in next version.": "다음 오래된 템플릿 기능은 사용 중단되었으며 다음 버전에서 제거될 것입니다.",
|
||||||
"Full permissions": "모든 권한"
|
"Full permissions": "모든 권한",
|
||||||
|
"Refresh data blocks": "데이터 블록 새로 고침",
|
||||||
|
"Select data blocks to refresh": "데이터 블록을 선택하여 새로 고침",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "전송 후, 선택한 데이터 블록이 자동으로 새로 고쳐집니다."
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Display <1><0>10</0><1>20</1><2>50</2><3>100</3></1> items per page": "Toon <1><0>10</0><1>20</1><2>50</2><3>100</3></1> items per pagina",
|
"Display <1><0>10</0><1>20</1><2>50</2><3>100</3></1> items per page": "Toon <1><0>10</0><1>20</1><2>50</2><3>100</3></1> items per pagina",
|
||||||
"Page number": "Paginanummer",
|
"Page number": "Paginanummer",
|
||||||
"Page size": "Paginagrootte",
|
"Page size": "Paginagrootte",
|
||||||
"Meet <1><0>All</0><1>Any</1></1> conditions in the group": "Voldoe aan <1><0>Alle</0><1>Een</1></1> voorwaarde(n) in de groep",
|
"Meet <1><0>All</0><1>Any</1></1> conditions in the group": "Voldoe aan <1><0>Alle</0><1>Een</1></1> voorwaarde(n) in de groep",
|
||||||
"Open in<1><0>Modal</0><1>Drawer</1><2>Window</2></1>": "Open in<1><0>Modal</0><1>Drawer</1><2>Venster</2></1>",
|
"Open in<1><0>Modal</0><1>Drawer</1><2>Window</2></1>": "Open in<1><0>Modal</0><1>Drawer</1><2>Venster</2></1>",
|
||||||
"{{count}} filter items": "{{count}} filter items",
|
"{{count}} filter items": "{{count}} filter items",
|
||||||
@ -1054,5 +1054,8 @@
|
|||||||
"Font Size(px)": "Lettergrootte(px)",
|
"Font Size(px)": "Lettergrootte(px)",
|
||||||
"Font Weight": "Letterdikte",
|
"Font Weight": "Letterdikte",
|
||||||
"Font Style": "Letterstijl",
|
"Font Style": "Letterstijl",
|
||||||
"Italic": "Cursief"
|
"Italic": "Cursief",
|
||||||
}
|
"Refresh data blocks": "Vernieuw gegevensblokken",
|
||||||
|
"Select data blocks to refresh": "Selecteer gegevensblokken om te vernieuwen",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Na succesvolle indiening worden de geselecteerde gegevensblokken automatisch vernieuwd."
|
||||||
|
}
|
||||||
|
@ -782,5 +782,13 @@
|
|||||||
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Depois de ocultar, esta guia não aparecerá mais na barra de guias. Para mostrá-la novamente, você precisa ir à página de gerenciamento de rotas para configurá-la.",
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Depois de ocultar, esta guia não aparecerá mais na barra de guias. Para mostrá-la novamente, você precisa ir à página de gerenciamento de rotas para configurá-la.",
|
||||||
"Deprecated": "Descontinuado",
|
"Deprecated": "Descontinuado",
|
||||||
"The following old template features have been deprecated and will be removed in next version.": "As seguintes funcionalidades de modelo antigo foram descontinuadas e serão removidas na próxima versão.",
|
"The following old template features have been deprecated and will be removed in next version.": "As seguintes funcionalidades de modelo antigo foram descontinuadas e serão removidas na próxima versão.",
|
||||||
"Full permissions": "Todas as permissões"
|
"Full permissions": "Todas as permissões",
|
||||||
|
"Refresh data blocks": "Atualizar blocos de dados",
|
||||||
|
"Select data blocks to refresh": "Selecionar blocos de dados para atualizar",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Após a atualização em massa bem sucedida.",
|
||||||
|
"No pages yet, please configure first": "Ainda não há páginas, por favor configure primeiro",
|
||||||
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur",
|
||||||
|
"Refresh data blocks": "Atualizar blocos de dados",
|
||||||
|
"Select data blocks to refresh": "Selecionar blocos de dados para atualizar",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Após a atualização em massa bem sucedida."
|
||||||
}
|
}
|
||||||
|
@ -611,5 +611,13 @@
|
|||||||
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "После скрытия этой вкладки она больше не будет отображаться во вкладке. Чтобы снова отобразить ее, вам нужно будет перейти на страницу управления маршрутами, чтобы установить ее.",
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "После скрытия этой вкладки она больше не будет отображаться во вкладке. Чтобы снова отобразить ее, вам нужно будет перейти на страницу управления маршрутами, чтобы установить ее.",
|
||||||
"Deprecated": "Устаревший",
|
"Deprecated": "Устаревший",
|
||||||
"The following old template features have been deprecated and will be removed in next version.": "Следующие старые функции шаблонов устарели и будут удалены в следующей версии.",
|
"The following old template features have been deprecated and will be removed in next version.": "Следующие старые функции шаблонов устарели и будут удалены в следующей версии.",
|
||||||
"Full permissions": "Полные права"
|
"Full permissions": "Полные права",
|
||||||
|
"Refresh data blocks": "Обновить блоки данных",
|
||||||
|
"Select data blocks to refresh": "Выберите блоки данных для обновления",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "После успешной отправки выбранные блоки данных будут автоматически обновлены.",
|
||||||
|
"No pages yet, please configure first": "Пока нет страниц, пожалуйста, настройте сначала",
|
||||||
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Нажмите на значок \"Редактор пользовательского интерфейса\" в правом верхнем углу, чтобы войти в режим редактора пользовательского интерфейса",
|
||||||
|
"Refresh data blocks": "Обновить блоки данных",
|
||||||
|
"Select data blocks to refresh": "Выберите блоки данных для обновления",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "После успешной отправки выбранные блоки данных будут автоматически обновлены."
|
||||||
}
|
}
|
||||||
|
@ -609,5 +609,13 @@
|
|||||||
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Gizlendikten sonra, bu sekme artık sekme çubuğunda görünmeyecek. Onu tekrar göstermek için, rotayı yönetim sayfasına gidip ayarlamanız gerekiyor.",
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Gizlendikten sonra, bu sekme artık sekme çubuğunda görünmeyecek. Onu tekrar göstermek için, rotayı yönetim sayfasına gidip ayarlamanız gerekiyor.",
|
||||||
"Deprecated": "Kullanımdan kaldırıldı",
|
"Deprecated": "Kullanımdan kaldırıldı",
|
||||||
"The following old template features have been deprecated and will be removed in next version.": "Aşağıdaki eski şablon özellikleri kullanımdan kaldırıldı ve gelecek sürümlerde kaldırılacaktır.",
|
"The following old template features have been deprecated and will be removed in next version.": "Aşağıdaki eski şablon özellikleri kullanımdan kaldırıldı ve gelecek sürümlerde kaldırılacaktır.",
|
||||||
"Full permissions": "Tüm izinler"
|
"Full permissions": "Tüm izinler",
|
||||||
|
"Refresh data blocks": "Yenile veri blokları",
|
||||||
|
"Select data blocks to refresh": "Veri bloklarını yenilemek için seçin",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Başarılı bir şekilde gönderildikten sonra, seçilen veri blokları otomatik olarak yenilenecektir.",
|
||||||
|
"No pages yet, please configure first": "Henüz sayfa yok, lütfen önce yapılandırın",
|
||||||
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Kullanıcı arayüzü düzenleyici moduna girmek için sağ üst köşedeki \"Kullanıcı Arayüzü Düzenleyici\" simgesine tıklayın",
|
||||||
|
"Refresh data blocks": "Yenile veri blokları",
|
||||||
|
"Select data blocks to refresh": "Veri bloklarını yenilemek için seçin",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Başarılı bir şekilde gönderildikten sonra, seçilen veri blokları otomatik olarak yenilenecektir."
|
||||||
}
|
}
|
||||||
|
@ -825,5 +825,13 @@
|
|||||||
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Після приховування цієї вкладки вона більше не з'явиться в панелі вкладок. Щоб знову показати її, вам потрібно перейти на сторінку керування маршрутами, щоб налаштувати її.",
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Після приховування цієї вкладки вона більше не з'явиться в панелі вкладок. Щоб знову показати її, вам потрібно перейти на сторінку керування маршрутами, щоб налаштувати її.",
|
||||||
"Deprecated": "Застаріло",
|
"Deprecated": "Застаріло",
|
||||||
"The following old template features have been deprecated and will be removed in next version.": "Наступні старі функції шаблонів були застарілі і будуть видалені в наступній версії.",
|
"The following old template features have been deprecated and will be removed in next version.": "Наступні старі функції шаблонів були застарілі і будуть видалені в наступній версії.",
|
||||||
"Full permissions": "Повні права"
|
"Full permissions": "Повні права",
|
||||||
}
|
"Refresh data blocks": "Оновити дані блоків",
|
||||||
|
"Select data blocks to refresh": "Виберіть блоки даних для оновлення",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Після успішної подачі вибрані блоки даних будуть автоматично оновлені.",
|
||||||
|
"No pages yet, please configure first": "Ще немає сторінок, будь ласка, спочатку налаштуйте",
|
||||||
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Натисніть на значок \"Редактор користувацького інтерфейсу\" в правому верхньому куті, щоб увійти в режим редактора користувацького інтерфейсу.",
|
||||||
|
"Refresh data blocks": "Оновити дані блоків",
|
||||||
|
"Select data blocks to refresh": "Виберіть блоки даних для оновлення",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "Після успішної подачі вибрані блоки даних будуть автоматично оновлені."
|
||||||
|
}
|
||||||
|
@ -167,6 +167,8 @@
|
|||||||
"Year": "年",
|
"Year": "年",
|
||||||
"QuarterYear": "季度",
|
"QuarterYear": "季度",
|
||||||
"Select grouping field": "选择分组字段",
|
"Select grouping field": "选择分组字段",
|
||||||
|
"Refresh data blocks": "刷新数据区块",
|
||||||
|
"Select data blocks to refresh": "选择要刷新的数据区块",
|
||||||
"Media": "多媒体",
|
"Media": "多媒体",
|
||||||
"Markdown": "Markdown",
|
"Markdown": "Markdown",
|
||||||
"Wysiwyg": "富文本",
|
"Wysiwyg": "富文本",
|
||||||
@ -259,6 +261,7 @@
|
|||||||
"Parent collection fields": "父表字段",
|
"Parent collection fields": "父表字段",
|
||||||
"Basic": "基本类型",
|
"Basic": "基本类型",
|
||||||
"Single line text": "单行文本",
|
"Single line text": "单行文本",
|
||||||
|
"Automatically remove heading and tailing spaces": "自动去除首尾空白字符",
|
||||||
"Long text": "多行文本",
|
"Long text": "多行文本",
|
||||||
"Phone": "手机号码",
|
"Phone": "手机号码",
|
||||||
"Email": "电子邮箱",
|
"Email": "电子邮箱",
|
||||||
@ -818,6 +821,7 @@
|
|||||||
"File size should not exceed {{size}}.": "文件大小不能超过 {{size}}",
|
"File size should not exceed {{size}}.": "文件大小不能超过 {{size}}",
|
||||||
"File size exceeds the limit": "文件大小超过限制",
|
"File size exceeds the limit": "文件大小超过限制",
|
||||||
"File type is not allowed": "文件类型不允许",
|
"File type is not allowed": "文件类型不允许",
|
||||||
|
"Uploading": "上传中",
|
||||||
"Incomplete uploading files need to be resolved": "未完成上传的文件需要处理",
|
"Incomplete uploading files need to be resolved": "未完成上传的文件需要处理",
|
||||||
"Default title for each record": "用作数据的默认标题",
|
"Default title for each record": "用作数据的默认标题",
|
||||||
"If collection inherits, choose inherited collections as templates": "当前表有继承关系时,可选择继承链路上的表作为模板来源",
|
"If collection inherits, choose inherited collections as templates": "当前表有继承关系时,可选择继承链路上的表作为模板来源",
|
||||||
@ -1098,5 +1102,10 @@
|
|||||||
"Font Weight": "字体粗细",
|
"Font Weight": "字体粗细",
|
||||||
"Font Style": "字体样式",
|
"Font Style": "字体样式",
|
||||||
"Italic": "斜体",
|
"Italic": "斜体",
|
||||||
"Response record":"响应结果记录"
|
"Response record":"响应结果记录",
|
||||||
|
"Colon":"冒号",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "提交成功后,会自动刷新这里选中的数据区块。",
|
||||||
|
"No pages yet, please configure first": "暂无页面,请先配置",
|
||||||
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "点击右上角的“界面配置”图标,进入界面配置模式",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "提交成功后,会自动刷新这里选中的数据区块。"
|
||||||
}
|
}
|
||||||
|
@ -916,5 +916,13 @@
|
|||||||
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "隱藏後,這個標籤將不再出現在標籤欄中。要再次顯示它,你需要到路由管理頁面進行設置。",
|
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "隱藏後,這個標籤將不再出現在標籤欄中。要再次顯示它,你需要到路由管理頁面進行設置。",
|
||||||
"Deprecated": "已棄用",
|
"Deprecated": "已棄用",
|
||||||
"The following old template features have been deprecated and will be removed in next version.": "以下舊的模板功能已棄用,將在下個版本移除。",
|
"The following old template features have been deprecated and will be removed in next version.": "以下舊的模板功能已棄用,將在下個版本移除。",
|
||||||
"Full permissions": "完全權限"
|
"Full permissions": "完全權限",
|
||||||
}
|
"Refresh data blocks": "刷新數據區塊",
|
||||||
|
"Select data blocks to refresh": "選擇要刷新的數據區塊",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "提交成功後,選中的數據區塊將自動刷新。",
|
||||||
|
"No pages yet, please configure first": "尚未配置頁面,請先配置",
|
||||||
|
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "點擊右上角的 \"介面設定\" 圖示進入介面設定模式",
|
||||||
|
"Refresh data blocks": "刷新數據區塊",
|
||||||
|
"Select data blocks to refresh": "選擇要刷新的數據區塊",
|
||||||
|
"After successful submission, the selected data blocks will be automatically refreshed.": "提交成功後,選中的數據區塊將自動刷新。"
|
||||||
|
}
|
||||||
|
@ -15,6 +15,8 @@ import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/actio
|
|||||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||||
import { SchemaSettingsEnableChildCollections } from '../../../schema-settings/SchemaSettings';
|
import { SchemaSettingsEnableChildCollections } from '../../../schema-settings/SchemaSettings';
|
||||||
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||||
|
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||||
|
import { useDataBlockProps } from '../../../data-source';
|
||||||
|
|
||||||
export const addNewActionSettings = new SchemaSettings({
|
export const addNewActionSettings = new SchemaSettings({
|
||||||
name: 'actionSettings:addNew',
|
name: 'actionSettings:addNew',
|
||||||
@ -27,6 +29,16 @@ export const addNewActionSettings = new SchemaSettings({
|
|||||||
return buttonEditorProps;
|
return buttonEditorProps;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'linkageRules',
|
||||||
|
Component: SchemaSettingsLinkageRules,
|
||||||
|
useComponentProps() {
|
||||||
|
const { linkageRulesProps } = useSchemaToolbar();
|
||||||
|
return {
|
||||||
|
...linkageRulesProps,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'openMode',
|
name: 'openMode',
|
||||||
Component: SchemaSettingOpenModeSchemaItems,
|
Component: SchemaSettingOpenModeSchemaItems,
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
SecondConFirm,
|
SecondConFirm,
|
||||||
RefreshDataBlockRequest,
|
RefreshDataBlockRequest,
|
||||||
} from '../../../schema-component/antd/action/Action.Designer';
|
} from '../../../schema-component/antd/action/Action.Designer';
|
||||||
|
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||||
|
|
||||||
export const bulkDeleteActionSettings = new SchemaSettings({
|
export const bulkDeleteActionSettings = new SchemaSettings({
|
||||||
name: 'actionSettings:bulkDelete',
|
name: 'actionSettings:bulkDelete',
|
||||||
@ -27,6 +28,16 @@ export const bulkDeleteActionSettings = new SchemaSettings({
|
|||||||
return buttonEditorProps;
|
return buttonEditorProps;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'linkageRules',
|
||||||
|
Component: SchemaSettingsLinkageRules,
|
||||||
|
useComponentProps() {
|
||||||
|
const { linkageRulesProps } = useSchemaToolbar();
|
||||||
|
return {
|
||||||
|
...linkageRulesProps,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'secondConFirm',
|
name: 'secondConFirm',
|
||||||
Component: SecondConFirm,
|
Component: SecondConFirm,
|
||||||
|
@ -32,11 +32,9 @@ export const disassociateActionSettings = new SchemaSettings({
|
|||||||
name: 'linkageRules',
|
name: 'linkageRules',
|
||||||
Component: SchemaSettingsLinkageRules,
|
Component: SchemaSettingsLinkageRules,
|
||||||
useComponentProps() {
|
useComponentProps() {
|
||||||
const { name } = useCollection_deprecated();
|
|
||||||
const { linkageRulesProps } = useSchemaToolbar();
|
const { linkageRulesProps } = useSchemaToolbar();
|
||||||
return {
|
return {
|
||||||
...linkageRulesProps,
|
...linkageRulesProps,
|
||||||
collectionName: name,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -14,7 +14,7 @@ import { useDesignable } from '../../..';
|
|||||||
import { useSchemaToolbar } from '../../../application';
|
import { useSchemaToolbar } from '../../../application';
|
||||||
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||||
import { RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
import { RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
||||||
import { SchemaSettingsModalItem } from '../../../schema-settings';
|
import { SchemaSettingsModalItem, SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||||
|
|
||||||
function ButtonEditor() {
|
function ButtonEditor() {
|
||||||
const field = useField();
|
const field = useField();
|
||||||
@ -110,6 +110,17 @@ export const expendableActionSettings = new SchemaSettings({
|
|||||||
return buttonEditorProps;
|
return buttonEditorProps;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'linkageRules',
|
||||||
|
Component: SchemaSettingsLinkageRules,
|
||||||
|
useComponentProps() {
|
||||||
|
const { linkageRulesProps } = useSchemaToolbar();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...linkageRulesProps,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'remove',
|
name: 'remove',
|
||||||
sort: 100,
|
sort: 100,
|
||||||
|
@ -11,10 +11,9 @@ import { useField, useFieldSchema } from '@formily/react';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useCollectionRecord, useDesignable } from '../../../';
|
import { useDesignable } from '../../../';
|
||||||
import { useSchemaToolbar } from '../../../application';
|
import { useSchemaToolbar } from '../../../application';
|
||||||
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||||
import { useCollection_deprecated } from '../../../collection-manager';
|
|
||||||
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
||||||
import {
|
import {
|
||||||
SchemaSettingsLinkageRules,
|
SchemaSettingsLinkageRules,
|
||||||
@ -22,6 +21,7 @@ import {
|
|||||||
SchemaSettingAccessControl,
|
SchemaSettingAccessControl,
|
||||||
} from '../../../schema-settings';
|
} from '../../../schema-settings';
|
||||||
import { useURLAndHTMLSchema } from './useURLAndHTMLSchema';
|
import { useURLAndHTMLSchema } from './useURLAndHTMLSchema';
|
||||||
|
import { useDataBlockProps } from '../../../data-source';
|
||||||
|
|
||||||
export const SchemaSettingsActionLinkItem: FC = () => {
|
export const SchemaSettingsActionLinkItem: FC = () => {
|
||||||
const field = useField();
|
const field = useField();
|
||||||
@ -94,16 +94,10 @@ export const customizeLinkActionSettings = new SchemaSettings({
|
|||||||
{
|
{
|
||||||
name: 'linkageRules',
|
name: 'linkageRules',
|
||||||
Component: SchemaSettingsLinkageRules,
|
Component: SchemaSettingsLinkageRules,
|
||||||
useVisible() {
|
|
||||||
const record = useCollectionRecord();
|
|
||||||
return !_.isEmpty(record?.data);
|
|
||||||
},
|
|
||||||
useComponentProps() {
|
useComponentProps() {
|
||||||
const { name } = useCollection_deprecated();
|
|
||||||
const { linkageRulesProps } = useSchemaToolbar();
|
const { linkageRulesProps } = useSchemaToolbar();
|
||||||
return {
|
return {
|
||||||
...linkageRulesProps,
|
...linkageRulesProps,
|
||||||
collectionName: name,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
import { useSchemaToolbar } from '../../../application';
|
import { useSchemaToolbar } from '../../../application';
|
||||||
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||||
import { ButtonEditor, RemoveButton, SecondConFirm } from '../../../schema-component/antd/action/Action.Designer';
|
import { ButtonEditor, RemoveButton, SecondConFirm } from '../../../schema-component/antd/action/Action.Designer';
|
||||||
|
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||||
export const refreshActionSettings = new SchemaSettings({
|
export const refreshActionSettings = new SchemaSettings({
|
||||||
name: 'actionSettings:refresh',
|
name: 'actionSettings:refresh',
|
||||||
items: [
|
items: [
|
||||||
@ -22,6 +22,17 @@ export const refreshActionSettings = new SchemaSettings({
|
|||||||
return buttonEditorProps;
|
return buttonEditorProps;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'linkageRules',
|
||||||
|
Component: SchemaSettingsLinkageRules,
|
||||||
|
useComponentProps() {
|
||||||
|
const { linkageRulesProps } = useSchemaToolbar();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...linkageRulesProps,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'secondConFirm',
|
name: 'secondConFirm',
|
||||||
Component: SecondConFirm,
|
Component: SecondConFirm,
|
||||||
|
@ -29,6 +29,7 @@ import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks
|
|||||||
import { SchemaSettingsModalItem } from '../../../schema-settings/SchemaSettings';
|
import { SchemaSettingsModalItem } from '../../../schema-settings/SchemaSettings';
|
||||||
import { useParentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider';
|
import { useParentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider';
|
||||||
import { useDataBlockProps } from '../../../data-source';
|
import { useDataBlockProps } from '../../../data-source';
|
||||||
|
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||||
|
|
||||||
const Tree = connect(
|
const Tree = connect(
|
||||||
AntdTree,
|
AntdTree,
|
||||||
@ -149,6 +150,16 @@ export const createSubmitActionSettings = new SchemaSettings({
|
|||||||
return buttonEditorProps;
|
return buttonEditorProps;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'linkageRules',
|
||||||
|
Component: SchemaSettingsLinkageRules,
|
||||||
|
useComponentProps() {
|
||||||
|
const { linkageRulesProps } = useSchemaToolbar();
|
||||||
|
return {
|
||||||
|
...linkageRulesProps,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'secondConfirmation',
|
name: 'secondConfirmation',
|
||||||
Component: SecondConFirm,
|
Component: SecondConFirm,
|
||||||
|
@ -46,10 +46,6 @@ export const updateSubmitActionSettings = new SchemaSettings({
|
|||||||
collectionName: name,
|
collectionName: name,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
useVisible() {
|
|
||||||
const fieldSchema = useFieldSchema();
|
|
||||||
return !fieldSchema.parent['x-initializer'].includes('bulkEditForm');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'secondConfirmation',
|
name: 'secondConfirmation',
|
||||||
|
@ -6,17 +6,12 @@
|
|||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useFieldSchema } from '@formily/react';
|
|
||||||
import { useSchemaToolbar } from '../../../application';
|
import { useSchemaToolbar } from '../../../application';
|
||||||
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
|
||||||
import { useCollection_deprecated } from '../../../collection-manager';
|
|
||||||
import { useCollection } from '../../../data-source';
|
|
||||||
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
|
||||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||||
import { SchemaSettingsLinkageRules, SchemaSettingAccessControl } from '../../../schema-settings';
|
import { SchemaSettingsLinkageRules, SchemaSettingAccessControl } from '../../../schema-settings';
|
||||||
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
import { useOpenModeContext } from '../../popup/OpenModeProvider';
|
||||||
import { useCurrentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider';
|
|
||||||
|
|
||||||
export const customizePopupActionSettings = new SchemaSettings({
|
export const customizePopupActionSettings = new SchemaSettings({
|
||||||
name: 'actionSettings:popup',
|
name: 'actionSettings:popup',
|
||||||
@ -33,18 +28,11 @@ export const customizePopupActionSettings = new SchemaSettings({
|
|||||||
name: 'linkageRules',
|
name: 'linkageRules',
|
||||||
Component: SchemaSettingsLinkageRules,
|
Component: SchemaSettingsLinkageRules,
|
||||||
useComponentProps() {
|
useComponentProps() {
|
||||||
const { name } = useCollection_deprecated();
|
|
||||||
const { linkageRulesProps } = useSchemaToolbar();
|
const { linkageRulesProps } = useSchemaToolbar();
|
||||||
return {
|
return {
|
||||||
...linkageRulesProps,
|
...linkageRulesProps,
|
||||||
collectionName: name,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
useVisible() {
|
|
||||||
const { collection } = useCurrentPopupRecord() || {};
|
|
||||||
const currentCollection = useCollection();
|
|
||||||
return !collection || collection?.name === currentCollection?.name;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'openMode',
|
name: 'openMode',
|
||||||
|
@ -35,11 +35,16 @@ test.describe('linkage rules', () => {
|
|||||||
// 条件:singleLineText 字段的值包含 123 时
|
// 条件:singleLineText 字段的值包含 123 时
|
||||||
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
|
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
|
||||||
await page.getByText('Add condition', { exact: true }).click();
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
await page.getByTestId('select-filter-field').click();
|
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).click();
|
|
||||||
await page.getByLabel('Linkage rules').locator('input[type="text"]').click();
|
|
||||||
await page.getByLabel('Linkage rules').locator('input[type="text"]').fill('123');
|
|
||||||
|
|
||||||
|
await page.getByLabel('variable-button').first().click();
|
||||||
|
await page.getByText('Current form').last().click();
|
||||||
|
await page.getByText('Current form').last().click();
|
||||||
|
await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).locator('div').click();
|
||||||
|
|
||||||
|
// await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).click();
|
||||||
|
await page.getByTestId('right-filter-field').getByRole('textbox').click();
|
||||||
|
await page.getByTestId('right-filter-field').getByRole('textbox').fill('123');
|
||||||
|
await page.getByRole('tabpanel').getByRole('textbox').last().fill('123');
|
||||||
// action:禁用 longText 字段
|
// action:禁用 longText 字段
|
||||||
await page.getByText('Add property').click();
|
await page.getByText('Add property').click();
|
||||||
await page.getByTestId('select-linkage-property-field').click();
|
await page.getByTestId('select-linkage-property-field').click();
|
||||||
@ -81,7 +86,7 @@ test.describe('linkage rules', () => {
|
|||||||
// 修改第一组规则,使其条件中包含一个变量 --------------------------------------------------------------------------
|
// 修改第一组规则,使其条件中包含一个变量 --------------------------------------------------------------------------
|
||||||
// 当 singleLineText 字段的值包含 longText 字段的值时,禁用 longText 字段
|
// 当 singleLineText 字段的值包含 longText 字段的值时,禁用 longText 字段
|
||||||
await openLinkageRules();
|
await openLinkageRules();
|
||||||
await page.getByLabel('variable-button').click();
|
await page.getByLabel('variable-button').last().click();
|
||||||
await expectSupportedVariables(page, [
|
await expectSupportedVariables(page, [
|
||||||
'Constant',
|
'Constant',
|
||||||
'Current user',
|
'Current user',
|
||||||
@ -136,8 +141,13 @@ test.describe('linkage rules', () => {
|
|||||||
.getByText('Add condition', { exact: true })
|
.getByText('Add condition', { exact: true })
|
||||||
.last()
|
.last()
|
||||||
.click();
|
.click();
|
||||||
await page.getByRole('button', { name: 'Select field' }).click();
|
// await page.getByRole('button', { name: 'Select field' }).click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'number' }).click();
|
|
||||||
|
await page.getByTestId('left-filter-field').getByLabel('variable-button').last().click();
|
||||||
|
await page.getByText('Current form').last().click();
|
||||||
|
await page.getByText('Current form').last().click();
|
||||||
|
await page.getByRole('menuitemcheckbox', { name: 'number' }).locator('div').click();
|
||||||
|
|
||||||
await page.getByLabel('Linkage rules').getByRole('spinbutton').click();
|
await page.getByLabel('Linkage rules').getByRole('spinbutton').click();
|
||||||
await page.getByLabel('Linkage rules').getByRole('spinbutton').fill('123');
|
await page.getByLabel('Linkage rules').getByRole('spinbutton').fill('123');
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ test.describe('deprecated variables', () => {
|
|||||||
await expect(page.getByLabel('variable-tag').getByText('Current record / Nickname')).toBeVisible();
|
await expect(page.getByLabel('variable-tag').getByText('Current record / Nickname')).toBeVisible();
|
||||||
|
|
||||||
// 2. 但是变量列表中是禁用状态
|
// 2. 但是变量列表中是禁用状态
|
||||||
await page.locator('button').filter({ hasText: /^x$/ }).click();
|
await page.locator('button').filter({ hasText: /^x$/ }).last().click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).hover({ position: { x: 40, y: 12 } });
|
await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).hover({ position: { x: 40, y: 12 } });
|
||||||
await expect(page.getByRole('tooltip', { name: 'This variable has been deprecated' })).toBeVisible();
|
await expect(page.getByRole('tooltip', { name: 'This variable has been deprecated' })).toBeVisible();
|
||||||
await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toHaveClass(
|
await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toHaveClass(
|
||||||
@ -45,11 +45,11 @@ test.describe('deprecated variables', () => {
|
|||||||
await page.getByLabel('Linkage rules').getByText('Linkage rules').click();
|
await page.getByLabel('Linkage rules').getByText('Linkage rules').click();
|
||||||
|
|
||||||
// 3. 当设置为其它变量后,再次打开,变量列表中的弃用变量不再显示
|
// 3. 当设置为其它变量后,再次打开,变量列表中的弃用变量不再显示
|
||||||
await page.locator('button').filter({ hasText: /^x$/ }).click();
|
await page.locator('button').filter({ hasText: /^x$/ }).last().click();
|
||||||
await expect(page.getByRole('menuitemcheckbox', { name: 'Current form right' })).toHaveCount(1);
|
await expect(page.getByRole('menuitemcheckbox', { name: 'Current form right' })).toHaveCount(1);
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click();
|
await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
||||||
await expect(page.getByLabel('variable-tag').getByText('Current form / Nickname')).toBeVisible();
|
await expect(page.getByLabel('variable-tag').getByText('Current form / Nickname').last()).toBeVisible();
|
||||||
// 清空表达式
|
// 清空表达式
|
||||||
await page.getByLabel('textbox').clear();
|
await page.getByLabel('textbox').clear();
|
||||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||||
@ -58,7 +58,7 @@ test.describe('deprecated variables', () => {
|
|||||||
await page.getByLabel('block-item-CardItem-users-form').hover();
|
await page.getByLabel('block-item-CardItem-users-form').hover();
|
||||||
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:editForm-users').hover();
|
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:editForm-users').hover();
|
||||||
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
|
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
|
||||||
await page.locator('button').filter({ hasText: /^x$/ }).click();
|
await page.locator('button').filter({ hasText: /^x$/ }).last().click();
|
||||||
await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toBeHidden();
|
await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toBeHidden();
|
||||||
// 使下拉菜单消失
|
// 使下拉菜单消失
|
||||||
await page.getByLabel('Linkage rules').getByText('Linkage rules').click();
|
await page.getByLabel('Linkage rules').getByText('Linkage rules').click();
|
||||||
|
@ -85,7 +85,7 @@ test.describe('configure fields', () => {
|
|||||||
await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover();
|
await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover();
|
||||||
await page.getByRole('menuitem', { name: 'manyToOne3' }).click();
|
await page.getByRole('menuitem', { name: 'manyToOne3' }).click();
|
||||||
await page.mouse.move(600, 0);
|
await page.mouse.move(600, 0);
|
||||||
|
await page.reload();
|
||||||
await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne1-manyToOne1')).toHaveText(
|
await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne1-manyToOne1')).toHaveText(
|
||||||
`manyToOne1:${record.manyToOne1.id}`,
|
`manyToOne1:${record.manyToOne1.id}`,
|
||||||
);
|
);
|
||||||
|
@ -42,6 +42,7 @@ test.describe('where grid card block can be added', () => {
|
|||||||
await page.getByLabel('schema-initializer-Grid-').nth(1).hover();
|
await page.getByLabel('schema-initializer-Grid-').nth(1).hover();
|
||||||
await page.getByRole('menuitem', { name: 'Role name' }).click();
|
await page.getByRole('menuitem', { name: 'Role name' }).click();
|
||||||
await page.mouse.move(300, 0);
|
await page.mouse.move(300, 0);
|
||||||
|
await page.reload();
|
||||||
await expect(page.getByText('Root')).toBeVisible();
|
await expect(page.getByText('Root')).toBeVisible();
|
||||||
await expect(page.getByText('Admin')).toBeVisible();
|
await expect(page.getByText('Admin')).toBeVisible();
|
||||||
await expect(page.getByText('Member')).toBeVisible();
|
await expect(page.getByText('Member')).toBeVisible();
|
||||||
|
@ -41,6 +41,7 @@ test.describe('where list block can be added', () => {
|
|||||||
await page.getByLabel('schema-initializer-Grid-').nth(1).hover();
|
await page.getByLabel('schema-initializer-Grid-').nth(1).hover();
|
||||||
await page.getByRole('menuitem', { name: 'Role name' }).click();
|
await page.getByRole('menuitem', { name: 'Role name' }).click();
|
||||||
await page.mouse.move(300, 0);
|
await page.mouse.move(300, 0);
|
||||||
|
await page.reload();
|
||||||
await expect(page.getByLabel('block-item-CollectionField-').getByText('Root')).toBeVisible();
|
await expect(page.getByLabel('block-item-CollectionField-').getByText('Root')).toBeVisible();
|
||||||
await expect(page.getByLabel('block-item-CollectionField-').getByText('Admin')).toBeVisible();
|
await expect(page.getByLabel('block-item-CollectionField-').getByText('Admin')).toBeVisible();
|
||||||
await expect(page.getByLabel('block-item-CollectionField-').getByText('Member')).toBeVisible();
|
await expect(page.getByLabel('block-item-CollectionField-').getByText('Member')).toBeVisible();
|
||||||
|
@ -13,14 +13,16 @@ import { T4334 } from '../templatesOfBug';
|
|||||||
// fix https://nocobase.height.app/T-2187
|
// fix https://nocobase.height.app/T-2187
|
||||||
test('action linkage by row data', async ({ page, mockPage }) => {
|
test('action linkage by row data', async ({ page, mockPage }) => {
|
||||||
await mockPage(T4334).goto();
|
await mockPage(T4334).goto();
|
||||||
const adminEditAction = page.getByLabel('action-Action.Link-Edit-update-roles-table-admin');
|
const adminEditAction = page
|
||||||
|
.getByLabel('action-Action.Link-Edit-update-roles-table-admin')
|
||||||
|
.locator('.nb-action-title');
|
||||||
const adminEditActionStyle = await adminEditAction.evaluate((element) => {
|
const adminEditActionStyle = await adminEditAction.evaluate((element) => {
|
||||||
const computedStyle = window.getComputedStyle(element);
|
const computedStyle = window.getComputedStyle(element);
|
||||||
return {
|
return {
|
||||||
opacity: computedStyle.opacity,
|
opacity: computedStyle.opacity,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const rootEditAction = page.getByLabel('action-Action.Link-Edit-update-roles-table-root');
|
const rootEditAction = page.getByLabel('action-Action.Link-Edit-update-roles-table-root').locator('.nb-action-title');
|
||||||
const rootEditActionStyle = await rootEditAction.evaluate((element) => {
|
const rootEditActionStyle = await rootEditAction.evaluate((element) => {
|
||||||
const computedStyle = window.getComputedStyle(element);
|
const computedStyle = window.getComputedStyle(element);
|
||||||
return {
|
return {
|
||||||
@ -28,7 +30,6 @@ test('action linkage by row data', async ({ page, mockPage }) => {
|
|||||||
// 添加其他你需要的样式属性
|
// 添加其他你需要的样式属性
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(adminEditActionStyle.opacity).not.toBe('0.1');
|
expect(adminEditActionStyle.opacity).not.toBe('0.1');
|
||||||
expect(rootEditActionStyle.opacity).not.toBe('1');
|
expect(rootEditActionStyle.opacity).not.toBe('1');
|
||||||
});
|
});
|
||||||
|
@ -156,6 +156,7 @@ test.describe('configure columns', () => {
|
|||||||
await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover();
|
await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover();
|
||||||
await page.getByRole('menuitem', { name: 'manyToOne3' }).click();
|
await page.getByRole('menuitem', { name: 'manyToOne3' }).click();
|
||||||
await page.mouse.move(600, 0);
|
await page.mouse.move(600, 0);
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
// 2. Click on the association field, create a details block in the popup, display the ID field, and assert if it's correct
|
// 2. Click on the association field, create a details block in the popup, display the ID field, and assert if it's correct
|
||||||
await page
|
await page
|
||||||
@ -194,6 +195,7 @@ test.describe('configure columns', () => {
|
|||||||
await page.getByLabel('schema-initializer-Grid-details:configureFields-emptyCollection').hover();
|
await page.getByLabel('schema-initializer-Grid-details:configureFields-emptyCollection').hover();
|
||||||
await page.getByRole('menuitem', { name: 'ID', exact: true }).click();
|
await page.getByRole('menuitem', { name: 'ID', exact: true }).click();
|
||||||
await page.mouse.move(600, 0);
|
await page.mouse.move(600, 0);
|
||||||
|
|
||||||
await expect(page.getByLabel('block-item-CollectionField-')).toHaveText(
|
await expect(page.getByLabel('block-item-CollectionField-')).toHaveText(
|
||||||
`ID:${record.manyToOne1.manyToOne2.manyToOne3.id}`,
|
`ID:${record.manyToOne1.manyToOne2.manyToOne3.id}`,
|
||||||
);
|
);
|
||||||
@ -212,6 +214,7 @@ test.describe('configure columns', () => {
|
|||||||
await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover();
|
await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover();
|
||||||
await page.getByRole('menuitem', { name: 'manyToOne3' }).click();
|
await page.getByRole('menuitem', { name: 'manyToOne3' }).click();
|
||||||
await page.mouse.move(600, 0);
|
await page.mouse.move(600, 0);
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
// 2. 点击每一个关系字段,创建一个详情区块,显示 ID 字段,断言 ID 是否正确
|
// 2. 点击每一个关系字段,创建一个详情区块,显示 ID 字段,断言 ID 是否正确
|
||||||
await page
|
await page
|
||||||
@ -306,9 +309,10 @@ test.describe('configure actions column', () => {
|
|||||||
await page.getByText('Actions', { exact: true }).hover({ force: true });
|
await page.getByText('Actions', { exact: true }).hover({ force: true });
|
||||||
await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
|
await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
await page.mouse.move(500, 0);
|
||||||
|
|
||||||
await page.getByText('Actions', { exact: true }).hover({ force: true });
|
// await page.getByText('Actions', { exact: true }).hover({ force: true });
|
||||||
await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
|
// await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
|
||||||
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
|
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
|
||||||
|
|
||||||
await page.mouse.move(300, 0);
|
await page.mouse.move(300, 0);
|
||||||
|
@ -316,7 +316,8 @@ test.describe('actions schema settings', () => {
|
|||||||
|
|
||||||
// 添加一个条件:ID 等于 1
|
// 添加一个条件:ID 等于 1
|
||||||
await page.getByText('Add condition', { exact: true }).click();
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
await page.getByTestId('select-filter-field').click();
|
await page.getByTestId('left-filter-field').getByLabel('variable-button').click();
|
||||||
|
await page.getByText('Current record').last().click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||||
await page.getByRole('spinbutton').click();
|
await page.getByRole('spinbutton').click();
|
||||||
await page.getByRole('spinbutton').fill('1');
|
await page.getByRole('spinbutton').fill('1');
|
||||||
@ -340,7 +341,8 @@ test.describe('actions schema settings', () => {
|
|||||||
|
|
||||||
// 添加一个条件:ID 等于 1
|
// 添加一个条件:ID 等于 1
|
||||||
await page.getByRole('tabpanel').getByText('Add condition', { exact: true }).last().click();
|
await page.getByRole('tabpanel').getByText('Add condition', { exact: true }).last().click();
|
||||||
await page.getByRole('button', { name: 'Select field' }).click();
|
await page.getByTestId('left-filter-field').getByLabel('variable-button').last().click();
|
||||||
|
await page.getByText('Current record').last().click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||||
await page.getByRole('spinbutton').click();
|
await page.getByRole('spinbutton').click();
|
||||||
await page.getByRole('spinbutton').fill('1');
|
await page.getByRole('spinbutton').fill('1');
|
||||||
@ -902,7 +904,6 @@ test.describe('actions schema settings', () => {
|
|||||||
await page.getByRole('menuitem', { name: 'Submit' }).click();
|
await page.getByRole('menuitem', { name: 'Submit' }).click();
|
||||||
await page.mouse.move(300, 0);
|
await page.mouse.move(300, 0);
|
||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
await page.getByLabel('designer-schema-settings-CardItem-TableBlockDesigner-treeCollection').hover();
|
await page.getByLabel('designer-schema-settings-CardItem-TableBlockDesigner-treeCollection').hover();
|
||||||
await page.getByRole('menuitem', { name: 'Tree table' }).click();
|
await page.getByRole('menuitem', { name: 'Tree table' }).click();
|
||||||
|
|
||||||
@ -928,6 +929,7 @@ test.describe('actions schema settings', () => {
|
|||||||
await page.getByLabel('schema-initializer-Grid-form:').hover();
|
await page.getByLabel('schema-initializer-Grid-form:').hover();
|
||||||
await page.getByRole('menuitem', { name: 'Parent', exact: true }).click();
|
await page.getByRole('menuitem', { name: 'Parent', exact: true }).click();
|
||||||
await page.mouse.move(300, 0);
|
await page.mouse.move(300, 0);
|
||||||
|
await page.reload();
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
.getByLabel('block-item-CollectionField-')
|
.getByLabel('block-item-CollectionField-')
|
||||||
|
@ -37,7 +37,7 @@ const enabledIndexColumn: SchemaSettingsItemType = {
|
|||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
return {
|
return {
|
||||||
title: t('Enable index column'),
|
title: t('Enable index column'),
|
||||||
checked: field.decoratorProps.enableSelectColumn !== false,
|
checked: field.decoratorProps.enableIndexÏColumn !== false,
|
||||||
onChange: async (enableIndexÏColumn) => {
|
onChange: async (enableIndexÏColumn) => {
|
||||||
field.decoratorProps = field.decoratorProps || {};
|
field.decoratorProps = field.decoratorProps || {};
|
||||||
field.decoratorProps.enableIndexÏColumn = enableIndexÏColumn;
|
field.decoratorProps.enableIndexÏColumn = enableIndexÏColumn;
|
||||||
|
@ -94,9 +94,13 @@ export const FilterCollectionFieldInternalField: React.FC = (props: Props) => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
field.dataSource = uiSchema.enum;
|
field.dataSource = uiSchema.enum;
|
||||||
const originalProps =
|
const originalProps =
|
||||||
compile({ ...(operator?.schema?.['x-component-props'] || {}), ...(uiSchema['x-component-props'] || {}) }) || {};
|
compile({
|
||||||
|
...(operator?.schema?.['x-component-props'] || {}),
|
||||||
|
...(uiSchema['x-component-props'] || {}),
|
||||||
|
...(fieldSchema?.['x-component-props'] || {}),
|
||||||
|
}) || {};
|
||||||
|
|
||||||
field.componentProps = merge(originalProps, field.componentProps || {}, dynamicProps || {});
|
field.componentProps = merge(field.componentProps || {}, originalProps, dynamicProps || {});
|
||||||
}, [uiSchemaOrigin]);
|
}, [uiSchemaOrigin]);
|
||||||
|
|
||||||
if (!uiSchemaOrigin) return null;
|
if (!uiSchemaOrigin) return null;
|
||||||
|
@ -27,7 +27,8 @@ test.describe('options of Select field in linkage rule', () => {
|
|||||||
await page.getByRole('switch', { name: 'On Off' }).click();
|
await page.getByRole('switch', { name: 'On Off' }).click();
|
||||||
await page.getByRole('button', { name: 'OK' }).click();
|
await page.getByRole('button', { name: 'OK' }).click();
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(page.getByRole('option', { name: 'option2' })).toBeVisible();
|
await page.getByLabel('block-item-CollectionField-').click();
|
||||||
|
await expect(page.getByRole('option', { name: 'option2' }).last()).toBeVisible();
|
||||||
await expect(page.getByRole('option', { name: 'option3' })).toBeVisible();
|
await expect(page.getByRole('option', { name: 'option3' })).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -36,7 +36,8 @@ test.describe('popup router', () => {
|
|||||||
await page.locator('.ant-drawer-mask').click();
|
await page.locator('.ant-drawer-mask').click();
|
||||||
|
|
||||||
// expect to be back to the first page
|
// expect to be back to the first page
|
||||||
await page.getByText('Users单层子页面Configure').hover();
|
await page.getByLabel('block-item-CardItem-users-').getByText('Users 单层子页面Configure').hover();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'designer-schema-settings-CardItem-blockSettings:table-users' }),
|
page.getByRole('button', { name: 'designer-schema-settings-CardItem-blockSettings:table-users' }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@ -60,7 +61,7 @@ test.describe('popup router', () => {
|
|||||||
await page.locator('.ant-drawer-mask').click();
|
await page.locator('.ant-drawer-mask').click();
|
||||||
|
|
||||||
// expect to be back to the first page
|
// expect to be back to the first page
|
||||||
await page.getByText('Users单层子页面Configure').hover();
|
await page.getByLabel('block-item-CardItem-users-').getByText('Users 单层子页面Configure').hover();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'designer-schema-settings-CardItem-blockSettings:table-users' }),
|
page.getByRole('button', { name: 'designer-schema-settings-CardItem-blockSettings:table-users' }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
@ -57,6 +57,7 @@ test.describe('add blocks to the popup', () => {
|
|||||||
await page.getByRole('menuitem', { name: 'Details right' }).hover();
|
await page.getByRole('menuitem', { name: 'Details right' }).hover();
|
||||||
await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
|
await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
|
||||||
await page.getByRole('menuitem', { name: 'Roles' }).click();
|
await page.getByRole('menuitem', { name: 'Roles' }).click();
|
||||||
|
await page.mouse.move(300, 0);
|
||||||
await page.getByLabel('schema-initializer-Grid-details:configureFields-roles').hover();
|
await page.getByLabel('schema-initializer-Grid-details:configureFields-roles').hover();
|
||||||
await page.getByRole('menuitem', { name: 'Role UID' }).click();
|
await page.getByRole('menuitem', { name: 'Role UID' }).click();
|
||||||
await page.mouse.move(300, 0);
|
await page.mouse.move(300, 0);
|
||||||
|
@ -215,7 +215,7 @@ test.describe('where to open a popup and what can be added to it', () => {
|
|||||||
await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible();
|
await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible();
|
||||||
|
|
||||||
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
|
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
|
||||||
await page.getByRole('menuitem', { name: 'Table right' }).hover();
|
await page.getByRole('menuitem', { name: 'Table right' }).click();
|
||||||
await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1);
|
await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1);
|
||||||
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
|
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
|
||||||
await page.getByRole('menuitem', { name: 'One to many' }).click();
|
await page.getByRole('menuitem', { name: 'One to many' }).click();
|
||||||
@ -282,7 +282,7 @@ test.describe('where to open a popup and what can be added to it', () => {
|
|||||||
await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible();
|
await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible();
|
||||||
|
|
||||||
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
|
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
|
||||||
await page.getByRole('menuitem', { name: 'Table right' }).hover();
|
await page.getByRole('menuitem', { name: 'Table right' }).click();
|
||||||
await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1);
|
await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1);
|
||||||
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
|
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
|
||||||
await page.getByRole('menuitem', { name: 'One to many' }).click();
|
await page.getByRole('menuitem', { name: 'One to many' }).click();
|
||||||
|
@ -71,7 +71,7 @@ test.describe('sub page', () => {
|
|||||||
expect(page.url()).not.toContain('/popups/');
|
expect(page.url()).not.toContain('/popups/');
|
||||||
|
|
||||||
// 确认是否回到了主页面
|
// 确认是否回到了主页面
|
||||||
await page.getByText('Users单层子页面Configure').hover();
|
// await page.getByText('Users单层子页面Configure').hover();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'designer-schema-settings-CardItem-blockSettings:table-users' }),
|
page.getByRole('button', { name: 'designer-schema-settings-CardItem-blockSettings:table-users' }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
@ -18,7 +18,7 @@ test.describe('variables', () => {
|
|||||||
await page.getByLabel('action-Action.Link-View-view-').hover();
|
await page.getByLabel('action-Action.Link-View-view-').hover();
|
||||||
await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:view-users').hover();
|
await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:view-users').hover();
|
||||||
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
|
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
|
||||||
await page.getByLabel('variable-button').click();
|
await page.getByTestId('left-filter-field').getByLabel('variable-button').click();
|
||||||
|
|
||||||
// 2. 断言应该显示的变量
|
// 2. 断言应该显示的变量
|
||||||
['Constant', 'Current user', 'Current role', 'API token', 'Date variables', 'Current record'].forEach(
|
['Constant', 'Current user', 'Current role', 'API token', 'Date variables', 'Current record'].forEach(
|
||||||
|
@ -22,14 +22,14 @@ test.describe('variable: Current Record', () => {
|
|||||||
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
|
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
|
||||||
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
|
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
|
||||||
await page.getByText('Add condition', { exact: true }).click();
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
await page.getByLabel('variable-button').click();
|
await page.getByLabel('variable-button').first().click();
|
||||||
|
|
||||||
// 当前表单中应该包含 “Nickname” 字段
|
// 当前表单中应该包含 “Nickname” 字段
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click();
|
await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
||||||
|
|
||||||
// 当前对象中应该包含 “Role UID” 字段
|
// 当前对象中应该包含 “Role UID” 字段
|
||||||
await page.getByLabel('variable-button').click();
|
await page.getByLabel('variable-button').first().click();
|
||||||
await page.getByText('Current object').click();
|
await page.getByText('Current object').click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click();
|
await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Role UID' }).click();
|
await page.getByRole('menuitemcheckbox', { name: 'Role UID' }).click();
|
||||||
@ -43,12 +43,12 @@ test.describe('variable: Current Record', () => {
|
|||||||
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
|
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
|
||||||
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
|
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
|
||||||
await page.getByText('Add condition', { exact: true }).click();
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
await page.getByLabel('variable-button').click();
|
await page.getByLabel('variable-button').first().click();
|
||||||
|
|
||||||
// 当前记录中应该包含 “Nickname” 字段
|
// 当前记录中应该包含 “Nickname” 字段
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).click();
|
await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
||||||
await page.getByLabel('variable-button').click();
|
await page.getByLabel('variable-button').first().click();
|
||||||
|
|
||||||
// 当前对象中应该包含 “Role UID” 字段
|
// 当前对象中应该包含 “Role UID” 字段
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click();
|
await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click();
|
||||||
|
@ -74,7 +74,7 @@ const useErrorProps = (app: Application, error: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const AppError: FC<{ error: Error; app: Application }> = observer(
|
const AppError: FC<{ error: Error & { title?: string }; app: Application }> = observer(
|
||||||
({ app, error }) => {
|
({ app, error }) => {
|
||||||
const props = getProps(app);
|
const props = getProps(app);
|
||||||
return (
|
return (
|
||||||
@ -87,7 +87,7 @@ const AppError: FC<{ error: Error; app: Application }> = observer(
|
|||||||
transform: translate(0, -50%);
|
transform: translate(0, -50%);
|
||||||
`}
|
`}
|
||||||
status="error"
|
status="error"
|
||||||
title={app.i18n.t('App error')}
|
title={error?.title || app.i18n.t('App error', { ns: 'client' })}
|
||||||
subTitle={app.i18n.t(error?.message)}
|
subTitle={app.i18n.t(error?.message)}
|
||||||
{...props}
|
{...props}
|
||||||
extra={[
|
extra={[
|
||||||
|
@ -0,0 +1,212 @@
|
|||||||
|
/**
|
||||||
|
* 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 { findFirstPageRoute, NocoBaseDesktopRouteType } from '..';
|
||||||
|
import { NocoBaseDesktopRoute } from '../convertRoutesToSchema';
|
||||||
|
|
||||||
|
describe('findFirstPageRoute', () => {
|
||||||
|
// 基本测试:空路由数组
|
||||||
|
it('should return undefined for empty routes array', () => {
|
||||||
|
const result = findFirstPageRoute([]);
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 基本测试:undefined 路由数组
|
||||||
|
it('should return undefined for undefined routes', () => {
|
||||||
|
const result = findFirstPageRoute(undefined);
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试:只有一个页面路由
|
||||||
|
it('should find the first page route when there is only one page', () => {
|
||||||
|
const routes: NocoBaseDesktopRoute[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
schemaUid: 'page1',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 1',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = findFirstPageRoute(routes);
|
||||||
|
expect(result).toEqual(routes[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试:多个页面路由
|
||||||
|
it('should find the first page route when there are multiple pages', () => {
|
||||||
|
const routes: NocoBaseDesktopRoute[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
schemaUid: 'page1',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
schemaUid: 'page2',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 2',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = findFirstPageRoute(routes);
|
||||||
|
expect(result).toEqual(routes[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试:不同类型的路由混合
|
||||||
|
it('should find the first page route among mixed route types', () => {
|
||||||
|
const routes: NocoBaseDesktopRoute[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
schemaUid: 'link1',
|
||||||
|
type: NocoBaseDesktopRouteType.link,
|
||||||
|
title: 'Link 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
schemaUid: 'page1',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 1',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = findFirstPageRoute(routes);
|
||||||
|
expect(result).toEqual(routes[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试:隐藏的菜单项
|
||||||
|
it('should ignore hidden menu items', () => {
|
||||||
|
const routes: NocoBaseDesktopRoute[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
schemaUid: 'page1',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 1',
|
||||||
|
hideInMenu: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
schemaUid: 'page2',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 2',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = findFirstPageRoute(routes);
|
||||||
|
expect(result).toEqual(routes[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试:嵌套路由
|
||||||
|
it('should find page route in nested group', () => {
|
||||||
|
const routes: NocoBaseDesktopRoute[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: NocoBaseDesktopRouteType.group,
|
||||||
|
title: 'Group 1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
schemaUid: 'page1',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = findFirstPageRoute(routes);
|
||||||
|
expect(result).toEqual(routes[0].children[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试:多层嵌套路由
|
||||||
|
it('should find page route in deeply nested groups', () => {
|
||||||
|
const routes: NocoBaseDesktopRoute[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: NocoBaseDesktopRouteType.group,
|
||||||
|
title: 'Group 1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
type: NocoBaseDesktopRouteType.group,
|
||||||
|
title: 'Group 1-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 111,
|
||||||
|
schemaUid: 'page1',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = findFirstPageRoute(routes);
|
||||||
|
expect(result).toEqual(routes[0].children[0].children[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试:复杂路由结构
|
||||||
|
it('should find the first visible page in a complex route structure', () => {
|
||||||
|
const routes: NocoBaseDesktopRoute[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: NocoBaseDesktopRouteType.group,
|
||||||
|
title: 'Group 1',
|
||||||
|
hideInMenu: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
schemaUid: 'page1',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
type: NocoBaseDesktopRouteType.group,
|
||||||
|
title: 'Group 2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 21,
|
||||||
|
schemaUid: 'page2',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = findFirstPageRoute(routes);
|
||||||
|
expect(result).toEqual(routes[1].children[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试:空组
|
||||||
|
it('should skip empty groups and find page in next group', () => {
|
||||||
|
const routes: NocoBaseDesktopRoute[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: NocoBaseDesktopRouteType.group,
|
||||||
|
title: 'Empty Group',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
schemaUid: 'page1',
|
||||||
|
type: NocoBaseDesktopRouteType.page,
|
||||||
|
title: 'Page 1',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = findFirstPageRoute(routes);
|
||||||
|
expect(result).toEqual(routes[1]);
|
||||||
|
});
|
||||||
|
});
|
@ -7,14 +7,15 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EllipsisOutlined } from '@ant-design/icons';
|
import { EllipsisOutlined, HighlightOutlined } from '@ant-design/icons';
|
||||||
import ProLayout, { RouteContext, RouteContextType } from '@ant-design/pro-layout';
|
import ProLayout, { RouteContext, RouteContextType } from '@ant-design/pro-layout';
|
||||||
import { HeaderViewProps } from '@ant-design/pro-layout/es/components/Header';
|
import { HeaderViewProps } from '@ant-design/pro-layout/es/components/Header';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { theme as antdTheme, ConfigProvider, Popover, Tooltip } from 'antd';
|
import { theme as antdTheme, ConfigProvider, Popover, Result, Tooltip } from 'antd';
|
||||||
import { createStyles } from 'antd-style';
|
import { createStyles } from 'antd-style';
|
||||||
import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
import { Link, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ACLRolesCheckProvider,
|
ACLRolesCheckProvider,
|
||||||
@ -199,6 +200,27 @@ const pageContentStyle: React.CSSProperties = {
|
|||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ShowTipWhenNoPages = () => {
|
||||||
|
const { allAccessRoutes } = useAllAccessDesktopRoutes();
|
||||||
|
const { designable } = useDesignable();
|
||||||
|
const { token } = useToken();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
// Check if there are any pages
|
||||||
|
if (allAccessRoutes.length === 0 && !designable && ['/admin', '/admin/'].includes(location.pathname)) {
|
||||||
|
return (
|
||||||
|
<Result
|
||||||
|
icon={<HighlightOutlined style={{ fontSize: '8em', color: token.colorText }} />}
|
||||||
|
title={t('No pages yet, please configure first')}
|
||||||
|
subTitle={t(`Click the "UI Editor" icon in the upper right corner to enter the UI Editor mode`)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
// 移动端中需要使用 dvh 单位来计算高度,否则会出现滚动不到最底部的问题
|
// 移动端中需要使用 dvh 单位来计算高度,否则会出现滚动不到最底部的问题
|
||||||
const mobileHeight = {
|
const mobileHeight = {
|
||||||
height: `calc(100dvh - var(--nb-header-height))`,
|
height: `calc(100dvh - var(--nb-header-height))`,
|
||||||
@ -224,6 +246,7 @@ export const LayoutContent = () => {
|
|||||||
<div className={`${layoutContentClass} nb-subpages-slot-without-header-and-side`} style={style}>
|
<div className={`${layoutContentClass} nb-subpages-slot-without-header-and-side`} style={style}>
|
||||||
<div style={pageContentStyle}>
|
<div style={pageContentStyle}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
<ShowTipWhenNoPages />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -493,6 +516,8 @@ const subMenuItemRender = (item, dom) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CollapsedButton: FC<{ collapsed: boolean }> = (props) => {
|
const CollapsedButton: FC<{ collapsed: boolean }> = (props) => {
|
||||||
|
const { token } = useToken();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteContext.Consumer>
|
<RouteContext.Consumer>
|
||||||
{(context) =>
|
{(context) =>
|
||||||
@ -505,7 +530,7 @@ const CollapsedButton: FC<{ collapsed: boolean }> = (props) => {
|
|||||||
// Fix the issue where the collapse/expand button is covered by subpages
|
// Fix the issue where the collapse/expand button is covered by subpages
|
||||||
.ant-pro-sider-collapsed-button {
|
.ant-pro-sider-collapsed-button {
|
||||||
top: 64px;
|
top: 64px;
|
||||||
left: ${props.collapsed ? 52 : 188}px;
|
left: ${props.collapsed ? 52 : (token.siderWidth || 200) - 12}px;
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
transition: left 0.2s;
|
transition: left 0.2s;
|
||||||
}
|
}
|
||||||
@ -661,7 +686,7 @@ export const InternalAdminLayout = () => {
|
|||||||
<DndContext onDragEnd={onDragEnd}>
|
<DndContext onDragEnd={onDragEnd}>
|
||||||
<ProLayout
|
<ProLayout
|
||||||
contentStyle={contentStyle}
|
contentStyle={contentStyle}
|
||||||
siderWidth={200}
|
siderWidth={token.siderWidth || 200}
|
||||||
className={resetStyle}
|
className={resetStyle}
|
||||||
location={location}
|
location={location}
|
||||||
route={route}
|
route={route}
|
||||||
@ -700,27 +725,11 @@ export const InternalAdminLayout = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function getDefaultPageUid(routes: NocoBaseDesktopRoute[]) {
|
|
||||||
// Find the first route of type "page"
|
|
||||||
for (const route of routes) {
|
|
||||||
if (route.type === NocoBaseDesktopRouteType.page) {
|
|
||||||
return route.schemaUid;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route.children?.length) {
|
|
||||||
const result = getDefaultPageUid(route.children);
|
|
||||||
if (result) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const NavigateToDefaultPage: FC = (props) => {
|
const NavigateToDefaultPage: FC = (props) => {
|
||||||
const { allAccessRoutes } = useAllAccessDesktopRoutes();
|
const { allAccessRoutes } = useAllAccessDesktopRoutes();
|
||||||
const location = useLocationNoUpdate();
|
const location = useLocationNoUpdate();
|
||||||
|
|
||||||
const defaultPageUid = getDefaultPageUid(allAccessRoutes);
|
const defaultPageUid = findFirstPageRoute(allAccessRoutes)?.schemaUid;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -952,16 +961,17 @@ function findRouteById(id: string, treeArray: any[]) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) {
|
export function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) {
|
||||||
if (!routes) return;
|
if (!routes) return;
|
||||||
|
|
||||||
for (const route of routes) {
|
for (const route of routes.filter((item) => !item.hideInMenu)) {
|
||||||
if (route.type === NocoBaseDesktopRouteType.page) {
|
if (route.type === NocoBaseDesktopRouteType.page) {
|
||||||
return route;
|
return route;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.children?.length) {
|
if (route.type === NocoBaseDesktopRouteType.group && route.children?.length) {
|
||||||
return findFirstPageRoute(route.children);
|
const result = findFirstPageRoute(route.children);
|
||||||
|
if (result) return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,14 +9,16 @@
|
|||||||
|
|
||||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||||
import { isValid, uid } from '@formily/shared';
|
import { isValid, uid } from '@formily/shared';
|
||||||
import { ModalProps } from 'antd';
|
import { ModalProps, Select } from 'antd';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useCompile, useDesignable } from '../..';
|
import { useCompile, useDesignable } from '../..';
|
||||||
import { isInitializersSame, useApp } from '../../../application';
|
import { isInitializersSame, useApp, usePlugin } from '../../../application';
|
||||||
|
import { useGlobalVariable } from '../../../application/hooks/useGlobalVariable';
|
||||||
import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings';
|
import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings';
|
||||||
import { useSchemaToolbar } from '../../../application/schema-toolbar';
|
import { useSchemaToolbar } from '../../../application/schema-toolbar';
|
||||||
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager';
|
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager';
|
||||||
|
import { highlightBlock, startScrollEndTracking, stopScrollEndTracking, unhighlightBlock } from '../../../filter-provider/highlightBlock';
|
||||||
import { FlagProvider } from '../../../flag-provider';
|
import { FlagProvider } from '../../../flag-provider';
|
||||||
import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings';
|
import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings';
|
||||||
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
|
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
|
||||||
@ -32,10 +34,10 @@ import {
|
|||||||
SchemaSettingsSwitchItem,
|
SchemaSettingsSwitchItem,
|
||||||
} from '../../../schema-settings/SchemaSettings';
|
} from '../../../schema-settings/SchemaSettings';
|
||||||
import { DefaultValueProvider } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
|
import { DefaultValueProvider } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
|
||||||
|
import { useAllDataBlocks } from '../page/AllDataBlocksProvider';
|
||||||
import { useLinkageAction } from './hooks';
|
import { useLinkageAction } from './hooks';
|
||||||
import { requestSettingsSchema } from './utils';
|
|
||||||
import { useAfterSuccessOptions } from './hooks/useGetAfterSuccessVariablesOptions';
|
import { useAfterSuccessOptions } from './hooks/useGetAfterSuccessVariablesOptions';
|
||||||
import { useGlobalVariable } from '../../../application/hooks/useGlobalVariable';
|
import { requestSettingsSchema } from './utils';
|
||||||
|
|
||||||
const MenuGroup = (props) => {
|
const MenuGroup = (props) => {
|
||||||
return props.children;
|
return props.children;
|
||||||
@ -294,27 +296,105 @@ const useVariableProps = (environmentVariables) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hideDialog = (dialogClassName: string) => {
|
||||||
|
const dialogMask = document.querySelector<HTMLElement>(`.${dialogClassName} > .ant-modal-mask`);
|
||||||
|
const dialogWrap = document.querySelector<HTMLElement>(`.${dialogClassName} > .ant-modal-wrap`);
|
||||||
|
if (dialogMask) {
|
||||||
|
dialogMask.style.opacity = '0';
|
||||||
|
dialogMask.style.transition = 'opacity 0.5s ease';
|
||||||
|
}
|
||||||
|
if (dialogWrap) {
|
||||||
|
dialogWrap.style.opacity = '0';
|
||||||
|
dialogWrap.style.transition = 'opacity 0.5s ease';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showDialog = (dialogClassName: string) => {
|
||||||
|
const dialogMask = document.querySelector<HTMLElement>(`.${dialogClassName} > .ant-modal-mask`);
|
||||||
|
const dialogWrap = document.querySelector<HTMLElement>(`.${dialogClassName} > .ant-modal-wrap`);
|
||||||
|
if (dialogMask) {
|
||||||
|
dialogMask.style.opacity = '1';
|
||||||
|
dialogMask.style.transition = 'opacity 0.5s ease';
|
||||||
|
}
|
||||||
|
if (dialogWrap) {
|
||||||
|
dialogWrap.style.opacity = '1';
|
||||||
|
dialogWrap.style.transition = 'opacity 0.5s ease';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BlocksSelector = (props) => {
|
||||||
|
const { getAllDataBlocks } = useAllDataBlocks();
|
||||||
|
const allDataBlocks = getAllDataBlocks();
|
||||||
|
const compile = useCompile();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// 转换 allDataBlocks 为 Select 选项
|
||||||
|
const options = useMemo(() => {
|
||||||
|
return allDataBlocks.map(block => {
|
||||||
|
// 防止列表中出现已关闭的弹窗中的区块
|
||||||
|
if (!block.dom?.isConnected) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = `${compile(block.collection.title)} #${block.uid.slice(0, 4)}`;
|
||||||
|
return {
|
||||||
|
label: title,
|
||||||
|
value: block.uid,
|
||||||
|
onMouseEnter() {
|
||||||
|
block.highlightBlock();
|
||||||
|
hideDialog('dialog-after-successful-submission');
|
||||||
|
startScrollEndTracking(block.dom, () => {
|
||||||
|
highlightBlock(block.dom.cloneNode(true) as HTMLElement, block.dom.getBoundingClientRect());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onMouseLeave() {
|
||||||
|
block.unhighlightBlock();
|
||||||
|
showDialog('dialog-after-successful-submission');
|
||||||
|
stopScrollEndTracking(block.dom);
|
||||||
|
unhighlightBlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).filter(Boolean);
|
||||||
|
}, [allDataBlocks, t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={props.value}
|
||||||
|
mode="multiple"
|
||||||
|
allowClear
|
||||||
|
placeholder={t('Select data blocks to refresh')}
|
||||||
|
options={options}
|
||||||
|
onChange={props.onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function AfterSuccess() {
|
export function AfterSuccess() {
|
||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { onSuccess } = fieldSchema?.['x-action-settings'] || {};
|
const { onSuccess } = fieldSchema?.['x-action-settings'] || {};
|
||||||
const environmentVariables = useGlobalVariable('$env');
|
const environmentVariables = useGlobalVariable('$env');
|
||||||
|
const templatePlugin: any = usePlugin('@nocobase/plugin-block-template');
|
||||||
|
const isInBlockTemplateConfigPage = templatePlugin?.isInBlockTemplateConfigPage?.();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SchemaSettingsModalItem
|
<SchemaSettingsModalItem
|
||||||
|
dialogRootClassName='dialog-after-successful-submission'
|
||||||
|
width={700}
|
||||||
title={t('After successful submission')}
|
title={t('After successful submission')}
|
||||||
initialValues={
|
initialValues={
|
||||||
onSuccess
|
onSuccess
|
||||||
? {
|
? {
|
||||||
actionAfterSuccess: onSuccess?.redirecting ? 'redirect' : 'previous',
|
actionAfterSuccess: onSuccess?.redirecting ? 'redirect' : 'previous',
|
||||||
...onSuccess,
|
...onSuccess,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
manualClose: false,
|
manualClose: false,
|
||||||
redirecting: false,
|
redirecting: false,
|
||||||
successMessage: '{{t("Saved successfully")}}',
|
successMessage: '{{t("Saved successfully")}}',
|
||||||
actionAfterSuccess: 'previous',
|
actionAfterSuccess: 'previous',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
schema={
|
schema={
|
||||||
{
|
{
|
||||||
@ -382,6 +462,18 @@ export function AfterSuccess() {
|
|||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
'x-use-component-props': () => useVariableProps(environmentVariables),
|
'x-use-component-props': () => useVariableProps(environmentVariables),
|
||||||
},
|
},
|
||||||
|
blocksToRefresh: {
|
||||||
|
type: 'array',
|
||||||
|
title: t('Refresh data blocks'),
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-use-decorator-props': () => {
|
||||||
|
return {
|
||||||
|
tooltip: t('After successful submission, the selected data blocks will be automatically refreshed.'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
'x-component': BlocksSelector,
|
||||||
|
'x-hidden': isInBlockTemplateConfigPage, // 模板配置页面暂不支持该配置
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as ISchema
|
} as ISchema
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||||
import { Drawer } from 'antd';
|
import { Drawer } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import React, { FC, startTransition, useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { FC, startTransition, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
|
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
|
||||||
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
|
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
|
||||||
@ -22,6 +23,7 @@ import { useActionContext } from './hooks';
|
|||||||
import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer';
|
import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer';
|
||||||
import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types';
|
import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types';
|
||||||
import { getZIndex, useZIndexContext, zIndexContext } from './zIndexContext';
|
import { getZIndex, useZIndexContext, zIndexContext } from './zIndexContext';
|
||||||
|
import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant';
|
||||||
|
|
||||||
const MemoizeRecursionField = React.memo(RecursionField);
|
const MemoizeRecursionField = React.memo(RecursionField);
|
||||||
MemoizeRecursionField.displayName = 'MemoizeRecursionField';
|
MemoizeRecursionField.displayName = 'MemoizeRecursionField';
|
||||||
@ -81,6 +83,7 @@ export const InternalActionDrawer: React.FC<ActionDrawerProps> = observer(
|
|||||||
const { visible, setVisible, openSize = 'middle', drawerProps } = useActionContext();
|
const { visible, setVisible, openSize = 'middle', drawerProps } = useActionContext();
|
||||||
const schema = useFieldSchema();
|
const schema = useFieldSchema();
|
||||||
const field = useField();
|
const field = useField();
|
||||||
|
const { t } = useTranslation();
|
||||||
const { componentCls, hashId } = useStyles();
|
const { componentCls, hashId } = useStyles();
|
||||||
const tabContext = useTabsContext();
|
const tabContext = useTabsContext();
|
||||||
const parentZIndex = useZIndexContext();
|
const parentZIndex = useZIndexContext();
|
||||||
@ -118,7 +121,6 @@ export const InternalActionDrawer: React.FC<ActionDrawerProps> = observer(
|
|||||||
},
|
},
|
||||||
[footerNodeName],
|
[footerNodeName],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionContextNoRerender>
|
<ActionContextNoRerender>
|
||||||
<zIndexContext.Provider value={zIndex}>
|
<zIndexContext.Provider value={zIndex}>
|
||||||
@ -126,7 +128,7 @@ export const InternalActionDrawer: React.FC<ActionDrawerProps> = observer(
|
|||||||
<Drawer
|
<Drawer
|
||||||
zIndex={zIndex}
|
zIndex={zIndex}
|
||||||
width={openSizeWidthMap.get(openSize)}
|
width={openSizeWidthMap.get(openSize)}
|
||||||
title={field.title}
|
title={typeof field.title === 'string' ? t(field.title, { ns: NAMESPACE_UI_SCHEMA }) : field.title}
|
||||||
{...others}
|
{...others}
|
||||||
{...drawerProps}
|
{...drawerProps}
|
||||||
rootStyle={rootStyle}
|
rootStyle={rootStyle}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { observer, useField, useFieldSchema } from '@formily/react';
|
import { observer, useField, useFieldSchema } from '@formily/react';
|
||||||
import { Modal, ModalProps } from 'antd';
|
import { Modal, ModalProps, Skeleton } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { FC, startTransition, useEffect, useState } from 'react';
|
import React, { FC, startTransition, useEffect, useState } from 'react';
|
||||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
|
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
|
||||||
@ -53,7 +53,6 @@ const ActionModalContent: FC<{ footerNodeName: string; field: any; schema: any }
|
|||||||
if (!deferredVisible) {
|
if (!deferredVisible) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NocoBaseRecursionField
|
<NocoBaseRecursionField
|
||||||
basePath={field.address}
|
basePath={field.address}
|
||||||
@ -67,9 +66,25 @@ const ActionModalContent: FC<{ footerNodeName: string; field: any; schema: any }
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export function useDelayedVisible(visible: boolean, delay = 200) {
|
||||||
|
const [ready, setReady] = useState(delay === 0);
|
||||||
|
useEffect(() => {
|
||||||
|
if (ready) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (visible) {
|
||||||
|
const timer = setTimeout(() => setReady(true), delay);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
} else {
|
||||||
|
setReady(false);
|
||||||
|
}
|
||||||
|
}, [delay, ready, visible]);
|
||||||
|
return ready;
|
||||||
|
}
|
||||||
|
|
||||||
export const InternalActionModal: React.FC<ActionDrawerProps<ModalProps>> = observer(
|
export const InternalActionModal: React.FC<ActionDrawerProps<ModalProps>> = observer(
|
||||||
(props) => {
|
(props) => {
|
||||||
const { footerNodeName = 'Action.Modal.Footer', width, zIndex: _zIndex, ...others } = props;
|
const { footerNodeName = 'Action.Modal.Footer', width, zIndex: _zIndex, delay = 200, ...others } = props;
|
||||||
const { visible, setVisible, openSize = 'middle', modalProps } = useActionContext();
|
const { visible, setVisible, openSize = 'middle', modalProps } = useActionContext();
|
||||||
const actualWidth = width ?? openSizeWidthMap.get(openSize);
|
const actualWidth = width ?? openSizeWidthMap.get(openSize);
|
||||||
const schema = useFieldSchema();
|
const schema = useFieldSchema();
|
||||||
@ -90,6 +105,7 @@ export const InternalActionModal: React.FC<ActionDrawerProps<ModalProps>> = obse
|
|||||||
}
|
}
|
||||||
|
|
||||||
const zIndex = getZIndex('modal', _zIndex || parentZIndex, props.level || 0);
|
const zIndex = getZIndex('modal', _zIndex || parentZIndex, props.level || 0);
|
||||||
|
const ready = useDelayedVisible(visible, delay); // 200ms 与 Modal 动画时间一致
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionContextNoRerender>
|
<ActionContextNoRerender>
|
||||||
@ -154,7 +170,11 @@ export const InternalActionModal: React.FC<ActionDrawerProps<ModalProps>> = obse
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ActionModalContent footerNodeName={footerNodeName} field={field} schema={schema} />
|
{ready ? (
|
||||||
|
<ActionModalContent footerNodeName={footerNodeName} field={field} schema={schema} />
|
||||||
|
) : (
|
||||||
|
<Skeleton active paragraph={{ rows: 6 }} />
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
</TabsContextProvider>
|
</TabsContextProvider>
|
||||||
</zIndexContext.Provider>
|
</zIndexContext.Provider>
|
||||||
|
@ -48,9 +48,12 @@ import { ActionContextProvider } from './context';
|
|||||||
import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction';
|
import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction';
|
||||||
import { ActionContextProps, ActionProps, ComposedAction } from './types';
|
import { ActionContextProps, ActionProps, ComposedAction } from './types';
|
||||||
import { linkageAction, setInitialActionState } from './utils';
|
import { linkageAction, setInitialActionState } from './utils';
|
||||||
|
import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant';
|
||||||
|
import { BlockContext } from '../../../block-provider/BlockProvider';
|
||||||
|
|
||||||
// 这个要放到最下面,否则会导致前端单测失败
|
// 这个要放到最下面,否则会导致前端单测失败
|
||||||
import { useApp } from '../../../application';
|
import { useApp } from '../../../application';
|
||||||
|
import { useAllDataBlocks } from '../page/AllDataBlocksProvider';
|
||||||
|
|
||||||
const useA = () => {
|
const useA = () => {
|
||||||
return {
|
return {
|
||||||
@ -94,12 +97,16 @@ export const Action: ComposedAction = withDynamicSchemaProps(
|
|||||||
const { designable } = useDesignable();
|
const { designable } = useDesignable();
|
||||||
const tarComponent = useComponent(component) || component;
|
const tarComponent = useComponent(component) || component;
|
||||||
const variables = useVariables();
|
const variables = useVariables();
|
||||||
const localVariables = useLocalVariables({ currentForm: { values: recordData, readPretty: false } as any });
|
const localVariables = useLocalVariables({
|
||||||
|
currentForm: { values: recordData, readPretty: false } as any,
|
||||||
|
});
|
||||||
const { visibleWithURL, setVisibleWithURL } = usePopupUtils();
|
const { visibleWithURL, setVisibleWithURL } = usePopupUtils();
|
||||||
const { setSubmitted } = useActionContext();
|
const { setSubmitted } = useActionContext();
|
||||||
const { getAriaLabel } = useGetAriaLabelOfAction(title);
|
const { getAriaLabel } = useGetAriaLabelOfAction(title);
|
||||||
const parentRecordData = useCollectionParentRecordData();
|
const parentRecordData = useCollectionParentRecordData();
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
|
const { getAllDataBlocks } = useAllDataBlocks();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (field.stateOfLinkageRules) {
|
if (field.stateOfLinkageRules) {
|
||||||
setInitialActionState(field);
|
setInitialActionState(field);
|
||||||
@ -116,6 +123,7 @@ export const Action: ComposedAction = withDynamicSchemaProps(
|
|||||||
condition: v.condition,
|
condition: v.condition,
|
||||||
variables,
|
variables,
|
||||||
localVariables,
|
localVariables,
|
||||||
|
conditionType: v.conditionType,
|
||||||
},
|
},
|
||||||
app.jsonLogic,
|
app.jsonLogic,
|
||||||
);
|
);
|
||||||
@ -130,37 +138,62 @@ export const Action: ComposedAction = withDynamicSchemaProps(
|
|||||||
[onMouseEnter],
|
[onMouseEnter],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleClick = useMemo(() => {
|
||||||
|
return (
|
||||||
|
onClick &&
|
||||||
|
(async (e, callback) => {
|
||||||
|
await onClick?.(e, callback);
|
||||||
|
|
||||||
|
// 执行完 onClick 之后,刷新数据区块
|
||||||
|
const blocksToRefresh = fieldSchema['x-action-settings']?.onSuccess?.blocksToRefresh || [];
|
||||||
|
if (blocksToRefresh.length > 0) {
|
||||||
|
getAllDataBlocks().forEach((block) => {
|
||||||
|
if (blocksToRefresh.includes(block.uid)) {
|
||||||
|
try {
|
||||||
|
block.service?.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to refresh block:', block.uid, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [onClick, fieldSchema, getAllDataBlocks]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InternalAction
|
<BlockContext.Provider value={{ name: 'action' }}>
|
||||||
containerRefKey={containerRefKey}
|
<InternalAction
|
||||||
fieldSchema={fieldSchema}
|
containerRefKey={containerRefKey}
|
||||||
designable={designable}
|
fieldSchema={fieldSchema}
|
||||||
field={field}
|
designable={designable}
|
||||||
icon={icon}
|
field={field}
|
||||||
loading={loading}
|
icon={icon}
|
||||||
handleMouseEnter={handleMouseEnter}
|
loading={loading}
|
||||||
tarComponent={tarComponent}
|
handleMouseEnter={handleMouseEnter}
|
||||||
className={className}
|
tarComponent={tarComponent}
|
||||||
type={props.type}
|
className={className}
|
||||||
Designer={Designer}
|
type={props.type}
|
||||||
onClick={onClick}
|
Designer={Designer}
|
||||||
confirm={confirm}
|
onClick={onClick}
|
||||||
confirmTitle={confirmTitle}
|
confirm={confirm}
|
||||||
popover={popover}
|
confirmTitle={confirmTitle}
|
||||||
addChild={addChild}
|
popover={popover}
|
||||||
recordData={recordData}
|
addChild={addChild}
|
||||||
title={title}
|
recordData={recordData}
|
||||||
style={style}
|
title={title}
|
||||||
propsDisabled={propsDisabled}
|
style={style}
|
||||||
useAction={useAction}
|
propsDisabled={propsDisabled}
|
||||||
visibleWithURL={visibleWithURL}
|
useAction={useAction}
|
||||||
setVisibleWithURL={setVisibleWithURL}
|
visibleWithURL={visibleWithURL}
|
||||||
setSubmitted={setSubmitted}
|
setVisibleWithURL={setVisibleWithURL}
|
||||||
getAriaLabel={getAriaLabel}
|
setSubmitted={setSubmitted}
|
||||||
parentRecordData={parentRecordData}
|
getAriaLabel={getAriaLabel}
|
||||||
actionCallback={actionCallback}
|
parentRecordData={parentRecordData}
|
||||||
{...others}
|
actionCallback={actionCallback}
|
||||||
/>
|
{...others}
|
||||||
|
/>
|
||||||
|
</BlockContext.Provider>
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
{ displayName: 'Action' },
|
{ displayName: 'Action' },
|
||||||
@ -538,6 +571,7 @@ const RenderButtonInner = observer(
|
|||||||
designerProps: any;
|
designerProps: any;
|
||||||
title: string;
|
title: string;
|
||||||
isLink?: boolean;
|
isLink?: boolean;
|
||||||
|
onlyIcon?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
designable,
|
designable,
|
||||||
@ -559,8 +593,10 @@ const RenderButtonInner = observer(
|
|||||||
designerProps,
|
designerProps,
|
||||||
title,
|
title,
|
||||||
isLink,
|
isLink,
|
||||||
|
onlyIcon,
|
||||||
...others
|
...others
|
||||||
} = props;
|
} = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
const debouncedClick = useCallback(
|
const debouncedClick = useCallback(
|
||||||
debounce(
|
debounce(
|
||||||
(e: React.MouseEvent, checkPortal = true) => {
|
(e: React.MouseEvent, checkPortal = true) => {
|
||||||
@ -582,7 +618,8 @@ const RenderButtonInner = observer(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionTitle = title || field?.title;
|
const rawTitle = title ?? field?.title;
|
||||||
|
const actionTitle = typeof rawTitle === 'string' ? t(rawTitle, { ns: NAMESPACE_UI_SCHEMA }) : rawTitle;
|
||||||
const { opacity, ...restButtonStyle } = buttonStyle;
|
const { opacity, ...restButtonStyle } = buttonStyle;
|
||||||
const linkStyle = isLink && opacity ? { opacity } : undefined;
|
const linkStyle = isLink && opacity ? { opacity } : undefined;
|
||||||
return (
|
return (
|
||||||
@ -602,7 +639,7 @@ const RenderButtonInner = observer(
|
|||||||
type={type === 'danger' ? undefined : type}
|
type={type === 'danger' ? undefined : type}
|
||||||
title={actionTitle}
|
title={actionTitle}
|
||||||
>
|
>
|
||||||
{actionTitle && (
|
{!onlyIcon && actionTitle && (
|
||||||
<span className={icon ? 'nb-action-title' : null} style={linkStyle}>
|
<span className={icon ? 'nb-action-title' : null} style={linkStyle}>
|
||||||
{actionTitle}
|
{actionTitle}
|
||||||
</span>
|
</span>
|
||||||
|
@ -72,7 +72,7 @@ const InternalActionBar: FC = (props: any) => {
|
|||||||
<Portal>
|
<Portal>
|
||||||
<DndContext>
|
<DndContext>
|
||||||
<div
|
<div
|
||||||
style={{ display: 'flex', alignItems: 'center', gap: 8, ...style, marginTop: 0 }}
|
style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 0, ...style }}
|
||||||
{...others}
|
{...others}
|
||||||
className={cx(others.className, 'nb-action-bar')}
|
className={cx(others.className, 'nb-action-bar')}
|
||||||
>
|
>
|
||||||
|
@ -12,9 +12,9 @@ import { useCollection_deprecated, useCollectionFilterOptions } from '../../../.
|
|||||||
import { useCollectionRecordData } from '../../../../data-source';
|
import { useCollectionRecordData } from '../../../../data-source';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useCompile } from '../../../';
|
import { useCompile } from '../../../';
|
||||||
import { useBlockContext } from '../../../../block-provider/BlockProvider';
|
|
||||||
import { usePopupVariable } from '../../../../schema-settings/VariableInput/hooks';
|
import { usePopupVariable } from '../../../../schema-settings/VariableInput/hooks';
|
||||||
import { useCurrentRoleVariable } from '../../../../schema-settings/VariableInput/hooks';
|
import { useCurrentRoleVariable } from '../../../../schema-settings/VariableInput/hooks';
|
||||||
|
import { useFormBlockContext } from '../../../../block-provider';
|
||||||
|
|
||||||
export const useAfterSuccessOptions = () => {
|
export const useAfterSuccessOptions = () => {
|
||||||
const collection = useCollection_deprecated();
|
const collection = useCollection_deprecated();
|
||||||
@ -23,7 +23,7 @@ export const useAfterSuccessOptions = () => {
|
|||||||
const userFieldOptions = useCollectionFilterOptions('users', 'main');
|
const userFieldOptions = useCollectionFilterOptions('users', 'main');
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const recordData = useCollectionRecordData();
|
const recordData = useCollectionRecordData();
|
||||||
const { name: blockType } = useBlockContext() || {};
|
const { form } = useFormBlockContext();
|
||||||
const [fields, userFields] = useMemo(() => {
|
const [fields, userFields] = useMemo(() => {
|
||||||
return [compile(fieldsOptions), compile(userFieldOptions)];
|
return [compile(fieldsOptions), compile(userFieldOptions)];
|
||||||
}, [fieldsOptions, userFieldOptions]);
|
}, [fieldsOptions, userFieldOptions]);
|
||||||
@ -32,7 +32,7 @@ export const useAfterSuccessOptions = () => {
|
|||||||
const record = useCollectionRecordData();
|
const record = useCollectionRecordData();
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return [
|
return [
|
||||||
(record || blockType === 'form') && {
|
(record || form) && {
|
||||||
value: '$record',
|
value: '$record',
|
||||||
label: t('Response record', { ns: 'client' }),
|
label: t('Response record', { ns: 'client' }),
|
||||||
children: [...fields],
|
children: [...fields],
|
||||||
@ -62,5 +62,5 @@ export const useAfterSuccessOptions = () => {
|
|||||||
children: null,
|
children: null,
|
||||||
},
|
},
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
}, [recordData, t, fields, blockType, userFields]);
|
}, [recordData, t, fields, form, userFields]);
|
||||||
};
|
};
|
||||||
|
@ -32,7 +32,7 @@ export const useGetAriaLabelOfAction = (title: string) => {
|
|||||||
let { name: blockName } = useBlockContext() || {};
|
let { name: blockName } = useBlockContext() || {};
|
||||||
const actionTitle = title || compile(fieldSchema.title);
|
const actionTitle = title || compile(fieldSchema.title);
|
||||||
collectionName = collectionName ? `-${collectionName}` : '';
|
collectionName = collectionName ? `-${collectionName}` : '';
|
||||||
blockName = blockName ? `-${blockName}` : '';
|
blockName = blockName && blockName !== 'action' ? `-${blockName}` : '';
|
||||||
action = action ? `-${action}` : '';
|
action = action ? `-${action}` : '';
|
||||||
recordName = recordName ? `-${recordName}` : '';
|
recordName = recordName ? `-${recordName}` : '';
|
||||||
|
|
||||||
|
@ -92,6 +92,7 @@ export type ActionDrawerProps<T = DrawerProps> = T & {
|
|||||||
footerNodeName?: string;
|
footerNodeName?: string;
|
||||||
/** 当前弹窗嵌套的层级 */
|
/** 当前弹窗嵌套的层级 */
|
||||||
level?: number;
|
level?: number;
|
||||||
|
delay?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ComposedActionDrawer<T = DrawerProps> = React.FC<ActionDrawerProps<T>> & {
|
export type ComposedActionDrawer<T = DrawerProps> = React.FC<ActionDrawerProps<T>> & {
|
||||||
|
@ -87,12 +87,14 @@ export const linkageAction = async (
|
|||||||
condition,
|
condition,
|
||||||
variables,
|
variables,
|
||||||
localVariables,
|
localVariables,
|
||||||
|
conditionType,
|
||||||
}: {
|
}: {
|
||||||
operator;
|
operator;
|
||||||
field;
|
field;
|
||||||
condition;
|
condition;
|
||||||
variables: VariablesContextType;
|
variables: VariablesContextType;
|
||||||
localVariables: VariableOption[];
|
localVariables: VariableOption[];
|
||||||
|
conditionType: 'advanced' | 'basic';
|
||||||
},
|
},
|
||||||
jsonLogic: any,
|
jsonLogic: any,
|
||||||
) => {
|
) => {
|
||||||
@ -101,7 +103,7 @@ export const linkageAction = async (
|
|||||||
|
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case ActionType.Visible:
|
case ActionType.Visible:
|
||||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
|
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
|
||||||
displayResult.push(operator);
|
displayResult.push(operator);
|
||||||
field.data = field.data || {};
|
field.data = field.data || {};
|
||||||
field.data.hidden = false;
|
field.data.hidden = false;
|
||||||
@ -113,7 +115,7 @@ export const linkageAction = async (
|
|||||||
field.display = last(displayResult);
|
field.display = last(displayResult);
|
||||||
break;
|
break;
|
||||||
case ActionType.Hidden:
|
case ActionType.Hidden:
|
||||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
|
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
|
||||||
field.data = field.data || {};
|
field.data = field.data || {};
|
||||||
field.data.hidden = true;
|
field.data.hidden = true;
|
||||||
} else {
|
} else {
|
||||||
@ -122,7 +124,7 @@ export const linkageAction = async (
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ActionType.Disabled:
|
case ActionType.Disabled:
|
||||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
|
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
|
||||||
disableResult.push(true);
|
disableResult.push(true);
|
||||||
}
|
}
|
||||||
field.stateOfLinkageRules = {
|
field.stateOfLinkageRules = {
|
||||||
@ -133,7 +135,7 @@ export const linkageAction = async (
|
|||||||
field.componentProps['disabled'] = last(disableResult);
|
field.componentProps['disabled'] = last(disableResult);
|
||||||
break;
|
break;
|
||||||
case ActionType.Active:
|
case ActionType.Active:
|
||||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
|
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
|
||||||
disableResult.push(false);
|
disableResult.push(false);
|
||||||
} else {
|
} else {
|
||||||
disableResult.push(!!field.componentProps?.['disabled']);
|
disableResult.push(!!field.componentProps?.['disabled']);
|
||||||
|
@ -14,6 +14,7 @@ import React, { useEffect, useMemo, useState } from 'react';
|
|||||||
import { useAPIClient, useRequest } from '../../../api-client';
|
import { useAPIClient, useRequest } from '../../../api-client';
|
||||||
import { useCollectionManager } from '../../../data-source/collection';
|
import { useCollectionManager } from '../../../data-source/collection';
|
||||||
import { markRecordAsNew } from '../../../data-source/collection-record/isNewRecord';
|
import { markRecordAsNew } from '../../../data-source/collection-record/isNewRecord';
|
||||||
|
import { getDataSourceHeaders } from '../../../data-source/utils';
|
||||||
import { useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive';
|
import { useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive';
|
||||||
import { useSchemaComponentContext } from '../../hooks';
|
import { useSchemaComponentContext } from '../../hooks';
|
||||||
import { AssociationFieldContext } from './context';
|
import { AssociationFieldContext } from './context';
|
||||||
@ -67,9 +68,11 @@ export const AssociationFieldProvider = observer(
|
|||||||
if (_.isUndefined(ids) || _.isNil(ids) || _.isNaN(ids)) {
|
if (_.isUndefined(ids) || _.isNil(ids) || _.isNaN(ids)) {
|
||||||
return Promise.reject(null);
|
return Promise.reject(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.request({
|
return api.request({
|
||||||
resource: collectionField.target,
|
resource: collectionField.target,
|
||||||
action: Array.isArray(ids) ? 'list' : 'get',
|
action: Array.isArray(ids) ? 'list' : 'get',
|
||||||
|
headers: getDataSourceHeaders(cm?.dataSource?.key),
|
||||||
params: {
|
params: {
|
||||||
filter: {
|
filter: {
|
||||||
[targetKey]: ids,
|
[targetKey]: ids,
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
SchemaComponentContext,
|
SchemaComponentContext,
|
||||||
useAPIClient,
|
useAPIClient,
|
||||||
useCollectionRecordData,
|
useCollectionRecordData,
|
||||||
|
useCollectionManager_deprecated,
|
||||||
} from '../../../';
|
} from '../../../';
|
||||||
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
|
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
|
||||||
import { isVariable } from '../../../variables/utils/isVariable';
|
import { isVariable } from '../../../variables/utils/isVariable';
|
||||||
@ -31,6 +32,11 @@ import { Action } from '../action';
|
|||||||
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
|
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
|
||||||
import useServiceOptions, { useAssociationFieldContext } from './hooks';
|
import useServiceOptions, { useAssociationFieldContext } from './hooks';
|
||||||
|
|
||||||
|
const removeIfKeyEmpty = (obj, filterTargetKey) => {
|
||||||
|
if (!obj || typeof obj !== 'object' || !filterTargetKey || Array.isArray(obj)) return obj;
|
||||||
|
return !obj[filterTargetKey] ? null : obj;
|
||||||
|
};
|
||||||
|
|
||||||
export const AssociationFieldAddNewer = (props) => {
|
export const AssociationFieldAddNewer = (props) => {
|
||||||
const schemaComponentCtxValue = useContext(SchemaComponentContext);
|
const schemaComponentCtxValue = useContext(SchemaComponentContext);
|
||||||
return (
|
return (
|
||||||
@ -93,6 +99,9 @@ const InternalAssociationSelect = observer(
|
|||||||
const resource = api.resource(collectionField.target);
|
const resource = api.resource(collectionField.target);
|
||||||
const recordData = useCollectionRecordData();
|
const recordData = useCollectionRecordData();
|
||||||
const schemaComponentCtxValue = useContext(SchemaComponentContext);
|
const schemaComponentCtxValue = useContext(SchemaComponentContext);
|
||||||
|
const { getCollection } = useCollectionManager_deprecated();
|
||||||
|
const associationCollection = getCollection(collectionField.target);
|
||||||
|
const { filterTargetKey } = associationCollection;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initValue = isVariable(field.value) ? undefined : field.value;
|
const initValue = isVariable(field.value) ? undefined : field.value;
|
||||||
@ -167,7 +176,7 @@ const InternalAssociationSelect = observer(
|
|||||||
{...rest}
|
{...rest}
|
||||||
size={'middle'}
|
size={'middle'}
|
||||||
objectValue={objectValue}
|
objectValue={objectValue}
|
||||||
value={value || innerValue}
|
value={removeIfKeyEmpty(value || innerValue, filterTargetKey)}
|
||||||
service={service}
|
service={service}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
const val = value?.length !== 0 ? value : null;
|
const val = value?.length !== 0 ? value : null;
|
||||||
|
@ -13,6 +13,8 @@ import { FormProvider, connect, createSchemaField, observer, useField, useFieldS
|
|||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { Select as AntdSelect, Input, Space, Spin, Tag } from 'antd';
|
import { Select as AntdSelect, Input, Space, Spin, Tag } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAPIClient, useCollectionManager_deprecated } from '../../../';
|
import { useAPIClient, useCollectionManager_deprecated } from '../../../';
|
||||||
@ -152,7 +154,11 @@ const CascadeSelect = connect((props) => {
|
|||||||
} else {
|
} else {
|
||||||
associationField.value = option;
|
associationField.value = option;
|
||||||
}
|
}
|
||||||
onChange?.(options);
|
if (options.length === 1 && !options[0].value) {
|
||||||
|
onChange?.(null);
|
||||||
|
} else {
|
||||||
|
onChange?.(options);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDropdownVisibleChange = async (visible, selectedValue, index) => {
|
const onDropdownVisibleChange = async (visible, selectedValue, index) => {
|
||||||
@ -238,28 +244,38 @@ export const InternalCascadeSelect = observer(
|
|||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { loading, data: formData } = useDataBlockRequest() || {};
|
const { loading, data: formData } = useDataBlockRequest() || {};
|
||||||
const initialValue = formData?.data?.[fieldSchema.name];
|
const initialValue = formData?.data?.[fieldSchema.name];
|
||||||
|
|
||||||
|
const handleFormValuesChange = debounce((form) => {
|
||||||
|
if (collectionField.interface === 'm2o') {
|
||||||
|
// 对 m2o 类型字段,提取最后一个非 null 值
|
||||||
|
const value = extractLastNonNullValueObjects(form.values?.[fieldSchema.name]);
|
||||||
|
setTimeout(() => {
|
||||||
|
form.setValuesIn(fieldSchema.name, value);
|
||||||
|
field.value = value;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 对 select_array 类型字段,过滤掉空对象
|
||||||
|
const value = extractLastNonNullValueObjects(form.values?.select_array).filter(
|
||||||
|
(v) => v && Object.keys(v).length > 0,
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
field.value = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = uid();
|
const id = uid();
|
||||||
selectForm.addEffects(id, () => {
|
selectForm.addEffects(id, () => {
|
||||||
onFormValuesChange((form) => {
|
onFormValuesChange((form) => {
|
||||||
if (collectionField.interface === 'm2o') {
|
handleFormValuesChange(form);
|
||||||
const value = extractLastNonNullValueObjects(form.values?.[fieldSchema.name]);
|
|
||||||
setTimeout(() => {
|
|
||||||
form.setValuesIn(fieldSchema.name, value);
|
|
||||||
field.value = value;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const value = extractLastNonNullValueObjects(form.values?.select_array).filter(
|
|
||||||
(v) => v && Object.keys(v).length > 0,
|
|
||||||
);
|
|
||||||
setTimeout(() => {
|
|
||||||
field.value = value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
selectForm.removeEffects(id);
|
selectForm.removeEffects(id);
|
||||||
|
// 清除防抖定时器
|
||||||
|
handleFormValuesChange.cancel();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -282,6 +298,24 @@ export const InternalCascadeSelect = observer(
|
|||||||
items: {
|
items: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-component': 'Space',
|
'x-component': 'Space',
|
||||||
|
'x-component-props': {
|
||||||
|
style: {
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
className: css`
|
||||||
|
.ant-formily-item-control {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
.ant-space-item:nth-child(1) {
|
||||||
|
flex: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-space-item:nth-child(2) {
|
||||||
|
flex: 3;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
properties: {
|
properties: {
|
||||||
sort: {
|
sort: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
@ -257,8 +257,7 @@ export const SubTable: any = observer(
|
|||||||
{field.editable && (
|
{field.editable && (
|
||||||
<Space
|
<Space
|
||||||
style={{
|
style={{
|
||||||
marginTop: '10px',
|
position: 'relative',
|
||||||
position: field.value?.length ? 'absolute' : 'relative',
|
|
||||||
bottom: '0',
|
bottom: '0',
|
||||||
gap: 15,
|
gap: 15,
|
||||||
}}
|
}}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user