Merge branch 'main' into T-1155

This commit is contained in:
Rain 2023-07-28 08:42:25 +08:00
commit c5169dde3a
286 changed files with 4880 additions and 2490 deletions

View File

@ -1,4 +1,9 @@
{
"env": {
"node": true,
"es6": true,
"browser": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",

View File

@ -1,26 +1,25 @@
name: Aliyun Container Registry
name: Build Docker Image
on:
push:
branches:
- 'main'
- 'develop'
paths:
- 'packages/**'
- 'docker/nocobase/**'
- 'Dockerfile.acr'
- '.github/workflows/aliyun-container-registry.yml'
- 'Dockerfile'
- '.github/workflows/build-docker-image.yml'
pull_request:
branches:
- '**'
paths:
- 'packages/**'
- 'docker/nocobase/**'
- 'Dockerfile.acr'
- '.github/workflows/aliyun-container-registry.yml'
- 'Dockerfile'
- '.github/workflows/build-docker-image.yml'
jobs:
push-acr:
build-and-push:
if: github.event.pull_request.head.repo.fork != true
runs-on: ubuntu-latest
services:
@ -51,22 +50,41 @@ jobs:
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Login to Docker Hub
- 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: Login to Docker Hub
if: github.ref == 'refs/heads/main'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set tags
id: set-tags
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "::set-output name=tags::${{ steps.meta.outputs.tags }},${{ secrets.ALI_DOCKER_REGISTRY }}/${{ steps.meta.outputs.tags }}"
else
echo "::set-output name=tags::${{ secrets.ALI_DOCKER_REGISTRY }}/${{ steps.meta.outputs.tags }}"
fi
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
file: Dockerfile.acr
file: Dockerfile
build-args: |
VERDACCIO_URL=http://localhost:4873/
COMMIT_HASH=${GITHUB_SHA}
push: true
tags: ${{ secrets.ALI_DOCKER_REGISTRY }}/${{ steps.meta.outputs.tags }}
tags: ${{ steps.set-tags.outputs.tags }}
- name: Deploy NocoBase
env:
IMAGE_TAG: ${{ steps.meta.outputs.tags }}

View File

@ -1,61 +0,0 @@
name: Docker Hub
on:
push:
branches:
- 'main'
paths:
- 'packages/**'
- 'docker/nocobase/**'
- 'Dockerfile'
- '.github/workflows/docker-hub.yml'
jobs:
push-docker:
runs-on: ubuntu-latest
services:
verdaccio:
image: verdaccio/verdaccio
ports:
- 4873:4873
steps:
-
name: Checkout
uses: actions/checkout@v3
-
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 Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v3
with:
context: .
file: Dockerfile
build-args: |
VERDACCIO_URL=http://localhost:4873/
# platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}

View File

@ -38,13 +38,13 @@ jobs:
cache: 'yarn'
- run: yarn install
- name: Test with Sqlite
run: yarn nocobase install -f && yarn test
run: yarn nocobase install -f && node --max_old_space_size=4096 ./node_modules/.bin/jest --maxWorkers=1 --workerIdleMemoryLimit=3000MB
env:
NODE_OPTIONS: '--max_old_space_size=4096'
LOGGER_LEVEL: error
DB_DIALECT: sqlite
DB_STORAGE: /tmp/db.sqlite
DB_UNDERSCORED: ${{ matrix.underscored }}
timeout-minutes: 30
timeout-minutes: 35
postgres-test:
strategy:
@ -80,9 +80,9 @@ jobs:
- run: yarn install
# - run: yarn build
- name: Test with postgres
run: yarn nocobase install -f && yarn test
run: yarn nocobase install -f && node --max_old_space_size=4096 ./node_modules/.bin/jest --maxWorkers=1 --workerIdleMemoryLimit=3000MB
env:
NODE_OPTIONS: '--max_old_space_size=4096'
LOGGER_LEVEL: error
DB_DIALECT: postgres
DB_HOST: postgres
DB_PORT: 5432
@ -92,7 +92,7 @@ jobs:
DB_UNDERSCORED: ${{ matrix.underscored }}
DB_SCHEMA: ${{ matrix.schema }}
COLLECTION_MANAGER_SCHEMA: ${{ matrix.collection_schema }}
timeout-minutes: 30
timeout-minutes: 35
mysql-test:
strategy:
@ -118,9 +118,9 @@ jobs:
- run: yarn install
# - run: yarn build
- name: Test with MySQL
run: yarn nocobase install -f && yarn test
run: yarn nocobase install -f && node --max_old_space_size=4096 ./node_modules/.bin/jest --maxWorkers=1 --workerIdleMemoryLimit=3000MB
env:
NODE_OPTIONS: '--max_old_space_size=4096'
LOGGER_LEVEL: error
DB_DIALECT: mysql
DB_HOST: mysql
DB_PORT: 3306
@ -128,4 +128,4 @@ jobs:
DB_PASSWORD: password
DB_DATABASE: nocobase
DB_UNDERSCORED: ${{ matrix.underscored }}
timeout-minutes: 30
timeout-minutes: 35

View File

@ -7,6 +7,104 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
## [v0.11.1-alpha.3](https://github.com/nocobase/nocobase/compare/v0.11.1-alpha.2...v0.11.1-alpha.3) - 2023-07-26
### Merged
- fix(plugin-workflow): fix expression field in sub-form [`#2324`](https://github.com/nocobase/nocobase/pull/2324)
- chore: improve FormProvider [`#2322`](https://github.com/nocobase/nocobase/pull/2322)
- fix: collectionField undefined [`#2320`](https://github.com/nocobase/nocobase/pull/2320)
- fix: should use `filter` instead of `where` [`#2318`](https://github.com/nocobase/nocobase/pull/2318)
- fix(bi): issue of dnd [`#2315`](https://github.com/nocobase/nocobase/pull/2315)
- feat(filter-block): support foreign key and inheritance [`#2302`](https://github.com/nocobase/nocobase/pull/2302)
- chore: merge docker build [`#2317`](https://github.com/nocobase/nocobase/pull/2317)
- feat(locale): allows to manage locale resources in core package [`#2293`](https://github.com/nocobase/nocobase/pull/2293)
- fix(plugin-workflow): fix styles [`#2316`](https://github.com/nocobase/nocobase/pull/2316)
- Feat/translation fr_FR [`#2275`](https://github.com/nocobase/nocobase/pull/2275)
- feat: customize action support create record for any collection [`#2264`](https://github.com/nocobase/nocobase/pull/2264)
- refactor: form data template support data scope config [`#2229`](https://github.com/nocobase/nocobase/pull/2229)
- chore: auto fix eslint errors when pre-commit [`#2304`](https://github.com/nocobase/nocobase/pull/2304)
- refactor: sub-table acl ignore [`#2259`](https://github.com/nocobase/nocobase/pull/2259)
- refactor: date field UI supports configuration formatting [`#2241`](https://github.com/nocobase/nocobase/pull/2241)
- fix(plugin-workflow): fix schedule duplicated triggering in multi-apps [`#2313`](https://github.com/nocobase/nocobase/pull/2313)
- refactor: table column field provider optimize [`#2312`](https://github.com/nocobase/nocobase/pull/2312)
- fix: table column field undefined fix [`#2311`](https://github.com/nocobase/nocobase/pull/2311)
- fix: table column field failed to be actived [`#2309`](https://github.com/nocobase/nocobase/pull/2309)
- fix(default-value): fix tag in RemoteSelect [`#2306`](https://github.com/nocobase/nocobase/pull/2306)
- fix: modal not displayed when clicking on the association field in the table [`#2292`](https://github.com/nocobase/nocobase/pull/2292)
- fix(database): skip reference delete on view collection [`#2303`](https://github.com/nocobase/nocobase/pull/2303)
### Commits
- chore(versions): 😊 publish v0.11.1-alpha.3 [`81819f0`](https://github.com/nocobase/nocobase/commit/81819f04e3bdd108a1a70038352545748552c2f9)
- chore: fix Warning if eslint [`986e241`](https://github.com/nocobase/nocobase/commit/986e2414d4b8eba2bd0cf3cf1932a74ff507271e)
- chore: fix prettier [`30b0d9b`](https://github.com/nocobase/nocobase/commit/30b0d9b3f303a43eeb340482a567a50145437f27)
## [v0.11.1-alpha.2](https://github.com/nocobase/nocobase/compare/v0.11.1-alpha.1...v0.11.1-alpha.2) - 2023-07-23
### Commits
- chore(versions): 😊 publish v0.11.1-alpha.2 [`c84476d`](https://github.com/nocobase/nocobase/commit/c84476d805bae897fea7a23cec38813dbe28cae0)
- chore(theme-editor): fix deps [`d0528cf`](https://github.com/nocobase/nocobase/commit/d0528cf1f273fd7e3efbe6eb58a247a20dbaffb1)
- chore(theme-editor): fix deps [`25decf0`](https://github.com/nocobase/nocobase/commit/25decf0aa9f6d37b972ba460a999558ecc25a819)
## [v0.11.1-alpha.1](https://github.com/nocobase/nocobase/compare/v0.11.0-alpha.1...v0.11.1-alpha.1) - 2023-07-22
### Merged
- fix(plugin-workflow): workflow collections should not appear in blocks [`#2290`](https://github.com/nocobase/nocobase/pull/2290)
- chore: remove belongsToMany through table as collection dependency [`#2289`](https://github.com/nocobase/nocobase/pull/2289)
- feat(database): handle targetCollection option in repository find [`#2175`](https://github.com/nocobase/nocobase/pull/2175)
- feat: add built-in themes [`#2284`](https://github.com/nocobase/nocobase/pull/2284)
- docs: add doc for Theme Editor [`#2280`](https://github.com/nocobase/nocobase/pull/2280)
- fix: fix sorting of user menu [`#2288`](https://github.com/nocobase/nocobase/pull/2288)
- feat(theme-editor): support to config Header's color and Settings button's color [`#2263`](https://github.com/nocobase/nocobase/pull/2263)
- feat(plugin-workflow): add sql node [`#2276`](https://github.com/nocobase/nocobase/pull/2276)
- fix: the drop-down multiple selection fields are not displayed as title fields when inherited collection [`#2257`](https://github.com/nocobase/nocobase/pull/2257)
- fix(bi): orderBy bug under MySQL [`#2283`](https://github.com/nocobase/nocobase/pull/2283)
- test: make testing more stable [`#2277`](https://github.com/nocobase/nocobase/pull/2277)
- fix(bi): eliminate redundancy queries [`#2268`](https://github.com/nocobase/nocobase/pull/2268)
- fix(client): using component as action title [`#2274`](https://github.com/nocobase/nocobase/pull/2274)
- fix(middleware): revert now variable back [`#2267`](https://github.com/nocobase/nocobase/pull/2267)
- fix: linkage failed with current date variable [`#2272`](https://github.com/nocobase/nocobase/pull/2272)
- fix: fix style of page tab [`#2270`](https://github.com/nocobase/nocobase/pull/2270)
- fix: collection select no options [`#2271`](https://github.com/nocobase/nocobase/pull/2271)
- refactor: add locale plugin [`#2261`](https://github.com/nocobase/nocobase/pull/2261)
- feat(plugin-workflow): allow manual form button to be configured with preset values [`#2225`](https://github.com/nocobase/nocobase/pull/2225)
- feat(plugin-workflow): change to unlimited depth preloading associations in workflow [`#2142`](https://github.com/nocobase/nocobase/pull/2142)
- feat: localization management [`#2210`](https://github.com/nocobase/nocobase/pull/2210)
- refactor: linkage rules support datetime [`#2260`](https://github.com/nocobase/nocobase/pull/2260)
- fix: view inherited collection field reported error [`#2249`](https://github.com/nocobase/nocobase/pull/2249)
- fix: loading did not disappear after submission failure [`#2252`](https://github.com/nocobase/nocobase/pull/2252)
- feat: support custome themes [`#2228`](https://github.com/nocobase/nocobase/pull/2228)
- chore(plugin-workflow): fix breadcrumb warning [`#2256`](https://github.com/nocobase/nocobase/pull/2256)
- fix(plugin-workflow): fix request node error in loop [`#2254`](https://github.com/nocobase/nocobase/pull/2254)
- feat(database): view collection support for add new, update and delete actions [`#2119`](https://github.com/nocobase/nocobase/pull/2119)
- refactor(client): change isTitleField check to interface property titleUsable [`#2250`](https://github.com/nocobase/nocobase/pull/2250)
- fix: option field display value in workflow todo list [`#2246`](https://github.com/nocobase/nocobase/pull/2246)
- fix(plugin-workflow): fix dispatch bug [`#2247`](https://github.com/nocobase/nocobase/pull/2247)
- fix: avoid crashes when emptying DatePicker's value [`#2237`](https://github.com/nocobase/nocobase/pull/2237)
- fix: no template data requested during depulicating [`#2240`](https://github.com/nocobase/nocobase/pull/2240)
- fix(plugin-workflow): fix job button style [`#2243`](https://github.com/nocobase/nocobase/pull/2243)
- fix: avoid crashing when delete group menu [`#2239`](https://github.com/nocobase/nocobase/pull/2239)
- fix: should auto focus in drop-down menu [`#2234`](https://github.com/nocobase/nocobase/pull/2234)
- fix(plugin-fm): adjust upload file size to 1G which same as default on server side [`#2236`](https://github.com/nocobase/nocobase/pull/2236)
- fix: should only show one scroll bar in drop-down menu [`#2231`](https://github.com/nocobase/nocobase/pull/2231)
- fix: failed to correctly respond to optional fields in the child collection in the parent collection table [`#2207`](https://github.com/nocobase/nocobase/pull/2207)
- fix(core): fix batch update query logic [`#2230`](https://github.com/nocobase/nocobase/pull/2230)
- fix: should limit submenu height [`#2227`](https://github.com/nocobase/nocobase/pull/2227)
- fix(upload): fix style of attachement in Table [`#2213`](https://github.com/nocobase/nocobase/pull/2213)
### Fixed
- fix(plugin-fm): adjust upload file size to 1G which same as default on server side (#2236) [`#2215`](https://github.com/nocobase/nocobase/issues/2215)
### Commits
- chore(versions): 😊 publish v0.11.1-alpha.1 [`e979194`](https://github.com/nocobase/nocobase/commit/e979194cf29debcc10d2e6765c96083793186331)
- fix(theme-editor): remove db.sync [`fa2de8e`](https://github.com/nocobase/nocobase/commit/fa2de8e8060da00a85b381df0d7fbf9fca2793b3)
- fix(theme-editor): fix color of menu when it is selected [`8c90436`](https://github.com/nocobase/nocobase/commit/8c904363ad055d6aaacfe67d9f74a9467e7c90b5)
## [v0.11.0-alpha.1](https://github.com/nocobase/nocobase/compare/v0.10.1-alpha.1...v0.11.0-alpha.1) - 2023-07-08
### Merged

View File

@ -1,6 +1,8 @@
FROM node:18 as builder
FROM node:16 as builder
ARG VERDACCIO_URL=http://host.docker.internal:10104/
ARG COMIT_HASH
ARG COMMIT_HASH
ARG APPEND_PRESET_LOCAL_PLUGINS
ARG BEFORE_PACK_NOCOBASE="ls -l"
RUN apt-get update && apt-get install -y jq
WORKDIR /tmp
@ -23,10 +25,13 @@ RUN yarn config set registry $VERDACCIO_URL
WORKDIR /app
RUN cd /app \
&& yarn config set network-timeout 600000 -g \
&& yarn create nocobase-app my-nocobase-app -a -e APP_ENV=production \
&& yarn create nocobase-app my-nocobase-app -a -e APP_ENV=production -e APPEND_PRESET_LOCAL_PLUGINS=$APPEND_PRESET_LOCAL_PLUGINS \
&& cd /app/my-nocobase-app \
&& yarn install --production
WORKDIR /app/my-nocobase-app
RUN $BEFORE_PACK_NOCOBASE
RUN cd /app \
&& rm -rf my-nocobase-app/packages/app/client/src/.umi \
&& rm -rf nocobase.tar.gz \
@ -50,7 +55,7 @@ COPY --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz
WORKDIR /app/nocobase
RUN mkdir -p /app/nocobase/storage/uploads/ && echo "$COMIT_HASH" >> /app/nocobase/storage/uploads/COMIT_HASH
RUN mkdir -p /app/nocobase/storage/uploads/ && echo "$COMMIT_HASH" >> /app/nocobase/storage/uploads/COMMIT_HASH
COPY ./docker/nocobase/docker-entrypoint.sh /app/

View File

@ -1,63 +0,0 @@
FROM node:16 as builder
ARG VERDACCIO_URL=http://host.docker.internal:10104/
ARG COMMIT_HASH
ARG APPEND_PRESET_LOCAL_PLUGINS
ARG BEFORE_PACK_NOCOBASE="ls -l"
RUN apt-get update && apt-get install -y jq
WORKDIR /tmp
COPY . /tmp
RUN npx npm-cli-adduser --username test --password test -e test@nocobase.com -r $VERDACCIO_URL
RUN cd /tmp && \
NEWVERSION="$(cat lerna.json | jq '.version' | tr -d '"').$(date +'%Y%m%d%H%M%S')" \
&& tmp=$(mktemp) \
&& jq ".version = \"${NEWVERSION}\"" lerna.json > "$tmp" && mv "$tmp" lerna.json
RUN yarn install && yarn build
RUN git checkout -b release \
&& yarn version:alpha -y \
&& git config user.email "test@mail.com" \
&& git config user.name "test" && git add . \
&& git commit -m "chore(versions): test publish packages xxx" \
&& yarn release:force --registry $VERDACCIO_URL
RUN yarn config set registry $VERDACCIO_URL
WORKDIR /app
RUN cd /app \
&& yarn config set network-timeout 600000 -g \
&& yarn create nocobase-app my-nocobase-app -a -e APP_ENV=production -e APPEND_PRESET_LOCAL_PLUGINS=$APPEND_PRESET_LOCAL_PLUGINS \
&& cd /app/my-nocobase-app \
&& yarn install --production
WORKDIR /app/my-nocobase-app
RUN $BEFORE_PACK_NOCOBASE
RUN cd /app \
&& rm -rf my-nocobase-app/packages/app/client/src/.umi \
&& rm -rf nocobase.tar.gz \
&& rm -rf ./my-nocobase-app/node_modules/@antv \
&& rm -rf ./my-nocobase-app/node_modules/antd/dist \
&& rm -rf ./my-nocobase-app/node_modules/antd/es \
&& rm -rf ./my-nocobase-app/node_modules/antd/node_modules \
&& rm -rf ./my-nocobase-app/node_modules/@ant-design \
&& rm -rf ./my-nocobase-app/node_modules/china-division/dist/villages.json \
&& find ./my-nocobase-app/node_modules/china-division/dist -name '*.csv' -delete \
&& find ./my-nocobase-app/node_modules/china-division/dist -name '*.sqlite' -delete \
&& tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app .
FROM node:16.20-bullseye-slim
RUN apt-get update && apt-get install -y nginx
RUN rm -rf /etc/nginx/sites-enabled/default
COPY ./docker/nocobase/nocobase.conf /etc/nginx/sites-enabled/nocobase.conf
COPY --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz
WORKDIR /app/nocobase
RUN mkdir -p /app/nocobase/storage/uploads/ && echo "$COMMIT_HASH" >> /app/nocobase/storage/uploads/COMMIT_HASH
COPY ./docker/nocobase/docker-entrypoint.sh /app/
CMD ["/app/docker-entrypoint.sh"]

View File

@ -67,7 +67,7 @@ NocoBase 采用插件化架构,所有新功能都可以通过开发和安装
如果你需要商业版本和商业服务欢迎通过邮件联系我们hello@nocobase.com
也可以添加我们的微信,沟通商业合作或者加入微信群:
也可以添加我们的微信,沟通商业合作或者加入用户交流群:
![](https://www.nocobase.com/images/wechat.png)

View File

@ -1,4 +1,4 @@
const { pathsToModuleNameMapper } = require('ts-jest/utils');
const { pathsToModuleNameMapper } = require('ts-jest');
const { compilerOptions } = require('./tsconfig.json');
const { defaults } = require('jest-config');
@ -6,26 +6,26 @@ module.exports = {
rootDir: process.cwd(),
collectCoverage: false,
verbose: true,
testEnvironment: 'jsdom',
preset: 'ts-jest',
testMatch: ['**/__tests__/**/*.test.[jt]s'],
setupFiles: ['./jest.setup.ts'],
setupFilesAfterEnv: [require.resolve('jest-dom/extend-expect'), './jest.setupAfterEnv.ts'],
setupFilesAfterEnv: ['./jest.setupAfterEnv.ts'],
moduleNameMapper: {
...pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '<rootDir>/',
}),
},
globals: {
'ts-jest': {
babelConfig: false,
tsconfig: './tsconfig.jest.json',
diagnostics: false,
},
transform: {
'^.+\\.{ts|tsx}?$': [
'ts-jest',
{
babelConfig: false,
tsconfig: './tsconfig.jest.json',
diagnostics: false,
},
],
},
modulePathIgnorePatterns: ['/esm/', '/es/', '/dist/', '/lib/', '/client/', '/sdk/', '\\.test\\.tsx$'],
// add .mjs .cjs for formula.js
moduleFileExtensions: [...defaults.moduleFileExtensions, 'mjs', 'cjs'],
coveragePathIgnorePatterns: [
'/node_modules/',
'/__tests__/',

View File

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

View File

@ -33,6 +33,7 @@
"resolutions": {
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@typescript-eslint/parser": "^6.2.0",
"react-router-dom": "^6.11.2",
"react-router": "^6.11.2",
"react": "^18.0.0",
@ -40,11 +41,20 @@
},
"config": {
"ghooks": {
"pre-commit": "yarn lint-staged",
"commit-msg": "commitlint --edit"
}
},
"lint-staged": {
"*.{js,json}": [
"prettier --write"
],
"*.ts?(x)": [
"eslint --fix",
"prettier --parser=typescript --write"
]
},
"devDependencies": {
"commander": "^9.2.0",
"@commitlint/cli": "^16.1.0",
"@commitlint/config-conventional": "^16.0.0",
"@commitlint/prompt-cli": "^16.1.0",
@ -55,17 +65,21 @@
"@types/react-dom": "^17.0.0",
"@vitejs/plugin-react": "^4.0.0",
"auto-changelog": "^2.4.0",
"commander": "^9.2.0",
"dumi": "^2.2.0",
"dumi-theme-nocobase": "^0.2.14",
"eslint-plugin-jest-dom": "^5.0.1",
"eslint-plugin-testing-library": "^5.11.0",
"ghooks": "^2.0.4",
"jest": "^29.6.2",
"jest-cli": "^29.6.2",
"jsdom-worker": "^0.3.0",
"prettier": "^2.2.1",
"lint-staged": "^13.2.3",
"pretty-format": "^24.0.0",
"pretty-quick": "^3.1.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"ts-jest": "^29.1.1",
"typescript": "5.1.3",
"vite": "^4.4.1",
"vitest": "^0.33.0"
@ -73,5 +87,6 @@
"volta": {
"node": "18.14.2",
"yarn": "1.22.19"
}
},
"dependencies": {}
}

View File

@ -1,9 +1,9 @@
{
"name": "@nocobase/app-client",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"license": "AGPL-3.0",
"devDependencies": {
"@nocobase/client": "0.11.1-alpha.2"
"@nocobase/client": "0.11.1-alpha.3"
},
"repository": {
"type": "git",

View File

@ -1,12 +1,12 @@
{
"name": "@nocobase/app-server",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
"@nocobase/preset-nocobase": "0.11.1-alpha.2"
"@nocobase/preset-nocobase": "0.11.1-alpha.3"
},
"repository": {
"type": "git",

View File

@ -1,13 +1,13 @@
{
"name": "@nocobase/acl",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"description": "",
"license": "Apache-2.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
"@nocobase/resourcer": "0.11.1-alpha.2",
"@nocobase/utils": "0.11.1-alpha.2",
"@nocobase/resourcer": "0.11.1-alpha.3",
"@nocobase/utils": "0.11.1-alpha.3",
"minimatch": "^5.1.1"
},
"repository": {

View File

@ -1,14 +1,14 @@
{
"name": "@nocobase/actions",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"description": "",
"license": "Apache-2.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
"@nocobase/cache": "0.11.1-alpha.2",
"@nocobase/database": "0.11.1-alpha.2",
"@nocobase/resourcer": "0.11.1-alpha.2"
"@nocobase/cache": "0.11.1-alpha.3",
"@nocobase/database": "0.11.1-alpha.3",
"@nocobase/resourcer": "0.11.1-alpha.3"
},
"repository": {
"type": "git",

View File

@ -1,15 +1,15 @@
{
"name": "@nocobase/auth",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"description": "",
"license": "Apache-2.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
"@nocobase/actions": "0.11.1-alpha.2",
"@nocobase/database": "0.11.1-alpha.2",
"@nocobase/resourcer": "0.11.1-alpha.2",
"@nocobase/utils": "0.11.1-alpha.2"
"@nocobase/actions": "0.11.1-alpha.3",
"@nocobase/database": "0.11.1-alpha.3",
"@nocobase/resourcer": "0.11.1-alpha.3",
"@nocobase/utils": "0.11.1-alpha.3"
},
"repository": {
"type": "git",

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/build",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"description": "Library build tool based on rollup.",
"main": "lib/index.js",
"bin": {

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/cache",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"description": "",
"license": "Apache-2.0",
"main": "./lib/index.js",

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/cli",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"description": "",
"license": "Apache-2.0",
"main": "./src/index.js",
@ -18,10 +18,11 @@
"fs-extra": "^11.1.1",
"pm2": "^5.2.0",
"portfinder": "^1.0.28",
"serve": "^13.0.2"
"serve": "^13.0.2",
"tsx": "^3.12.7"
},
"devDependencies": {
"@nocobase/devtools": "0.11.1-alpha.2"
"@nocobase/devtools": "0.11.1-alpha.3"
},
"repository": {
"type": "git",

View File

@ -55,13 +55,13 @@ module.exports = (cli) => {
}
await runAppCommand('install', ['--silent']);
// if (opts.dbSync) {
// await runAppCommand('db:sync');
// }
if (server || !client) {
console.log('starting server', serverPort);
const argv = [
'-P',
'watch',
'--tsconfig',
'./tsconfig.server.json',
'-r',
'tsconfig-paths/register',
@ -74,8 +74,9 @@ module.exports = (cli) => {
if (opts.dbSync) {
argv.push('--db-sync');
}
const runDevServer = () => {
run('ts-node-dev', argv, {
run('tsx', argv, {
env: {
APP_PORT: serverPort,
},
@ -91,6 +92,7 @@ module.exports = (cli) => {
runDevServer();
}
if (client || !server) {
console.log('starting client', 1 * clientPort);
run('umi', ['dev'], {

View File

@ -106,7 +106,7 @@ exports.runInstall = async () => {
if (exports.isDev()) {
const argv = [
'-P',
'--tsconfig',
'./tsconfig.server.json',
'-r',
'tsconfig-paths/register',
@ -114,7 +114,7 @@ exports.runInstall = async () => {
'install',
'-s',
];
await exports.run('ts-node', argv);
await exports.run('tsx', argv);
} else if (isProd()) {
const file = `./packages/${APP_PACKAGE_ROOT}/server/lib/index.js`;
const argv = [file, 'install', '-s'];
@ -127,7 +127,7 @@ exports.runAppCommand = async (command, args = []) => {
if (exports.isDev()) {
const argv = [
'-P',
'--tsconfig',
'./tsconfig.server.json',
'-r',
'tsconfig-paths/register',
@ -135,7 +135,7 @@ exports.runAppCommand = async (command, args = []) => {
command,
...args,
];
await exports.run('ts-node', argv);
await exports.run('tsx', argv);
} else if (isProd()) {
const argv = [`./packages/${APP_PACKAGE_ROOT}/server/lib/index.js`, command, ...args];
await exports.run('node', argv);

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/client",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"license": "Apache-2.0",
"main": "lib",
"module": "es/index.js",
@ -15,9 +15,9 @@
"@formily/antd-v5": "^1.1.0-beta.4",
"@formily/core": "2.2.26",
"@formily/react": "2.2.26",
"@nocobase/evaluators": "0.11.1-alpha.2",
"@nocobase/sdk": "0.11.1-alpha.2",
"@nocobase/utils": "0.11.1-alpha.2",
"@nocobase/evaluators": "0.11.1-alpha.3",
"@nocobase/sdk": "0.11.1-alpha.3",
"@nocobase/utils": "0.11.1-alpha.3",
"ahooks": "^3.7.2",
"antd": "^5.6.4",
"antd-style": "^3.3.0",

View File

@ -233,7 +233,10 @@ export const useACLFieldWhitelist = () => {
.concat(params?.appends || []);
return {
whitelist,
schemaInWhitelist(fieldSchema: Schema) {
schemaInWhitelist(fieldSchema: Schema, isSkip?) {
if (isSkip) {
return true;
}
if (whitelist.length === 0) {
return true;
}

View File

@ -66,7 +66,7 @@ export const useIsEmptyRecord = () => {
export const FormBlockProvider = (props) => {
const record = useRecord();
const { collection } = props;
const { collection, isCusomeizeCreate } = props;
const { __collection } = record;
const currentCollection = useCollection();
const { designable } = useDesignable();
@ -81,9 +81,9 @@ export const FormBlockProvider = (props) => {
const createFlag =
(currentCollection.name === (collection?.name || collection) && !isEmptyRecord) || !currentCollection.name;
return (
(detailFlag || createFlag) && (
<BlockProvider {...props} block={'form'} params={{ ...props?.params, targetCollection: collection }}>
<InternalFormBlockProvider {...props} params={{ ...props?.params, targetCollection: collection }} />
(detailFlag || createFlag || isCusomeizeCreate) && (
<BlockProvider {...props} block={'form'}>
<InternalFormBlockProvider {...props} />
</BlockProvider>
)
);

View File

@ -3,10 +3,10 @@ import { FormContext, useField, useFieldSchema } from '@formily/react';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useCollectionManager } from '../collection-manager';
import { useFilterBlock } from '../filter-provider/FilterProvider';
import { FixedBlockWrapper, removeNullCondition, SchemaComponentOptions } from '../schema-component';
import { FixedBlockWrapper, SchemaComponentOptions, removeNullCondition } from '../schema-component';
import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider';
import { findFilterTargets } from './hooks';
import { mergeFilter } from './SharedFilterProvider';
import { findFilterTargets } from './hooks';
export const TableBlockContext = createContext<any>({});
export function getIdsWithChildren(nodes) {
@ -31,7 +31,7 @@ interface Props {
}
const InternalTableBlockProvider = (props: Props) => {
const { params, showIndex, dragSort, rowKey, childrenColumnName, fieldNames } = props;
const { params, showIndex, dragSort, rowKey, childrenColumnName, fieldNames, ...others } = props;
const field: any = useField();
const { resource, service } = useBlockRequestContext();
const fieldSchema = useFieldSchema();
@ -47,6 +47,7 @@ const InternalTableBlockProvider = (props: Props) => {
<FixedBlockWrapper>
<TableBlockContext.Provider
value={{
...others,
field,
service,
resource,
@ -97,7 +98,11 @@ export const TableBlockProvider = (props) => {
<SchemaComponentOptions scope={{ treeTable }}>
<FormContext.Provider value={form}>
<BlockProvider {...props} params={params} runWhenParamsChanged>
<InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={params} />
<InternalTableBlockProvider
{...props}
childrenColumnName={childrenColumnName}
params={params}
/>
</BlockProvider>
</FormContext.Provider>
</SchemaComponentOptions>
@ -117,7 +122,7 @@ export const useTableBlockProps = () => {
useEffect(() => {
if (!ctx?.service?.loading) {
field.value=[];
field.value = [];
field.value = ctx?.service?.data?.data;
field.data = field.data || {};
field.data.selectedRowKeys = ctx?.field?.data?.selectedRowKeys;

View File

@ -26,6 +26,11 @@ export const useCollection = () => {
const totalFields = unionBy(currentFields?.concat(inheritedFields), 'name').filter((v) => {
return !v.isForeignKey;
});
const foreignKeyFields = unionBy(currentFields?.concat(inheritedFields), 'name').filter((v) => {
return v.isForeignKey;
});
return {
...collection,
resource,
@ -47,5 +52,6 @@ export const useCollection = () => {
},
currentFields,
inheritedFields,
foreignKeyFields,
};
};

View File

@ -236,6 +236,34 @@ export const useCollectionManager = () => {
return getInheritChain(collectionName);
};
/**
* collectionName
* @param collectionName
* @returns
*/
const getInheritCollectionsChain = (collectionName: string) => {
const collectionsInheritChain = [collectionName];
const getInheritChain = (name: string) => {
const collection = getCollection(name);
if (collection) {
const { inherits } = collection;
if (inherits) {
for (let index = 0; index < inherits.length; index++) {
const collectionKey = inherits[index];
if (collectionsInheritChain.includes(collectionKey)) {
continue;
}
collectionsInheritChain.push(collectionKey);
getInheritChain(collectionKey);
}
}
}
return collectionsInheritChain;
};
return getInheritChain(collectionName);
};
return {
service,
interfaces,
@ -299,5 +327,6 @@ export const useCollectionManager = () => {
});
},
getAllCollectionsInheritChain,
getInheritCollectionsChain,
};
};

View File

@ -3,10 +3,20 @@ import { uniqBy } from 'lodash';
import React, { createContext, useEffect, useRef } from 'react';
import { useBlockRequestContext } from '../block-provider/BlockProvider';
import { SharedFilter, mergeFilter } from '../block-provider/SharedFilterProvider';
import { CollectionFieldOptions, useCollection } from '../collection-manager';
import { CollectionFieldOptions, FieldOptions, useCollection } from '../collection-manager';
import { removeNullCondition } from '../schema-component';
import { useAssociatedFields } from './utils';
export interface ForeignKeyField extends FieldOptions {
/** 外键字段所在的数据表的名称 */
collectionName: string;
isForeignKey: boolean;
key: string;
name: string;
parentKey: null | string;
reverseKey: null | string;
}
type Collection = ReturnType<typeof useCollection>;
export interface DataBlock {
@ -14,7 +24,7 @@ export interface DataBlock {
uid: string;
/** 用户自行设置的区块名称 */
title?: string;
/** 与当前区块相关的数据表信息 */
/** 与数据区块相关的数据表信息 */
collection: Collection;
/** 根据提供的参数执行该方法即可刷新数据区块的数据 */
doFilter: (params: any, params2?: any) => Promise<void>;
@ -22,10 +32,13 @@ export interface DataBlock {
clearFilter: (uid: string) => void;
/** 数据区块表中所有的关系字段 */
associatedFields?: CollectionFieldOptions[];
/** 通过右上角菜单设置的过滤条件 */
/** 数据区块表中所有的外键字段 */
foreignKeyFields?: ForeignKeyField[];
/** 数据区块已经存在的过滤条件(通过 `设置数据范围` 或者其它能设置筛选条件的功能) */
defaultFilter?: SharedFilter;
/** 数据区块用于请求数据的接口 */
service?: any;
/** 区块所对应的 DOM 容器 */
/** 数据区块所的 DOM 容器 */
dom: HTMLElement;
}
@ -73,6 +86,7 @@ export const FilterBlockRecord = ({
doFilter: service.runAsync,
collection,
associatedFields,
foreignKeyFields: collection.foreignKeyFields as ForeignKeyField[],
defaultFilter: params?.filter || {},
service,
dom: container.current,

View File

@ -0,0 +1,133 @@
import { getSupportFieldsByAssociation, getSupportFieldsByForeignKey } from '../utils';
describe('getSupportFieldsByAssociation', () => {
it('should return all associated fields matching the inherited collections chain', () => {
const block = {
associatedFields: [
{ id: 1, target: 'collection1', name: 'field1' },
{ id: 2, target: 'collection2', name: 'field2' },
{ id: 3, target: 'collection1', name: 'field3' },
],
};
const inheritCollectionsChain = ['collection1', 'collection2'];
const result = getSupportFieldsByAssociation(inheritCollectionsChain, block as any);
expect(result).toEqual([
{ id: 1, target: 'collection1', name: 'field1' },
{ id: 2, target: 'collection2', name: 'field2' },
{ id: 3, target: 'collection1', name: 'field3' },
]);
});
it('should return an empty array when there are no matching associated fields', () => {
const block = {
associatedFields: [
{ id: 1, target: 'collection1', name: 'field1' },
{ id: 2, target: 'collection2', name: 'field2' },
{ id: 3, target: 'collection1', name: 'field3' },
],
};
const inheritCollectionsChain = ['collection3', 'collection4'];
const result = getSupportFieldsByAssociation(inheritCollectionsChain, block as any);
expect(result).toEqual([]);
});
it('should return associated fields matching the inherited collections chain', () => {
const block = {
associatedFields: [
{ id: 1, target: 'collection1', name: 'field1' },
{ id: 2, target: 'collection2', name: 'field2' },
{ id: 3, target: 'collection1', name: 'field3' },
],
};
const inheritCollectionsChain = ['collection1'];
const result = getSupportFieldsByAssociation(inheritCollectionsChain, block as any);
expect(result).toEqual([
{ id: 1, target: 'collection1', name: 'field1' },
{ id: 3, target: 'collection1', name: 'field3' },
]);
});
});
describe('getSupportFieldsByForeignKey', () => {
it("should return all foreign key fields matching the filter block collection's foreign key properties", () => {
const filterBlockCollection = {
fields: [
{ id: 1, name: 'field1', foreignKey: 'fk1' },
{ id: 2, name: 'field2', foreignKey: 'fk2' },
{ id: 3, name: 'field3', foreignKey: 'fk3' },
],
};
const block = {
foreignKeyFields: [
{ id: 1, name: 'fk1', target: 'collection1' },
{ id: 2, name: 'fk2', target: 'collection2' },
{ id: 3, name: 'fk4', target: 'collection1' },
],
};
const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any);
expect(result).toEqual([
{ id: 1, name: 'fk1', target: 'collection1' },
{ id: 2, name: 'fk2', target: 'collection2' },
]);
});
it('should return an empty array when there are no matching foreign key fields', () => {
const filterBlockCollection = {
fields: [
{ id: 1, name: 'field1', foreignKey: 'fk1' },
{ id: 2, name: 'field2', foreignKey: 'fk2' },
{ id: 3, name: 'field3', foreignKey: 'fk3' },
],
};
const block = {
foreignKeyFields: [
{ id: 1, name: 'fk4', target: 'collection1' },
{ id: 2, name: 'fk5', target: 'collection2' },
{ id: 3, name: 'fk6', target: 'collection1' },
],
};
const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any);
expect(result).toEqual([]);
});
it("should return foreign key fields matching the filter block collection's foreign key properties", () => {
const filterBlockCollection = {
fields: [
{ id: 1, name: 'field1', foreignKey: 'fk1' },
{ id: 2, name: 'field2', foreignKey: 'fk2' },
{ id: 3, name: 'field3', foreignKey: 'fk3' },
],
};
const block = {
foreignKeyFields: [
{ id: 1, name: 'fk1', target: 'collection1' },
{ id: 2, name: 'fk2', target: 'collection2' },
{ id: 3, name: 'fk3', target: 'collection1' },
],
};
const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any);
expect(result).toEqual([
{ id: 1, name: 'fk1', target: 'collection1' },
{ id: 2, name: 'fk2', target: 'collection2' },
{ id: 3, name: 'fk3', target: 'collection1' },
]);
});
});

View File

@ -4,10 +4,16 @@ import _ from 'lodash';
import { useCallback, useEffect, useState } from 'react';
import { mergeFilter } from '../block-provider';
import { FilterTarget, findFilterTargets } from '../block-provider/hooks';
import { Collection, CollectionFieldOptions, FieldOptions, useCollection } from '../collection-manager';
import {
Collection,
CollectionFieldOptions,
FieldOptions,
useCollection,
useCollectionManager,
} from '../collection-manager';
import { removeNullCondition } from '../schema-component';
import { findFilterOperators } from '../schema-component/antd/form-item/SchemaSettingOptions';
import { useFilterBlock } from './FilterProvider';
import { DataBlock, useFilterBlock } from './FilterProvider';
export enum FilterBlockType {
FORM,
@ -16,6 +22,23 @@ export enum FilterBlockType {
COLLAPSE,
}
export const getSupportFieldsByAssociation = (inheritCollectionsChain: string[], block: DataBlock) => {
return block.associatedFields?.filter((field) =>
inheritCollectionsChain.some((collectionName) => collectionName === field.target),
);
};
export const getSupportFieldsByForeignKey = (
filterBlockCollection: ReturnType<typeof useCollection>,
block: DataBlock,
) => {
return block.foreignKeyFields?.filter((foreignKeyField) => {
return filterBlockCollection.fields.some(
(field) => field.type !== 'belongsTo' && field.foreignKey === foreignKeyField.name,
);
});
};
/**
*
* @param filterBlockType
@ -25,6 +48,7 @@ export const useSupportedBlocks = (filterBlockType: FilterBlockType) => {
const { getDataBlocks } = useFilterBlock();
const fieldSchema = useFieldSchema();
const collection = useCollection();
const { getAllCollectionsInheritChain } = useCollectionManager();
// Form 和 Collapse 仅支持同表的数据区块
if (filterBlockType === FilterBlockType.FORM || filterBlockType === FilterBlockType.COLLAPSE) {
@ -36,10 +60,14 @@ export const useSupportedBlocks = (filterBlockType: FilterBlockType) => {
// Table 和 Tree 支持同表或者关系表的数据区块
if (filterBlockType === FilterBlockType.TABLE || filterBlockType === FilterBlockType.TREE) {
return getDataBlocks().filter((block) => {
// 1. 同表
// 2. 关系字段
// 3. 外键字段
return (
fieldSchema['x-uid'] !== block.uid &&
(isSameCollection(block.collection, collection) ||
block.associatedFields.some((field) => field.target === collection.name))
getSupportFieldsByAssociation(getAllCollectionsInheritChain(collection.name), block)?.length ||
getSupportFieldsByForeignKey(collection, block)?.length)
);
});
}

View File

@ -707,5 +707,8 @@ export default {
"Current form": "Current form",
"Current object":"Current object",
"Linkage with form fields":"Linkage with form fields",
"Allow add new, update and delete actions":"Allow add new, update and delete actions"
"Allow add new, update and delete actions":"Allow add new, update and delete actions",
"Date display format":"Date display format",
"Assign data scope for the template":"Assign data scope for the template",
"Table selected records":"Table selected records"
};

View File

@ -0,0 +1,711 @@
export default {
"Display <1><0>10</0><1>20</1><2>50</2><3>100</3></1> items per page": "Afficher <1><0>10</0><1>20</1><2>50</2><3>100</3></1> éléments par page",
"Meet <1><0>All</0><1>Any</1></1> conditions in the group": "Remplir <1><0>Toutes</0><1>Quelconques</1></1> conditions dans le groupe",
"Open in<1><0>Modal</0><1>Drawer</1><2>Window</2></1>": "Ouvrir dans<1><0>Modale</0><1>Tiroir</1><2>Fenêtre</2></1>",
"{{count}} filter items": "{{count}} éléments filtrés",
"{{count}} more items": "{{count}} autres éléments",
"Total {{count}} items": "Total {{count}} éléments",
"Today": "Aujourd'hui",
"Yesterday": "Hier",
"Tomorrow": "Demain",
"Month": "Mois",
"Week": "Semaine",
"This week": "Cette semaine",
"This month": "Ce mois-ci",
"This year": "Cette année",
"Next year": "L'année prochaine",
"Last week": "La semaine dernière",
"Next week": "La semaine prochaine",
"Last month": "Le mois dernier",
"Next month": "Le mois prochain",
"Last quarter": "Le dernier trimestre",
"This quarter": "Ce trimestre",
"Next quarter": "Le prochain trimestre",
"Last year": "L'année dernière",
"Last 7 days": "Les 7 derniers jours",
"Last 30 days": "Les 30 derniers jours",
"Last 90 days": "Les 90 derniers jours",
"Next 7 days": "Les 7 prochains jours",
"Next 30 days": "Les 30 prochains jours",
"Next 90 days": "Les 90 prochains jours",
"Work week": "Semaine de travail",
"Day": "Jour",
"Agenda": "Agenda",
"Date": "Date",
"Time": "Heure",
"Event": "Événement",
"None": "Aucun",
"Unconnected": "Non connecté",
"System settings": "Paramètres système",
"System title": "Titre du système",
"Logo": "Logo",
"Add menu item": "Ajouter un élément de menu",
"Page": "Page",
"Name": "Nom",
"Icon": "Icône",
"Group": "Groupe",
"Link": "Lien",
"Save conditions": "Enregistrer les conditions",
"Edit menu item": "Modifier l'élément de menu",
"Move to": "Déplacer vers",
"Insert left": "Insérer à gauche",
"Insert right": "Insérer à droite",
"Insert inner": "Insérer à l'intérieur",
"Delete": "Supprimer",
"UI editor": "Éditeur d'interface utilisateur",
"Collection": "Collection",
"Collections & Fields": "Collections et champs",
"All collections":"Toutes les collections",
"Add category":"Ajouter une catégorie",
"Enable child collections":"Activer les collections enfants",
"Allow adding records to the current collection":"Autoriser l'ajout d'enregistrements à la collection actuelle",
"Delete category":"Supprimer la catégorie",
"Edit category":"Modifier la catégorie",
"Collection category":"Catégorie de collection",
"Collection template":"Modèle de collection",
"Sort":"Trier",
"Categories":"Catégories",
"Visible":"Visible",
"Read only":"Lecture seule",
"Easy reading":"Lecture facile",
"Hidden":"Caché",
"Hidden(reserved value)":"Caché (valeur réservée)",
"Not required":"Non requis",
"Value":"Valeur",
"Disabled":"Désactivé",
"Enabled":"Activé",
'On':'Actif',
'Off':'Inactif',
"Empty":"Vide",
"Linkage rule":"Règle de liaison",
"Linkage rules":"Règles de liaison",
"Condition":"Condition",
"Properties":"Propriétés",
"Add linkage rule":"Ajouter une règle de liaison",
"Add property":"Ajouter une propriété",
"Category name":"Nom de la catégorie",
"Roles & Permissions": "Rôles & permissions",
"Edit profile": "Modifier le profil",
"Change password": "Changer de mot de passe",
"Old password": "Ancien mot de passe",
"New password": "Nouveau mot de passe",
"Switch role": "Changer de rôle",
"Super admin": "Super administrateur",
"Language": "Langue",
"Allow sign up": "Autoriser l'inscription",
"Enable SMS authentication": "Activer l'authentification par SMS",
"Sign out": "Déconnexion",
"Cancel": "Annuler",
"Submit": "Envoyer",
"Close": "Fermer",
"Set the data scope": "Définir la portée des données",
"Data blocks": "Blocs de données",
"Filter blocks": "Blocs de filtre",
"Table": "Tableau",
"Table OID(Inheritance)": "Table OID(Héritage)",
"Form": "Formulaire",
"List": "Liste",
"Grid Card": "Grille de cartes",
"pixels": "pixels",
"Screen size": "Taille de l'écran",
"Display title": "Titre d'affichage",
'Set the count of columns displayed in a row': 'Définir le nombre de colonnes affichées par ligne',
'Column': 'Colonne',
'Phone device': 'Smartphone',
'Tablet device': 'Tablette',
'Desktop device': 'Ordinateur de bureau',
'Large screen device': 'Ordinateur à grand écran',
"Collapse": "Pliable",
"Select data source": "Sélectionner la source de données",
"Calendar": "Calendrier",
"Delete events": "Supprimer les événements",
"This event": "Cet événement",
"This and following events": "Cet événement et les suivants",
"All events": "Tous les événements",
"Delete this event?": "Supprimer cet événement ?",
"Delete Event": "Supprimer l'événement",
"Kanban": "Kanban",
"Gantt": "Gantt",
"Create gantt block": "Créer un bloc de Gantt",
"Progress field": "Champ de progression",
"Time scale": "Échelle de temps",
"Hour": "Heure",
"Quarter of day": "Quart de journée",
"Half of day": "Demi-journée",
"Year": "Année",
"QuarterYear": "Trimestre",
"Select grouping field": "Sélectionner le champ de regroupement",
"Media": "Média",
"Markdown": "Markdown",
"Wysiwyg": "Wysiwyg",
"Chart blocks": "Blocs de graphique",
"Column chart": "Graphique en colonnes",
"Bar chart": "Graphique à barres",
"Line chart": "Graphique linéaire",
"Pie chart": "Graphique en camembert",
"Area chart": "Graphique en aires",
"Other chart": "Autre graphique",
"Other blocks": "Autres blocs",
"In configuration": "En configuration",
"Chart title": "Titre du graphique",
"Chart type": "Type de graphique",
"Chart config": "Configuration du graphique",
"Templates": "Modèles",
"Select template": "Sélectionner un modèle",
"Action logs": "Logs d'action",
"Create template": "Créer un modèle",
"Edit markdown": "Modifier le markdown",
"Add block": "Ajouter un bloc",
"Add new": "Ajouter nouveau",
"Add record": "Ajouter un enregistrement",
'Add child': 'Ajouter un enfant',
'Collapse all': 'Réduire tout',
'Expand all': 'Développer tout',
'Expand/Collapse': 'Développer/Réduire',
'Default collapse': 'Développé/réduit par défaut',
"Tree table": "Tableau arborescent",
"Custom field display name": "Nom d'affichage personnalisé du champ",
"Display fields": "Afficher les champs de la collection",
"Edit record": "Modifier l'enregistrement",
"Delete menu item": "Supprimer l'élément de menu",
"Add page": "Ajouter une page",
"Add group": "Ajouter un groupe",
"Add link": "Ajouter un lien",
"Insert above": "Insérer au-dessus",
"Insert below": "Insérer en dessous",
"Save": "Enregistrer",
"Delete block": "Supprimer le bloc",
"Are you sure you want to delete it?": "Êtes-vous sûr de vouloir le supprimer ?",
"This is a demo text, **supports Markdown syntax**.": "Ceci est un texte de démonstration, **prend en charge la syntaxe Markdown.**",
"Filter": "Filtrer",
"Connect data blocks": "Connecter les blocs de données",
"Action type": "Type d'action",
"Actions": "Actions",
"Insert": "Insérer",
"Update": "Mettre à jour",
"View": "Voir",
"View record": "Voir l'enregistrement",
"Refresh": "Actualiser",
"Data changes": "Modifications des données",
"Field name": "Nom du champ",
"Before change": "Avant modification",
"After change": "Après modification",
"Delete record": "Supprimer l'enregistrement",
"Create collection": "Créer une collection",
"Collection display name": "Nom d'affichage de la collection",
"Collection name": "Nom de la collection",
"Inherits": "Hérite de",
"Generate ID field automatically": "Générer automatiquement le champ ID",
"Store the creation user of each record": "Enregistrer l'utilisateur de création de chaque enregistrement",
"Store the last update user of each record": "Enregistrer l'utilisateur de dernière mise à jour de chaque enregistrement",
"Store the creation time of each record": "Stocker l'heure de création de chaque enregistrement",
"Store the last update time of each record": "Stocker l'heure de dernière mise à jour de chaque enregistrement",
"More options": "Plus d'options",
"Records can be sorted": "Les enregistrements peuvent être triés",
"Calendar collection": "Collection de calendrier",
"General collection": "Collection générale",
"Connect to database view": "Connexion à la vue de la base de données",
"Source collections": "Collections source",
"Field source": "Source de champ",
"Preview": "Aperçu",
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Généré aléatoirement et peut être modifié. Prend en charge les lettres, les chiffres et les traits de soulignement, doit commencer par une lettre.",
"Edit": "Modifier",
"Edit collection": "Modifier la collection",
"Configure fields": "Configurer les champs",
"Configure columns": "Configurer les colonnes",
"Edit field": "Modifier le champ",
"Override": "Remplacer",
"Override field": "Remplacer le champ",
"Configure fields of {{title}}": "Configurer les champs de {{title}}",
"Association fields filter": "Filtre des champs d'association",
"PK & FK fields": "Champs PK & FK",
"Association fields": "Champs d'association",
"Choices fields": "Champs de choix",
"System fields": "Champs système",
"General fields": "Champs généraux",
"Inherited fields": "Champs hérités",
"Parent collection fields": "Champs de la collection parente",
"Basic": "Basique",
"Single line text": "Texte sur une seule ligne",
"Long text": "Texte long",
"Phone": "Téléphone",
"Email": "Email",
"Number": "Nombre",
"Integer": "Entier",
"Percent": "Pourcentage",
"Password": "Mot de passe",
"Advanced type": "Type avancé",
"Formula": "Formule",
"Formula description": "Calcule une valeur dans chaque enregistrement en fonction d'autres champs dans le même enregistrement.",
"Choices": "Choix",
"Checkbox": "Case à cocher",
"Single select": "Sélection unique",
"Multiple select": "Sélection multiple",
"Radio group": "Groupe de boutons radio",
"Checkbox group": "Groupe de cases à cocher",
"China region": "Région de Chine",
"Date & Time": "Date & heure",
"Datetime": "Date et heure",
"Relation": "Relation",
"Link to": "Lien vers",
"Link to description": "Utilisé pour créer rapidement des relations entre collections et compatible avec la plupart des scénarios courants. Convient à une utilisation sans développement. Lorsqu'il est présent en tant que champ, c'est une sélection déroulante utilisée pour sélectionner des enregistrements de la collection cible. Une fois créé, il génère simultanément les champs associés de la collection actuelle dans la collection cible.",
"Sub-table": "Sous-tableau",
"Sub-details": "Sous-détails",
"System info": "Informations système",
"Created at": "Créé le",
"Last updated at": "Dernière mise à jour le",
"Created by": "Créé par",
"Last updated by": "Dernière mise à jour par",
"Add field": "Ajouter un champ",
"Field display name": "Nom d'affichage du champ",
"Field type": "Type de champ",
"Field interface": "Interface du champ",
"Date format": "Format de date",
"Year/Month/Day": "Année/Mois/Jour",
"Year-Month-Day": "Année-Mois-Jour",
"Day/Month/Year": "Jour/Mois/Année",
"Show time": "Afficher l'heure",
"Time format": "Format d'heure",
"12 hour": "12 heures",
"24 hour": "24 heures",
"Relationship type": "Type de relation",
"Inverse relationship type": "Type de relation inverse",
"Source collection": "Collection source",
"Source key": "Clé source",
"Target collection": "Collection cible",
"Through collection": "Collection intermédiaire",
"Target key": "Clé cible",
"Foreign key": "Clé étrangère",
"One to one": "One to one",
"One to many": "One to many",
"Many to one": "Many to one",
"Many to many": "Many to many",
"Foreign key 1": "Clé étrangère 1",
"Foreign key 2": "Clé étrangère 2",
"One to one description": "Utilisé pour créer des relations un à un. Par exemple, un utilisateur a un profil.",
"One to many description": "Utilisé pour créer une relation un à plusieurs. Par exemple, un pays aura de nombreuses villes et une ville ne peut être que dans un pays. Lorsqu'il est présent en tant que champ, c'est un sous-tableau qui affiche les enregistrements de la collection associée. Lors de la création, un champ Many to one est automatiquement généré dans la collection associée.",
"Many to one description": "Utilisé pour créer des relations de plusieurs à un. Par exemple, une ville peut appartenir à un seul pays et un pays peut avoir de nombreuses villes. Lorsqu'il est présent en tant que champ, c'est une sélection déroulante utilisée pour sélectionner un enregistrement dans la collection associée. Une fois créé, un champ One to many est automatiquement généré dans la collection associée.",
"Many to many description": "Utilisé pour créer des relations de plusieurs à plusieurs. Par exemple, un étudiant aura de nombreux enseignants et un enseignant aura de nombreux étudiants. Lorsqu'il est présent en tant que champ, c'est une sélection déroulante utilisée pour sélectionner des enregistrements dans la collection associée.",
"Generated automatically if left blank": "Généré automatiquement si laissé vide",
"Display association fields": "Afficher les champs d'association",
"Display field title": "Afficher le titre du champ",
"Field component": "Composant de champ",
"Allow multiple": "Autoriser plusieurs",
"Quick upload": "Téléchargement rapide",
"Select file": "Sélectionner un fichier",
"Subtable": "Sous-tableau",
"Sub-form": "Sous-formulaire",
"Field mode": "Mode du champ",
"Allow add new data": "Autoriser l'ajout de nouvelles données",
"Record picker": "Sélecteur d'enregistrement",
"Toggles the subfield mode": "Activer/désactiver le mode sous-champ",
"Selector mode": "Mode sélecteur",
"Subtable mode": "Mode sous-table",
"Subform mode": "Mode sous-formulaire",
"Edit block title": "Modifier le titre du bloc",
"Block title": "Titre du bloc",
"Pattern": "Motif",
"Operator": "Opérateur",
"Editable": "Modifiable",
"Readonly": "Lecture seule",
"Easy-reading": "Lecture facile",
"Add filter": "Ajouter un filtre",
"Add filter group": "Ajouter un groupe de filtres",
"Comparision": "Comparaison",
"is": "est",
"is not": "n'est pas",
"contains": "contient",
"does not contain": "ne contient pas",
"starts with": "commence par",
"not starts with": "ne commence pas par",
"ends with": "se termine par",
"not ends with": "ne se termine pas par",
"is empty": "est vide",
"is not empty": "n'est pas vide",
"Edit chart": "Modifier le graphique",
"Add text": "Ajouter du texte",
"Filterable fields": "Champs filtrables",
"Edit button": "Modifier le bouton",
"Hide": "Masquer",
"Enable actions": "Activer les actions",
"Import": "Importer",
"Export": "Exporter",
"Customize": "Personnaliser",
"Custom": "Personnalisé",
"Function": "Fonction",
"Popup form": "Formulaire popup",
"Flexible popup": "Flexible popup",
"Configure actions": "Configurer les actions",
"Display order number": "Afficher numéro d'ordre",
"Enable drag and drop sorting": "Activer le tri par glisser-déposer",
"Triggered when the row is clicked": "Déclenché lorsque la ligne est cliquée",
"Add tab": "Ajouter un onglet",
"Disable tabs": "Désactiver les onglets",
"Details": "Détails",
"Edit tab": "Modifier l'onglet",
"Relationship blocks": "Blocs de relations",
"Select record": "Sélectionner un enregistrement",
"Display name": "Nom d'affichage",
"Select icon": "Sélectionner une icône",
"Custom column name": "Nom de colonne personnalisé",
"Edit description": "Modifier la description",
"Required": "Requis",
"Unique": "Unique",
"Label field": "Champ d'étiquette",
"Default is the ID field": "La valeur par défaut est le champ ID",
"Set default sorting rules": "Définir les règles de tri par défaut",
"Set validation rules": "Définir les règles de validation",
"Max length": "Longueur maximale",
"Min length": "Longueur minimale",
"Maximum": "Maximum",
"Minimum": "Minimum",
"Max length must greater than min length": "La longueur maximale doit être supérieure à la longueur minimale",
"Min length must less than max length": "La longueur minimale doit être inférieure à la longueur maximale",
"Maximum must greater than minimum": "La valeur maximale doit être supérieure à la valeur minimale",
"Minimum must less than maximum": "La valeur minimale doit être inférieure à la valeur maximale",
"Validation rule": "Règle de validation",
"Add validation rule": "Ajouter une règle de validation",
"Format": "Format",
"Regular expression": "Expression régulière",
"Error message": "Message d'erreur",
"Length": "Longueur",
"The field value cannot be greater than ": "La valeur du champ ne peut pas être supérieure à ",
"The field value cannot be less than ": "La valeur du champ ne peut pas être inférieure à ",
"The field value is not an integer number": "La valeur du champ n'est pas un nombre entier",
"Set default value": "Définir la valeur par défaut",
"Default value": "Valeur par défaut",
"is before": "est avant",
"is after": "est après",
"is on or after": "est le même jour ou après",
"is on or before": "est le même jour ou avant",
"is between": "est entre",
"Upload": "Télécharger",
"Select level": "Sélectionner un niveau",
"Province": "Province",
"City": "Ville",
"Area": "Région",
"Street": "Rue",
"Village": "Village",
"Must select to the last level": "Doit sélectionner jusqu'au dernier niveau",
"Move {{title}} to": "Déplacer {{title}} vers",
"Target position": "Position cible",
"After": "Après",
"Before": "Avant",
"Add {{type}} before \"{{title}}\"": "Ajouter {{type}} avant \"{{title}}\"",
"Add {{type}} after \"{{title}}\"": "Ajouter {{type}} après \"{{title}}\"",
"Add {{type}} in \"{{title}}\"": "Ajouter {{type}} dans \"{{title}}\"",
"Original name": "Nom d'origine",
"Custom name": "Nom personnalisé",
"Custom Title": "Titre personnalisé",
"Options": "Options",
"Option value": "Valeur de l'option",
"Option label": "Étiquette de l'option",
"Color": "Couleur",
"Add option": "Ajouter une option",
"Related collection": "Collection associée",
"Allow linking to multiple records": "Autoriser la liaison à plusieurs enregistrements",
"Allow uploading multiple files": "Autoriser le téléchargement de plusieurs fichiers",
"Configure calendar": "Configurer le calendrier",
"Title field": "Champ de titre",
"Custom title": "Titre personnalisé",
"Daily": "Quotidien",
"Weekly": "Hebdomadaire",
"Monthly": "Mensuel",
"Yearly": "Annuel",
"Repeats": "Répétitions",
"Show lunar": "Afficher le calendrier lunaire",
"Start date field": "Champ de date de début",
"End date field": "Champ de date de fin",
"Navigate": "Naviguer",
"Title": "Titre",
"Description": "Description",
"Select view": "Sélectionner la vue",
"Reset": "Réinitialiser",
"Importable fields": "Champs importables",
"Exportable fields": "Champs exportables",
"Saved successfully": "Enregistré avec succès",
"Nickname": "Pseudo",
"Sign in": "Se connecter",
"Sign in via account": "Se connecter via un compte",
"Sign in via phone": "Se connecter via un numéro de téléphone",
"Create an account": "Créer un compte",
"Sign up": "S'inscrire",
"Confirm password": "Confirmer le mot de passe",
"Log in with an existing account": "Se connecter avec un compte existant",
"Signed up successfully. It will jump to the login page.": "Inscription réussie. Vous allez être redirigé(e) vers la page de connexion.",
"Password mismatch": "Erreur de mot de passe",
"Users": "Utilisateurs",
"Verification code": "Code de vérification",
"Send code": "Envoyer le code",
"Retry after {{count}} seconds": "Réessayer après {{count}} secondes",
"Roles": "Rôles",
"Add role": "Ajouter un rôle",
"Role name": "Nom du rôle",
"Configure": "Configurer",
"Configure permissions": "Configurer les permissions",
"Edit role": "Modifier le rôle",
"Action permissions": "Permissions d'action",
"Menu permissions": "Permissions de menu",
"Menu item name": "Nom de l'élément de menu",
"Allow access": "Autoriser l'accès",
"Action name": "Nom de l'action",
"Allow action": "Autoriser l'action",
"Action scope": "Portée de l'action",
"Operate on new data": "Opérer sur de nouvelles données",
"Operate on existing data": "Opérer sur des données existantes",
"Yes": "Oui",
"No": "Non",
"Red": "Rouge",
"Magenta": "Magenta",
"Volcano": "Volcan",
"Orange": "Orange",
"Gold": "Or",
"Lime": "Citron vert",
"Green": "Vert",
"Cyan": "Cyan",
"Blue": "Bleu",
"Geek blue": "Bleu geek",
"Purple": "Violet",
"Default": "Par défaut",
"Add card": "Ajouter une carte",
"edit title": "modifier le titre",
"Turn pages": "Tourner les pages",
"Others": "Autres",
"Save as template": "Enregistrer en tant que modèle",
"Save as block template": "Enregistrer en tant que modèle de bloc",
"Block templates": "Modèles de bloc",
"Convert reference to duplicate": "Convertir la référence en doublon",
"Template name": "Nom du modèle",
"Block type": "Type de bloc",
"No blocks to connect": "Aucun bloc à connecter",
"Action column": "Colonne d'action",
"Records per page": "Enregistrements par page",
"(Fields only)": "(Champs uniquement)",
"Button title": "Titre du bouton",
"Button icon": "Icône du bouton",
"Submitted successfully": "Envoyé avec succès",
"Operation succeeded": "Opération réussie",
"Operation failed": "Échec de l'opération",
"Open mode": "Mode d'ouverture",
"Popup size": "Taille de la popup",
"Small": "Petite",
"Middle": "Moyenne",
"Large": "Grande",
"Menu item title": "Titre de l'élément de menu",
"Menu item icon": "Icône de l'élément de menu",
"Target": "Cible",
"Position": "Position",
"Insert before": "Insérer avant",
"Insert after": "Insérer après",
"UI Editor": "Éditeur d'interface utilisateur",
"ASC": "ASC",
"DESC": "DESC",
"Add sort field": "Ajouter un champ de tri",
"ID": "ID",
"Identifier for program usage. Support letters, numbers and underscores, must start with an letter.": "Identifiant pour une utilisation dans le programme. Prend en charge les lettres, les chiffres et les traits de soulignement et doit commencer par une lettre.",
"Drawer": "Tiroir",
"Dialog": "Dialogue",
"Delete action": "Supprimer l'action",
"Custom column title": "Titre de colonne personnalisé",
'Column title': 'Titre de colonne',
"Original title: ": "Titre original : ",
"Delete table column": "Supprimer la colonne de tableau",
"Skip required validation": "Ignorer la validation requise",
"Form values": "Valeurs du formulaire",
"Fields values": "Valeurs des champs",
'The field has been deleted': 'Le champ a été supprimé',
"When submitting the following fields, the saved values are": "Lors de l'envoi des champs suivants, les valeurs enregistrées sont",
"After successful submission": "Après un envoi réussi",
"Then": "Ensuite",
"Stay on current page": "Rester sur la page actuelle",
"Redirect to": "Rediriger vers",
"Save action": "Enregistrer l'action",
"Exists": "Existe",
"Add condition": "Ajouter une condition",
"Add condition group": "Ajouter un groupe de conditions",
"exists": "existe",
"not exists": "n'existe pas",
"=": "=",
"≠": "≠",
">": ">",
"≥": "≥",
"<": "<",
"≤": "≤",
"Role UID": "UID du rôle",
"Precision": "Précision",
"Formula mode": "Mode formule",
"Expression": "Expression",
"Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "Saisissez +, -, *, /, ( ) pour calculer, saisissez @ pour ouvrir les variables de champ.",
"Formula error.": "Erreur de formule.",
"Rich Text": "Texte enrichi",
"Junction collection": "Collection de jonction",
"Leave it blank, unless you need a custom intermediate table": "Laissez-le vide, sauf si vous avez besoin d'une table intermédiaire personnalisée",
"Fields": "Champs",
"Edit field title": "Modifier le titre du champ",
"Field title": "Titre du champ",
"Original field title: ": "Titre du champ d'origine : ",
"Edit tooltip": "Modifier l'info-bulle",
"Delete field": "Supprimer le champ",
"Select collection": "Sélectionner une collection",
"Blank block": "Bloc vierge",
"Duplicate template": "Dupliquer le modèle",
"Reference template": "Référencer le modèle",
"Create calendar block": "Créer un bloc de calendrier",
"Create kanban block": "Créer un bloc kanban",
"Grouping field": "Champ de regroupement",
"Single select and radio fields can be used as the grouping field": "Les champs de sélection unique et radio peuvent être utilisés comme champ de regroupement",
"Tab name": "Nom de l'onglet",
"Current record blocks": "Blocs d'enregistrement actuels",
"Popup message": "Message popup",
"Delete role": "Supprimer le rôle",
"Role display name": "Nom d'affichage du rôle",
"Default role": "Rôle par défaut",
"All collections use general action permissions by default; permission configured individually will override the default one.": "Toutes les collections utilisent les permissions d'action générales par défaut ; les permissions configurées individuellement remplaceront celles par défaut.",
"Allows configuration of the whole system, including UI, collections, permissions, etc.": "Permet de configurer l'ensemble du système, y compris l'interface utilisateur, les collections, les permissions, etc.",
"New menu items are allowed to be accessed by default.": "Les nouveaux éléments de menu peuvent être accessibles par défaut.",
"Global permissions": "Permissions globales",
"General permissions": "Permissions générales",
"Global action permissions": "Permissions d'action globales",
"General action permissions": "Permissions d'action générales",
"Plugin settings permissions": "Permissions de configuration des plugins",
"Allow to desgin pages": "Autoriser la conception des pages",
"Allow to manage plugins": "Autoriser la gestion des plugins",
"Allow to configure plugins": "Autoriser la configuration des plugins",
"Allows to configure interface": "Permet de configurer l'interface",
"Allows to install, activate, disable plugins": "Permet d'installer, d'activer, de désactiver des plugins",
"Allows to configure plugins": "Permet de configurer des plugins",
"Action display name": "Nom d'affichage de l'action",
"Allow": "Autoriser",
"Data scope": "Portée des données",
"Action on new records": "Action sur les nouveaux enregistrements",
"Action on existing records": "Action sur les enregistrements existants",
"All records": "Tous les enregistrements",
"Own records": "Ses propres enregistrements",
"Permission policy": "Politique de permission",
"Individual": "Individuelle",
"General": "Générale",
"Accessible": "Accessible",
"Configure permission": "Configurer la permission",
"Action permission": "Permission d'action",
"Field permission": "Permission de champ",
"Scope name": "Nom de la portée",
"Unsaved changes": "Modifications non enregistrées",
"Are you sure you don't want to save?": "Êtes-vous sûr de ne pas vouloir enregistrer ?",
"Dragging": "Déplacement",
"Popup": "Popup",
"Trigger workflow": "Déclencher un workflow",
"Request API": "Interroger une API",
"Assign field values": "Attribuer des valeurs de champ",
"Constant value": "Valeur constante",
"Dynamic value": "Valeur dynamique",
"Current user": "Utilisateur actuel",
"Current record": "Enregistrement actuel",
"Current time": "Heure actuelle",
"System variables": "Variables système",
"Date variables": "Variables de date",
"Popup close method": "Méthode de fermeture de la popup",
"Automatic close": "Fermeture automatique",
"Manually close": "Fermeture manuelle",
"After successful update": "Après une mise à jour réussie",
"Save record": "Enregistrer",
"Updated successfully": "Mis à jour avec succès",
"After successful save": "Après un enregistrement réussie",
"After clicking the custom button, the following field values will be assigned according to the following form.": "Après avoir cliqué sur le bouton personnalisé, les valeurs de champ suivantes seront attribuées selon le formulaire suivant.",
"After clicking the custom button, the following fields of the current record will be saved according to the following form.": "Après avoir cliqué sur le bouton personnalisé, les champs suivants de l'enregistrement actuel seront sauvegardés selon le formulaire suivant.",
"Button background color": "Couleur d'arrière-plan du bouton",
"Highlight": "Mise en évidence",
"Danger red": "Rouge danger",
"Custom request": "Requête personnalisée",
"Request settings": "Paramètres de la requête",
"Request URL": "URL de la requête",
"Request method": "Méthode de requête",
"Request query parameters": "Paramètres de requête",
"Request headers": "En-têtes de requête",
"Request body": "Corps de la requête",
"Request success": "Succès de la requête",
"Invalid JSON format": "Format JSON invalide",
"After successful request": "Après une requête réussie",
"Add exportable field": "Ajouter un champ exportable",
"Audit logs": "Journaux d'audit",
"Record ID": "ID de l'enregistrement",
"User": "Utilisateur",
"Field": "Champ",
"Select": "Sélectionner",
"Select field": "Sélectionner un champ",
"Field value changes": "Changements de valeur du champ",
"One to one (has one)": "One to one (has one)",
"One to one (belongs to)": "One to one (belongs to)",
"Use the same time zone (GMT) for all users": "Utiliser le même fuseau horaire (GMT) pour tous les utilisateurs",
"Province/city/area name": "Nom de la province/ville/région",
"Enabled languages": "Langues activées",
"View all plugins": "Voir tous les plugins",
"Print": "Imprimer",
"Done": "Terminé",
"Sign up successfully, and automatically jump to the sign in page": "Inscription réussie, et redirection automatique vers la page de connexion",
"File manager": "Gestionnaire de fichiers",
"ACL": "ACL",
"Collection manager": "Gestionnaire de collection",
"Plugin manager": "Gestionnaire de plugins",
"Local": "Local",
"Built-in": "Intégré",
"Marketplace": "Place de marché",
"Coming soon...": "Bientôt...",
"All plugin settings": "Tous les paramètres de plugin",
"Bookmark": "Signet",
"Manage all settings": "Gérer tous les paramètres",
"Create inverse field in the target collection": "Créer un champ inverse dans la collection cible",
"Inverse field name": "Nom du champ inverse",
"Inverse field display name": "Nom d'affichage du champ inverse",
"Bulk update": "Mise à jour en masse",
"After successful bulk update": "Après une mise à jour en masse réussie",
"Bulk edit": "Édition en masse",
"Data will be updated": "Les données seront mises à jour",
"Selected": "Sélectionné",
"All": "Tous",
"Update selected data?": "Mettre à jour les données sélectionnées ?",
"Update all data?": "Mettre à jour toutes les données ?",
"Remains the same": "Reste inchangé",
"Changed to": "Modifié en",
"Clear": "Effacer",
"Add attach": "Ajouter une pièce jointe",
"Please select the records to be updated": "Veuillez sélectionner les enregistrements à mettre à jour",
"Selector": "Sélecteur",
"Inner": "Interne",
"Search and select collection": "Rechercher et sélectionner une collection",
"Please fill in the iframe URL": "Veuillez remplir l'URL de l'iframe",
"Fix block": "Fixer le bloc",
"Plugin name": "Nom du plugin",
"Plugin tab name": "Nom de l'onglet du plugin",
"AutoGenId": "Champ d'ID généré automatiquement",
"CreatedBy": "Enregistrer l'utilisateur qui a créé une ligne",
"UpdatedBy": "Enregistrer le dernier utilisateur ayant effectué une mise à jour de la ligne",
"CreatedAt": "Enregistrer l'heure de création d'une ligne",
"UpdatedAt": "Enregistrer le dernier utilisateur ayant effectué une mise à jour de la ligne",
"Column width": "Largeur de colonne",
"Sortable": "Triable",
"Enable link": "Activer le lien",
"This is likely a NocoBase internals bug. Please open an issue at <1>here</1>": "Ceci est probablement un bogue interne de NocoBase. Veuillez ouvrir un problème <1>ici</1>",
"Render Failed": "Échec du rendu",
"Feedback": "Commentaires",
"Try again": "Réessayer",
"Data template": "Modèle de données",
"Duplicate":"Dupliquer",
"Duplicating":"Duplication",
"Duplicate mode":"Mode de duplication",
"Quick duplicate":"Duplication rapide",
"Duplicate and continue":"Dupliquer et continuer",
"Please configure the duplicate fields":"Veuillez configurer les champs de duplication",
"Add":"Ajouter",
"Add new mode":"Mode d'ajout",
"Quick add":"Ajout rapide",
"Modal add":"Ajout modal",
"Save mode":"Mode d'enregistrement",
"First or create":"D'abord ou créer",
"Update or create":"Mettre à jour ou créer",
"Find by the following fields":"Trouver par les champs suivants",
"Create":"Créer",
"Current form": "Formulaire actuel",
"Current object":"Objet actuel",
"Linkage with form fields":"Lien avec les champs de formulaire",
"Allow add new, update and delete actions":"Autoriser les actions d'ajout, de mise à jour et de suppression"
};

View File

@ -618,5 +618,7 @@ export default {
"Current form":"現在のフォーム",
"Current object":"現在のオブジェクト",
"Linkage with form fields":"フォームデータから連動",
"Allow add new, update and delete actions":"削除変更操作の許可"
"Allow add new, update and delete actions":"削除変更操作の許可",
"Date display format":"日付表示形式",
"Assign data scope for the template":"テンプレートのデータ範囲の指定",
}

View File

@ -792,5 +792,8 @@ export default {
"Copy into the form and continue to fill in": "复制到表单并继续填写",
"Linkage with form fields":"从表单字段联动",
"Failed to load plugin": "插件加载失败",
"Allow add new, update and delete actions":"允许增删改操作"
"Allow add new, update and delete actions":"允许增删改操作",
"Date display format":"日期显示格式",
"Assign data scope for the template":"为模板指定数据范围",
"Table selected records":"表格中选中的记录"
}

View File

@ -59,6 +59,7 @@ export type UseComponentStyleResult = {
wrapSSR: ReturnType<typeof useStyleRegister>;
hashId: string;
componentCls: string;
rootPrefixCls: string;
};
export const genStyleHook = <ComponentName extends OverrideComponent>(
@ -99,6 +100,7 @@ export const genStyleHook = <ComponentName extends OverrideComponent>(
),
hashId,
componentCls: prefixCls,
rootPrefixCls,
};
};
};

View File

@ -580,7 +580,7 @@ export const ActionDesigner = (props) => {
const { name } = useCollection();
const { getChildrenCollections } = useCollectionManager();
const isAction = useLinkageAction();
const isPopupAction = ['create', 'update', 'view', 'customize:popup', 'duplicate'].includes(
const isPopupAction = ['create', 'update', 'view', 'customize:popup', 'duplicate','customize:create'].includes(
fieldSchema['x-action'] || '',
);
const isUpdateModePopupAction = ['customize:bulkUpdate', 'customize:bulkEdit'].includes(fieldSchema['x-action']);

View File

@ -5,50 +5,42 @@ const useStyles = genStyleHook('nb-action', (token) => {
return {
[componentCls]: {
'.renderButton': {
position: 'relative',
'&:hover': { '> .general-schema-designer': { display: 'block' } },
'&.nb-action-link': {
'> .general-schema-designer': {
top: '-10px',
bottom: '-10px',
left: '-10px',
right: '-10px',
},
},
position: 'relative',
'&:hover': { '> .general-schema-designer': { display: 'block' } },
'&.nb-action-link': {
'> .general-schema-designer': {
position: 'absolute',
zIndex: 999,
top: '0',
bottom: '0',
left: '0',
right: '0',
display: 'none',
background: 'var(--colorBgSettingsHover)',
border: '0',
pointerEvents: 'none',
'> .general-schema-designer-icons': {
position: 'absolute',
right: '2px',
top: '2px',
lineHeight: '16px',
pointerEvents: 'all',
'.ant-space-item': {
backgroundColor: token.colorSettings,
color: '#fff',
lineHeight: '16px',
width: '16px',
paddingLeft: '1px',
alignSelf: 'stretch',
},
},
top: '-10px',
bottom: '-10px',
left: '-10px',
right: '-10px',
},
},
'.popover': {
display: 'flex',
justifyContent: 'flex-end',
width: '100%',
'> .general-schema-designer': {
position: 'absolute',
zIndex: 999,
top: '0',
bottom: '0',
left: '0',
right: '0',
display: 'none',
background: 'var(--colorBgSettingsHover)',
border: '0',
pointerEvents: 'none',
'> .general-schema-designer-icons': {
position: 'absolute',
right: '2px',
top: '2px',
lineHeight: '16px',
pointerEvents: 'all',
'.ant-space-item': {
backgroundColor: token.colorSettings,
color: '#fff',
lineHeight: '16px',
width: '16px',
paddingLeft: '1px',
alignSelf: 'stretch',
},
},
},
},
};

View File

@ -1,4 +1,5 @@
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
import { lodash } from '@nocobase/utils';
import { App, Button, Popover } from 'antd';
import classnames from 'classnames';
import React, { useEffect, useState } from 'react';
@ -21,7 +22,6 @@ import { ActionContextProvider } from './context';
import { useA } from './hooks';
import { ComposedAction } from './types';
import { linkageAction } from './utils';
import { lodash } from '@nocobase/utils';
export const Action: ComposedAction = observer(
(props: any) => {
@ -105,7 +105,7 @@ export const Action: ComposedAction = observer(
}
}}
component={tarComponent || Button}
className={classnames('renderButton', className)}
className={classnames(componentCls, hashId, className)}
type={props.type === 'danger' ? undefined : props.type}
>
{actionTitle}
@ -115,24 +115,22 @@ export const Action: ComposedAction = observer(
};
return wrapSSR(
<div className={`${componentCls} ${hashId}`}>
<ActionContextProvider
button={renderButton()}
visible={visible}
setVisible={setVisible}
formValueChanged={formValueChanged}
setFormValueChanged={setFormValueChanged}
openMode={openMode}
openSize={openSize}
containerRefKey={containerRefKey}
fieldSchema={fieldSchema}
>
{popover && <RecursionField basePath={field.address} onlyRenderProperties schema={fieldSchema} />}
{!popover && renderButton()}
{!popover && <div onClick={(e) => e.stopPropagation()}>{props.children}</div>}
{element}
</ActionContextProvider>
</div>,
<ActionContextProvider
button={renderButton()}
visible={visible}
setVisible={setVisible}
formValueChanged={formValueChanged}
setFormValueChanged={setFormValueChanged}
openMode={openMode}
openSize={openSize}
containerRefKey={containerRefKey}
fieldSchema={fieldSchema}
>
{popover && <RecursionField basePath={field.address} onlyRenderProperties schema={fieldSchema} />}
{!popover && renderButton()}
{!popover && <div onClick={(e) => e.stopPropagation()}>{props.children}</div>}
{element}
</ActionContextProvider>,
);
},
{ displayName: 'Action' },
@ -160,7 +158,17 @@ Action.Popover = observer(
Action.Popover.Footer = observer(
(props) => {
return <div className="popover">{props.children}</div>;
return (
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
width: '100%',
}}
>
{props.children}
</div>
);
},
{ displayName: 'Action.Popover.Footer' },
);

View File

@ -4,7 +4,8 @@ import { Space, message } from 'antd';
import { isFunction } from 'mathjs';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { RecordProvider, useAPIClient, useCollectionManager } from '../../../';
import { RecordProvider, useAPIClient } from '../../../';
import { isVariable } from '../../common/utils/uitls';
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
import useServiceOptions, { useAssociationFieldContext } from './hooks';
@ -17,10 +18,10 @@ const InternalAssociationSelect = observer((props: AssociationSelectProps) => {
const { objectValue = true } = props;
const field: any = useField();
const fieldSchema = useFieldSchema();
const { getCollection } = useCollectionManager();
const service = useServiceOptions(props);
const { options: collectionField } = useAssociationFieldContext();
const value = Array.isArray(props.value) ? props.value.filter(Boolean) : props.value;
const initValue = isVariable(props.value) ? undefined : props.value;
const value = Array.isArray(initValue) ? initValue.filter(Boolean) : initValue;
const addMode = fieldSchema['x-component-props']?.addMode;
const isAllowAddNew = fieldSchema['x-add-new'];
const { t } = useTranslation();
@ -28,7 +29,6 @@ const InternalAssociationSelect = observer((props: AssociationSelectProps) => {
const form = useForm();
const api = useAPIClient();
const resource = api.resource(collectionField.target);
const targetCollection = getCollection(collectionField.target);
const handleCreateAction = async (props) => {
const { search: value, callBack } = props;
const {

View File

@ -10,6 +10,7 @@ import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { AssociationFieldContext } from './context';
import { useAssociationFieldContext } from './hooks';
import { RecordProvider, useRecord } from '../../../record-provider';
export const Nester = (props) => {
const { options } = useContext(AssociationFieldContext);
@ -92,7 +93,9 @@ const ToManyNester = observer(
</Tooltip>
)}
</div>
<RecursionField onlyRenderProperties basePath={field.address.concat(index)} schema={fieldSchema} />
<RecordProvider record={value}>
<RecursionField onlyRenderProperties basePath={field.address.concat(index)} schema={fieldSchema} />
</RecordProvider>
<Divider />
</React.Fragment>
);

View File

@ -97,6 +97,7 @@ export const SubTable: any = observer(
</Button>
)
}
isSubTable={true}
/>
</div>
);

View File

@ -1,7 +1,7 @@
import { css, cx } from '@emotion/css';
import { ArrayCollapse, ArrayItems, FormLayout, FormItem as Item } from '@formily/antd-v5';
import { ArrayCollapse, ArrayItems, FormItem as Item, FormLayout } from '@formily/antd-v5';
import { Field } from '@formily/core';
import { ISchema, Schema, observer, useField, useFieldSchema } from '@formily/react';
import { ISchema, observer, Schema, useField, useFieldSchema } from '@formily/react';
import { dayjs } from '@nocobase/utils/client';
import { Select } from 'antd';
import _ from 'lodash';
@ -20,11 +20,9 @@ import {
} from '../../../collection-manager';
import { isTitleField } from '../../../collection-manager/Configuration/CollectionFields';
import { GeneralSchemaItems } from '../../../schema-items/GeneralSchemaItems';
import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled, isShowDefaultValue } from '../../../schema-settings';
import { VariableInput } from '../../../schema-settings/VariableInput/VariableInput';
import { GeneralSchemaDesigner, isPatternDisabled, isShowDefaultValue, SchemaSettings } from '../../../schema-settings';
import { useIsShowMultipleSwitch } from '../../../schema-settings/hooks/useIsShowMultipleSwitch';
import { isVariable, parseVariables, useVariablesCtx } from '../../common/utils/uitls';
import { SchemaComponent } from '../../core';
import { useCompile, useDesignable, useFieldModeOptions } from '../../hooks';
import { BlockItem } from '../block-item';
import { removeNullCondition } from '../filter';
@ -33,29 +31,72 @@ import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
import { FilterFormDesigner } from './FormItem.FilterFormDesigner';
import { useEnsureOperatorsValid } from './SchemaSettingOptions';
const defaultInputStyle = css`
& > .nb-form-item {
flex: 1;
}
`;
export const findColumnFieldSchema = (fieldSchema, getCollectionJoinField) => {
const childsSchema = new Set();
const getAssociationAppends = (schema) => {
schema.reduceProperties((_, s) => {
const collectionfield = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field']);
const isAssociationField = collectionfield && ['belongsTo'].includes(collectionfield.type);
if (collectionfield && isAssociationField && s.default?.includes?.('$context')) {
childsSchema.add(JSON.stringify({ name: s.name, default: s.default }));
} else {
getAssociationAppends(s);
}
}, []);
};
getAssociationAppends(fieldSchema);
return [...childsSchema];
};
export const FormItem: any = observer(
(props: any) => {
useEnsureOperatorsValid();
const field = useField<Field>();
const ctx = useBlockRequestContext();
const schema = useFieldSchema();
const variablesCtx = useVariablesCtx();
const { getCollectionJoinField } = useCollectionManager();
const collectionField = getCollectionJoinField(schema['x-collection-field']);
useEffect(() => {
if (ctx?.block === 'form') {
ctx.field.data = ctx.field.data || {};
ctx.field.data.activeFields = ctx.field.data.activeFields || new Set();
ctx.field.data.activeFields.add(schema.name);
// 如果默认值是一个变量,则需要解析之后再显示出来
if (isVariable(schema?.default)) {
if (isVariable(schema?.default) && !schema?.default.includes('$context')) {
field.setInitialValue?.(parseVariables(schema.default, variablesCtx));
} else if (
isVariable(schema?.default) &&
schema?.default?.includes('$context') &&
collectionField?.interface === 'm2m'
) {
// 直接对多
const contextData = parseVariables('{{$context}}', variablesCtx);
let iniValues = [];
contextData?.map((v) => {
const data = parseVariables(schema.default, { $context: v });
iniValues = iniValues.concat(data);
});
field.setInitialValue?.(_.uniqBy(iniValues, 'id'));
} else if (
collectionField?.interface === 'o2m' &&
['SubTable', 'Nester'].includes(schema?.['x-component-props']?.['mode']) // 间接对多
) {
const childrenFieldWithDefault = findColumnFieldSchema(schema, getCollectionJoinField);
// 子表格/子表单中找出所有belongsTo字段的上下文默认值
if (childrenFieldWithDefault.length > 0) {
const contextData = parseVariables('{{$context}}', variablesCtx);
const initValues = contextData?.map((v) => {
const obj = {};
childrenFieldWithDefault.forEach((s: any) => {
const child = JSON.parse(s);
obj[child.name] = parseVariables(child.default, { $context: v });
});
return obj;
});
field.setInitialValue?.(initValues);
}
}
}
}, []);
@ -109,7 +150,6 @@ FormItem.Designer = function Designer() {
const { t } = useTranslation();
const { dn, refresh, insertAdjacent } = useDesignable();
const compile = useCompile();
const variablesCtx = useVariablesCtx();
const IsShowMultipleSwitch = useIsShowMultipleSwitch();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
if (collectionField?.target) {
@ -168,13 +208,12 @@ FormItem.Designer = function Designer() {
direction: 'asc',
};
});
const fieldSchemaWithoutRequired = _.omit(fieldSchema, 'required');
const isSubFormMode = fieldSchema['x-component-props']?.mode === 'Nester';
const isPickerMode = fieldSchema['x-component-props']?.mode === 'Picker';
const showFieldMode = isAssociationField && fieldModeOptions && !isTableField;
const showModeSelect = showFieldMode && isPickerMode;
const isDateField = ['datetime', 'createdAt', 'updatedAt'].includes(collectionField?.interface);
return (
<GeneralSchemaDesigner>
<GeneralSchemaItems />
@ -343,76 +382,7 @@ FormItem.Designer = function Designer() {
{form &&
!form?.readPretty &&
isShowDefaultValue(collectionField, getInterface) &&
!isPatternDisabled(fieldSchema) && (
<SchemaSettings.ModalItem
title={t('Set default value')}
components={{ ArrayCollapse, FormLayout, VariableInput }}
width={800}
schema={
{
type: 'object',
title: t('Set default value'),
properties: {
default: {
...(fieldSchemaWithoutRequired || {}),
'x-decorator': 'FormItem',
'x-component': 'VariableInput',
'x-component-props': {
...(fieldSchema?.['x-component-props'] || {}),
collectionField,
targetField,
collectionName: collectionField?.collectionName,
schema: collectionField?.uiSchema,
className: defaultInputStyle,
renderSchemaComponent: function Com(props) {
const s = _.cloneDeep(fieldSchemaWithoutRequired) || ({} as Schema);
s.title = '';
s['x-read-pretty'] = false;
s['x-disabled'] = false;
return (
<SchemaComponent
schema={{
...(s || {}),
'x-component-props': {
...s['x-component-props'],
onChange: props.onChange,
value: props.value,
defaultValue: getFieldDefaultValue(s, collectionField),
style: {
width: '100%',
verticalAlign: 'top',
},
},
}}
/>
);
},
},
name: 'default',
title: t('Default value'),
default: getFieldDefaultValue(fieldSchema, collectionField),
},
},
} as ISchema
}
onSubmit={(v) => {
const schema: ISchema = {
['x-uid']: fieldSchema['x-uid'],
};
if (field.value !== v.default) {
field.value = parseVariables(v.default, variablesCtx);
}
fieldSchema.default = v.default;
schema.default = v.default;
dn.emit('patch', {
schema,
});
refresh();
}}
/>
)}
!isPatternDisabled(fieldSchema) && <SchemaSettings.DefaultValue />}
{isSelectFieldMode && !field.readPretty && (
<SchemaSettings.ModalItem
title={t('Set the data scope')}
@ -840,6 +810,7 @@ FormItem.Designer = function Designer() {
}}
/>
)}
{isDateField && <SchemaSettings.DataFormat fieldSchema={fieldSchema} />}
{collectionField && <SchemaSettings.Divider />}
<SchemaSettings.Remove
key="remove"

View File

@ -7,7 +7,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { useFormBlockContext } from '../../../block-provider';
import { useCollection, useCollectionManager } from '../../../collection-manager';
import { SchemaSettings, isPatternDisabled } from '../../../schema-settings';
import { isPatternDisabled, SchemaSettings } from '../../../schema-settings';
import { useCompile, useDesignable, useFieldModeOptions } from '../../hooks';
import { useOperatorList } from '../filter/useOperators';
import { isFileCollection } from './FormItem';

View File

@ -1,14 +1,16 @@
import { useFieldSchema } from '@formily/react';
import { error, forEach } from '@nocobase/utils/client';
import { Select } from 'antd';
import { Select, Space } from 'antd';
import _ from 'lodash';
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAPIClient } from '../../../api-client';
import { findFormBlock } from '../../../block-provider';
import { useCollectionManager } from '../../../collection-manager';
import { useDuplicatefieldsContext } from '../../../schema-initializer/components';
import { compatibleDataId } from '../../../schema-settings/DataTemplates/FormDataTemplates';
import { useToken } from '../__builtins__';
import { RemoteSelect } from '../remote-select';
export interface ITemplate {
config?: {
@ -23,9 +25,11 @@ export interface ITemplate {
key: string;
title: string;
collection: string;
dataId: number;
dataId?: number;
fields: string[];
default?: boolean;
dataScope?: object;
titleField?: string;
}[];
/** 是否在 Form 区块显示模板选择器 */
display: boolean;
@ -63,49 +67,38 @@ const useDataTemplates = () => {
key: 'none',
title: t('None'),
},
].concat(items.map<any>((item, i) => ({ key: i, ...item })));
].concat(
items.map<any>((t, i) => ({
key: i,
...t,
isLeaf: t.dataId !== null && t.dataId !== undefined,
titleCollectionField: t?.titleField && getCollectionJoinField(`${t.collection}.${t.titleField}`),
})),
);
const defaultTemplate = items.find((item) => item.default);
return {
templates,
display,
defaultTemplate,
enabled: items.length > 0 && items.every((item) => item.dataId !== undefined),
enabled: items.length > 0 && items.every((item) => item.dataId || item.dataScope),
};
};
function filterReferences(obj) {
const filteredObj = {};
for (const key in obj) {
if (typeof obj[key] !== 'object') {
filteredObj[key] = obj[key];
}
}
return filteredObj;
}
export const Templates = ({ style = {}, form }) => {
const { token } = useToken();
const { templates, display, enabled, defaultTemplate } = useDataTemplates();
const [value, setValue] = React.useState(defaultTemplate?.key || 'none');
const { getCollectionJoinField } = useCollectionManager();
const templateOptions = compatibleDataId(templates);
const [targetTemplate, setTargetTemplate] = useState(defaultTemplate?.key || 'none');
const [targetTemplateData, setTemplateData] = useState(null);
const api = useAPIClient();
const { t } = useTranslation();
useEffect(() => {
if (enabled && defaultTemplate) {
form.__template = true;
fetchTemplateData(api, defaultTemplate, t)
.then((data) => {
if (form && data) {
forEach(data, (value, key) => {
if (value) {
form.values[key] = value;
}
});
}
return data;
})
.catch((err) => {
console.error(err);
});
if (defaultTemplate.key === 'duplicate') {
handleTemplateDataChange(defaultTemplate.dataId, defaultTemplate);
}
}
}, []);
@ -122,46 +115,69 @@ export const Templates = ({ style = {}, form }) => {
return { fontSize: token.fontSize, fontWeight: 'bold', whiteSpace: 'nowrap', marginRight: token.marginXS };
}, [token.fontSize, token.marginXS]);
const handleChange = useCallback(async (value, option) => {
setValue(value);
if (option.key !== 'none') {
fetchTemplateData(api, option, t)
.then((data) => {
if (form && data) {
// 切换之前先把之前的数据清空
form.reset();
form.__template = true;
const handleTemplateChange = useCallback(async (value, option) => {
setTargetTemplate(value);
setTemplateData(null);
form?.reset();
}, []);
forEach(data, (value, key) => {
if (value) {
form.values[key] = value;
}
});
}
return data;
})
.catch((err) => {
console.error(err);
});
} else {
form?.reset();
}
const handleTemplateDataChange: any = useCallback(async (value, option) => {
const template = { ...option, dataId: value };
setTemplateData(option);
fetchTemplateData(api, template, t)
.then((data) => {
if (form && data) {
// 切换之前先把之前的数据清空
form.reset();
form.__template = true;
forEach(data, (value, key) => {
if (value) {
form.values[key] = value;
}
});
}
return data;
})
.catch((err) => {
console.error(err);
});
}, []);
if (!enabled || !display) {
return null;
}
const template = templateOptions?.find((v) => v.key === targetTemplate);
return (
<div style={wrapperStyle}>
<label style={labelStyle}>{t('Data template')}: </label>
<Select
popupMatchSelectWidth={false}
options={templates}
fieldNames={{ label: 'title', value: 'key' }}
value={value}
onChange={handleChange}
/>
<Space wrap>
<label style={labelStyle}>{t('Data template')}: </label>
<Select
popupMatchSelectWidth={false}
options={templateOptions}
fieldNames={{ label: 'title', value: 'key' }}
value={targetTemplate}
onChange={handleTemplateChange}
/>
{targetTemplate !== 'none' && (
<RemoteSelect
style={{ width: 220 }}
fieldNames={{ label: template.titleField, value: 'id' }}
target={template?.collection}
value={targetTemplateData}
objectValue
service={{
resource: template?.collection,
params: {
filter: template?.dataScope,
},
}}
onChange={(value) => handleTemplateDataChange(value.id, { ...value, ...template })}
targetField={getCollectionJoinField(`${template?.collection}.${template.titleField}`)}
/>
)}
</Space>
</div>
);
};
@ -175,7 +191,7 @@ function findDataTemplates(fieldSchema): ITemplate {
}
export async function fetchTemplateData(api, template: { collection: string; dataId: number; fields: string[] }, t) {
if (template.fields.length === 0) {
if (template.fields.length === 0 || !template.dataId) {
return;
}
return api

View File

@ -1,13 +1,13 @@
import { ISchema, useField, useFieldSchema } from '@formily/react';
import React from 'react';
import { set } from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useCollectionManager, useCollectionFilterOptions } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled } from '../../../schema-settings';
import { useCollectionFilterOptions, useCollectionManager } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled, isShowDefaultValue } from '../../../schema-settings';
import { useCompile, useDesignable } from '../../hooks';
import { useAssociationFieldContext } from '../association-field/hooks';
import { FilterDynamicComponent } from './FilterDynamicComponent';
import { removeNullCondition } from '../filter';
import { FilterDynamicComponent } from './FilterDynamicComponent';
const useLabelFields = (collectionName?: any) => {
// 需要在组件顶层调用
@ -44,6 +44,7 @@ export const TableColumnDesigner = (props) => {
const { currentMode, field: tableField } = useAssociationFieldContext();
const defaultFilter = fieldSchema?.['x-component-props']?.service?.params?.filter || {};
const dataSource = useCollectionFilterOptions(collectionField?.target);
const isDateField = ['datetime', 'createdAt', 'updatedAt'].includes(collectionField?.interface);
let readOnlyMode = 'editable';
if (fieldSchema['x-disabled'] === true) {
readOnlyMode = 'readonly';
@ -323,6 +324,11 @@ export const TableColumnDesigner = (props) => {
}}
/>
)}
{isDateField && <SchemaSettings.DataFormat fieldSchema={fieldSchema} />}
{isSubTableColumn && !field?.readPretty && isShowDefaultValue(collectionField, getInterface) && (
<SchemaSettings.DefaultValue fieldSchema={fieldSchema} />
)}
<SchemaSettings.Divider />
<SchemaSettings.Remove
removeParentsIfNoChildren={!isSubTableColumn}

View File

@ -37,7 +37,7 @@ const useTableColumns = (props) => {
const { exists, render } = useSchemaInitializer(schema['x-initializer']);
const columns = schema
.reduceProperties((buf, s) => {
if (isColumnComponent(s) && schemaInWhitelist(Object.values(s.properties || {}).pop())) {
if (isColumnComponent(s) && schemaInWhitelist(Object.values(s.properties || {}).pop(), props?.isSubTable)) {
return buf.concat([s]);
}
return buf;

View File

@ -14,10 +14,23 @@ export const ColumnFieldProvider = observer(
return buf;
}, null);
const collectionField = fieldSchema && getCollectionJoinField(fieldSchema['x-collection-field']);
if (fieldSchema && record?.__collection && ['select', 'multipleSelect'].includes(collectionField?.interface)) {
if (
fieldSchema &&
record?.__collection &&
collectionField &&
['select', 'multipleSelect'].includes(collectionField.interface)
) {
const fieldName = `${record.__collection}.${fieldSchema.name}`;
schema.properties[fieldSchema.name]['x-collection-field'] = fieldName;
return <RecursionField basePath={basePath} schema={schema} onlyRenderProperties />;
const newSchema = {
...schema.toJSON(),
properties: {
[fieldSchema.name]: {
...fieldSchema.toJSON(),
'x-collection-field': fieldName,
},
},
};
return <RecursionField basePath={basePath} schema={newSchema} onlyRenderProperties />;
}
return props.children;
},

View File

@ -3,6 +3,7 @@ import { css, cx } from '@emotion/css';
import { useForm } from '@formily/react';
import { dayjs, error } from '@nocobase/utils/client';
import { Input as AntInput, Cascader, DatePicker, InputNumber, Select, Space, Tag } from 'antd';
import useAntdInputStyle from 'antd/es/input/style';
import type { DefaultOptionType } from 'antd/lib/cascader';
import classNames from 'classnames';
import { cloneDeep } from 'lodash';
@ -155,7 +156,11 @@ export function Input(props) {
changeOnSelect,
fieldNames,
} = props;
const { wrapSSR, hashId, componentCls } = useStyles();
const { wrapSSR, hashId, componentCls, rootPrefixCls } = useStyles();
// 添加 antd input 样式,防止样式缺失
useAntdInputStyle(`${rootPrefixCls}-input`);
const compile = useCompile();
const { t } = useTranslation();
const form = useForm();

View File

@ -8,9 +8,6 @@ export const useStyles = genStyleHook('nb-variable', (token) => {
const tagFontSize = token.fontSizeSM;
const tagLineHeight = `${token.lineHeightSM * tagFontSize}px`;
const defaultBg = colorFillQuaternary;
const lightColor = token[`blue1`];
const lightBorderColor = token[`blue3`];
const textColor = token[`blue7`];
return {
[componentCls]: {

View File

@ -20,7 +20,6 @@ const useDragEnd = (props?: any) => {
const wrapSchema = over?.data?.current?.wrapSchema;
const onSuccess = over?.data?.current?.onSuccess;
const removeParentsIfNoChildren = over?.data?.current?.removeParentsIfNoChildren ?? true;
if (!activeSchema || !overSchema) {
props?.onDragEnd?.(event);
return;

View File

@ -2,6 +2,7 @@ import { dayjs } from '@nocobase/utils/client';
import flat from 'flat';
import _, { every, findIndex, isArray, some } from 'lodash';
import { useMemo } from 'react';
import { useTableBlockContext } from '../../../block-provider';
import { useCurrentUserContext } from '../../../user';
import jsonLogic from '../../common/utils/logic';
@ -14,12 +15,15 @@ type VariablesCtx = {
export const useVariablesCtx = (): VariablesCtx => {
const { data } = useCurrentUserContext() || {};
const { field, service, rowKey } = useTableBlockContext();
const contextData = service?.data?.data?.filter((v) => (field?.data?.selectedRowKeys || [])?.includes(v[rowKey]));
return useMemo(() => {
return {
$user: data?.data || {},
$date: {
now: () => dayjs().toISOString(),
},
$context: contextData,
};
}, [data]);
};
@ -33,7 +37,7 @@ export const isVariable = (str: unknown) => {
return matches ? true : false;
};
export const parseVariables = (str: string, ctx: VariablesCtx) => {
export const parseVariables = (str: string, ctx: VariablesCtx | any) => {
const regex = /{{(.*?)}}/;
const matches = str?.match?.(regex);
if (matches) {

View File

@ -7,13 +7,12 @@ import {
import React, { useContext, useMemo } from 'react';
import { SchemaComponentOptions } from './SchemaComponentOptions';
export const FormProvider: React.FC<any> = (props) => {
const { children, ...others } = props;
const WithForm = (props) => {
const { children, form, ...others } = props;
const options = useContext(SchemaOptionsContext);
const expressionScope = useContext(SchemaExpressionScopeContext);
const scope = { ...options?.scope, ...expressionScope };
const components = { ...options?.components };
const form = useMemo(() => props.form || createForm(), []);
return (
<FormilyFormProvider {...others} form={form}>
<SchemaComponentOptions components={components} scope={scope}>
@ -22,3 +21,23 @@ export const FormProvider: React.FC<any> = (props) => {
</FormilyFormProvider>
);
};
const WithoutForm = (props) => {
const { children, ...others } = props;
const options = useContext(SchemaOptionsContext);
const expressionScope = useContext(SchemaExpressionScopeContext);
const scope = { ...options?.scope, ...expressionScope };
const components = { ...options?.components };
const form = useMemo(() => createForm(), []);
return (
<FormilyFormProvider {...others} form={form}>
<SchemaComponentOptions components={components} scope={scope}>
{children}
</SchemaComponentOptions>
</FormilyFormProvider>
);
};
export const FormProvider: React.FC<any> = (props) => {
return props.form ? <WithForm {...props} /> : <WithoutForm {...props} />;
};

View File

@ -0,0 +1,43 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SchemaInitializer } from '../..';
import { gridRowColWrap } from '../utils';
export const CusomeizeCreateFormBlockInitializers = (props: any) => {
const { t } = useTranslation();
const { insertPosition, component } = props;
return (
<SchemaInitializer.Button
wrap={gridRowColWrap}
title={component ? null : t('Add block')}
icon={'PlusOutlined'}
insertPosition={insertPosition}
component={component}
items={[
{
type: 'itemGroup',
title: '{{t("Data blocks")}}',
children: [
{
type: 'item',
title: '{{t("Form")}}',
component: 'FormBlockInitializer',
isCusomeizeCreate: true,
},
],
},
{
type: 'itemGroup',
title: '{{t("Other blocks")}}',
children: [
{
type: 'item',
title: '{{t("Markdown")}}',
component: 'MarkdownBlockInitializer',
},
],
},
]}
/>
);
};

View File

@ -156,6 +156,19 @@ export const TableActionInitializers = {
},
},
},
{
type: 'item',
title: '{{t("Add record")}}',
component: 'CustomizeAddRecordActionInitializer',
schema: {
'x-align': 'right',
'x-decorator': 'ACLActionProvider',
'x-acl-action': 'create',
'x-acl-action-props': {
skipScopeCheck: true,
},
},
},
],
visible: function useVisible() {
const collection = useCollection();

View File

@ -4,6 +4,7 @@ export * from './CalendarActionInitializers';
export * from './CalendarFormActionInitializers';
export * from './CreateFormBlockInitializers';
export * from './CreateFormBulkEditBlockInitializers';
export * from './CusomeizeCreateFormBlockInitializers';
export * from './CustomFormItemInitializers';
export * from './DetailsActionInitializers';
export * from './FilterFormActionInitializers';
@ -25,4 +26,3 @@ export * from './TableColumnInitializers';
export * from './TableSelectorInitializers';
// association filter
export * from '../../schema-component/antd/association-filter/AssociationFilter';

View File

@ -0,0 +1,52 @@
import React from 'react';
import { BlockInitializer } from './BlockInitializer';
export const CustomizeAddRecordActionInitializer = (props) => {
const schema = {
type: 'void',
title: '{{t("Add record")}}',
'x-designer': 'Action.Designer',
'x-component': 'Action',
'x-action': 'customize:create',
'x-component-props': {
openMode: 'drawer',
icon: 'PlusOutlined',
},
properties: {
drawer: {
type: 'void',
title: '{{t("Add record")}}',
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
properties: {
tabs: {
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
'x-initializer': 'TabPaneInitializersForCreateFormBlock',
properties: {
tab1: {
type: 'void',
title: '{{t("Add record")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'CusomeizeCreateFormBlockInitializers',
properties: {},
},
},
},
},
},
},
},
},
};
return <BlockInitializer {...props} schema={schema} />;
};

View File

@ -6,10 +6,10 @@ import { useSchemaTemplateManager } from '../../schema-templates';
import { useCollectionDataSourceItems } from '../utils';
export const DataBlockInitializer = (props) => {
const { templateWrap, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props;
const { templateWrap, onCreateBlockSchema, componentType, createBlockSchema, insert, isCusomeizeCreate, ...others } =
props;
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
const { setVisible } = useContext(SchemaInitializerButtonContext);
return (
<SchemaInitializer.Item
icon={<TableOutlined />}
@ -22,7 +22,7 @@ export const DataBlockInitializer = (props) => {
if (onCreateBlockSchema) {
onCreateBlockSchema({ item });
} else if (createBlockSchema) {
insert(createBlockSchema({ collection: item.name }));
insert(createBlockSchema({ collection: item.name, isCusomeizeCreate }));
}
}
setVisible(false);

View File

@ -1,9 +1,10 @@
import React from 'react';
import { FormOutlined } from '@ant-design/icons';
import React from 'react';
import { createFormBlockSchema } from '../utils';
import { DataBlockInitializer } from './DataBlockInitializer';
export const FormBlockInitializer = (props) => {
const { isCusomeizeCreate } = props;
return (
<DataBlockInitializer
{...props}
@ -11,6 +12,7 @@ export const FormBlockInitializer = (props) => {
componentType={'FormItem'}
templateWrap={(templateSchema, { item }) => {
const s = createFormBlockSchema({
isCusomeizeCreate,
template: templateSchema,
collection: item.name,
});

View File

@ -17,6 +17,7 @@ export * from './CreateFormBulkEditBlockInitializer';
export * from './CreateResetActionInitializer';
export * from './CreateSubmitActionInitializer';
export * from './CustomizeActionInitializer';
export * from './CustomizeAddRecordActionInitializer';
export * from './CustomizeBulkEditActionInitializer';
export * from './DataBlockInitializer';
export * from './DeleteEventActionInitializer';
@ -53,4 +54,3 @@ export * from './TableSelectorInitializer';
export * from './UpdateActionInitializer';
export * from './UpdateSubmitActionInitializer';
export * from './ViewActionInitializer';

View File

@ -7,16 +7,11 @@ import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { mergeFilter } from '../../block-provider';
import { useCollectionManager } from '../../collection-manager';
import {
AssociationSelect,
SchemaComponent,
SchemaComponentContext,
removeNullCondition,
} from '../../schema-component';
import { SchemaComponent, SchemaComponentContext, removeNullCondition } from '../../schema-component';
import { ITemplate } from '../../schema-component/antd/form-v2/Templates';
import { AsDefaultTemplate } from './components/AsDefaultTemplate';
import { ArrayCollapse } from './components/DataTemplateTitle';
import { Designer, getSelectedIdFilter } from './components/Designer';
import { getSelectedIdFilter } from './components/Designer';
import { useCollectionState } from './hooks/useCollectionState';
const Tree = connect(
@ -27,11 +22,30 @@ const Tree = connect(
}),
);
export const compatibleDataId = (data, config?) => {
return data?.map((v) => {
const { dataId, ...others } = v;
const obj = { ...others };
if (dataId) {
obj.dataScope = { $and: [{ id: { $eq: dataId } }] };
obj.titleField = obj?.titleField || config?.[v.collection]?.['titleField'] || 'id';
}
return obj;
});
};
export const FormDataTemplates = observer(
(props: any) => {
const { useProps, formSchema, designerCtx } = props;
const { defaultValues, collectionName } = useProps();
const { collectionList, getEnableFieldTree, getOnLoadData, getOnCheck } = useCollectionState(collectionName);
const {
collectionList,
getEnableFieldTree,
getOnLoadData,
getOnCheck,
getScopeDataSource,
useTitleFieldDataSource,
} = useCollectionState(collectionName);
const { getCollection, getCollectionField } = useCollectionManager();
const { t } = useTranslation();
@ -39,11 +53,15 @@ export const FormDataTemplates = observer(
const activeData = useMemo<ITemplate>(
() =>
observable(
defaultValues || { items: [], display: true, config: { [collectionName]: { titleField: '', filter: {} } } },
{ ...defaultValues, items: compatibleDataId(defaultValues?.items || [], defaultValues?.config) } || {
items: [],
display: true,
config: { [collectionName]: { titleField: '', filter: {} } },
},
),
[],
);
console.log(activeData);
const getTargetField = (collectionName: string) => {
const collection = getCollection(collectionName);
return getCollectionField(
@ -63,8 +81,8 @@ export const FormDataTemplates = observer(
const filter = activeData.config?.[collectionName]?.filter;
return _.isEmpty(filter) ? {} : removeNullCondition(mergeFilter([filter, getSelectedIdFilter(value)], '$or'));
};
const components = useMemo(() => ({ ArrayCollapse }), []);
const scope = useMemo(
() => ({
getEnableFieldTree,
@ -75,6 +93,8 @@ export const FormDataTemplates = observer(
getOnLoadData,
getOnCheck,
collectionName,
getScopeDataSource,
useTitleFieldDataSource,
}),
[],
);
@ -117,49 +137,39 @@ export const FormDataTemplates = observer(
options: collectionList,
},
},
dataId: {
type: 'number',
title: '{{ t("Template Data") }}',
required: true,
description: t('Select an existing piece of data as the initialization data for the form'),
'x-designer': Designer,
'x-designer-props': {
formSchema,
data: activeData,
},
dataScope: {
type: 'object',
title: '{{ t("Assign data scope for the template") }}',
'x-decorator': 'FormItem',
'x-component': AssociationSelect,
'x-component-props': {
service: {
resource: '{{ $record.collection || collectionName }}',
params: {
filter: '{{ getFilter($self.componentProps.service.resource, $self.value) }}',
},
'x-component': 'Filter',
'x-decorator-props': {
style: {
marginBottom: '0px',
},
action: 'list',
multiple: false,
objectValue: false,
manual: false,
targetField: '{{ getTargetField($self.componentProps.service.resource) }}',
mapOptions: getMapOptions(),
fieldNames: '{{ getFieldNames($self.componentProps.service.resource) }}',
},
required: true,
'x-reactions': [
{
dependencies: ['.collection'],
fulfill: {
state: {
disabled: '{{ !$deps[0] }}',
componentProps: {
service: {
resource: '{{ getResource($deps[0], $self) }}',
},
},
},
schema: {
enum: '{{ getScopeDataSource($deps[0]) }}',
},
},
},
],
},
titleField: {
type: 'string',
'x-decorator': 'FormItem',
title: '{{ t("Title field") }}',
'x-component': 'Select',
required: true,
'x-reactions': '{{useTitleFieldDataSource}}',
},
fields: {
type: 'array',
title: '{{ t("Data fields") }}',
@ -246,15 +256,6 @@ export function getLabel(titleField) {
return titleField || 'label';
}
function getMapOptions() {
return (option) => {
if (option?.id === undefined) {
return null;
}
return option;
};
}
function getResource(resource: string, field: Field) {
if (resource !== field.componentProps.service.resource) {
// 切换 collection 后,之前选中的其它 collection 的数据就没有意义了,需要清空

View File

@ -1,11 +1,12 @@
import { ArrayField } from '@formily/core';
import React, { useCallback, useState } from 'react';
import { useCollectionManager } from '../../../collection-manager';
import { isTitleField } from '../../../collection-manager/Configuration/CollectionFields';
import { useCompile } from '../../../schema-component';
import { TreeNode } from '../TreeLabel';
export const useCollectionState = (currentCollectionName: string) => {
const { getCollectionFields, getAllCollectionsInheritChain, getCollection } = useCollectionManager();
const { getCollectionFields, getAllCollectionsInheritChain, getCollection, getInterface } = useCollectionManager();
const [collectionList] = useState(getCollectionList);
const compile = useCompile();
@ -150,11 +151,78 @@ export const useCollectionState = (currentCollectionName: string) => {
};
}, []);
const getScopeDataSource = (resource: string) => {
const fields = getCollectionFields(resource);
const field2option = (field, depth) => {
if (!field.interface) {
return;
}
const fieldInterface = getInterface(field.interface);
if (!fieldInterface?.filterable) {
return;
}
const { nested, children, operators } = fieldInterface.filterable;
const option = {
name: field.name,
title: field?.uiSchema?.title || field.name,
schema: field?.uiSchema,
operators:
operators?.filter?.((operator) => {
return !operator?.visible || operator.visible(field);
}) || [],
interface: field.interface,
};
if (field.target && depth > 2) {
return;
}
if (depth > 2) {
return option;
}
if (children?.length) {
option['children'] = children;
}
if (nested) {
const targetFields = getCollectionFields(field.target);
const options = getOptions(targetFields, depth + 1).filter(Boolean);
option['children'] = option['children'] || [];
option['children'].push(...options);
}
return option;
};
const getOptions = (fields, depth) => {
const options = [];
fields.forEach((field) => {
const option = field2option(field, depth);
if (option) {
options.push(option);
}
});
return options;
};
const options = getOptions(fields, 1);
return options;
};
const useTitleFieldDataSource = (field) => {
const fieldPath = field.path.entire.replace('titleField', 'collection');
const collectionName = field.query(fieldPath).get('value');
const targetFields = getCollectionFields(collectionName);
const options = targetFields
.filter((field) => {
return !field.isForeignKey && getInterface(field.interface)?.titleUsable;
})
.map((field) => ({
value: field?.name,
label: compile(field?.uiSchema?.title) || field?.name,
}));
field.dataSource = options;
};
return {
collectionList,
getEnableFieldTree,
getOnLoadData,
getOnCheck,
getScopeDataSource,
useTitleFieldDataSource,
};
};

View File

@ -0,0 +1,105 @@
import { css } from '@emotion/css';
import moment from 'moment';
import { connect, mapProps } from '@formily/react';
import { useBoolean } from 'ahooks';
import { Input, Radio, Space } from 'antd';
import React, { useState } from 'react';
import { useToken } from '../../';
const date = moment();
const spaceCSS = css`
width: 100%;
& > .ant-space-item {
flex: 1;
}
`;
export const DateFormatCom = (props?) => {
const date = moment();
return (
<div style={{ display: 'inline-flex' }}>
<span>{props.format}</span>
<DateTimeFormatPreview content={date.format(props.format)} />
</div>
);
};
const DateTimeFormatPreview = ({ content }) => {
const { token } = useToken();
return (
<span
style={{
display: 'inline-block',
background: token.colorBgTextHover,
marginLeft: token.marginMD,
lineHeight: '1',
padding: token.paddingXXS,
borderRadius: token.borderRadiusOuter,
}}
>
{content}
</span>
);
};
const InternalExpiresRadio = (props) => {
const { onChange, defaultValue, formats, timeFormat } = props;
const [isCustom, { setFalse, setTrue }] = useBoolean(props.value && !formats.includes(props.value));
const targetValue = props.value && !formats.includes(props.value) ? props.value : defaultValue;
const [customFormatPreview, setCustomFormatPreview] = useState(targetValue ? date.format(targetValue) : null);
const onSelectChange = (v) => {
if (v.target.value === 'custom') {
setTrue();
onChange(targetValue);
} else {
setFalse();
onChange(v.target.value);
}
};
return (
<Space className={spaceCSS}>
<Radio.Group value={isCustom ? 'custom' : props.value} onChange={onSelectChange}>
<Space direction="vertical">
{props.options.map((v) => {
if (v.value === 'custom') {
return (
<Radio value={v.value}>
<Input
style={{ width: '150px' }}
defaultValue={targetValue}
onChange={(e) => {
if (
e.target.value &&
moment(timeFormat ? date.format() : date.toLocaleString(), e.target.value).isValid()
) {
setCustomFormatPreview(date.format(e.target.value));
} else {
setCustomFormatPreview(null);
}
if (isCustom) {
onChange(e.target.value);
}
}}
/>
<DateTimeFormatPreview content={customFormatPreview} />
</Radio>
);
}
return <Radio value={v.value}>{v.label}</Radio>;
})}
</Space>
</Radio.Group>
</Space>
);
};
const ExpiresRadio = connect(
InternalExpiresRadio,
mapProps({
dataSource: 'options',
}),
);
export { ExpiresRadio };

View File

@ -1,3 +1,4 @@
import { css } from '@emotion/css';
import { ArrayCollapse, ArrayItems, FormItem, FormLayout, Input } from '@formily/antd-v5';
import { Field, GeneralField, createForm } from '@formily/core';
import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema, useForm } from '@formily/react';
@ -56,17 +57,28 @@ import {
useGlobalTheme,
useLinkageCollectionFilterOptions,
} from '..';
import { useTableBlockContext } from '../block-provider';
import { findFilterTargets, updateFilterTargets } from '../block-provider/hooks';
import { FilterBlockType, isSameCollection, useSupportedBlocks } from '../filter-provider/utils';
import {
FilterBlockType,
getSupportFieldsByAssociation,
getSupportFieldsByForeignKey,
isSameCollection,
useSupportedBlocks,
} from '../filter-provider/utils';
import { useCollectMenuItem, useCollectMenuItems, useMenuItem } from '../hooks/useMenuItem';
import { getTargetKey } from '../schema-component/antd/association-filter/utilts';
import { getFieldDefaultValue } from '../schema-component/antd/form-item';
import { parseVariables, useVariablesCtx } from '../schema-component/common/utils/uitls';
import { useSchemaTemplateManager } from '../schema-templates';
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate';
import { FormDataTemplates } from './DataTemplates';
import { DateFormatCom, ExpiresRadio } from './DateFormat/ExpiresRadio';
import { EnableChildCollections } from './EnableChildCollections';
import { ChildDynamicComponent } from './EnableChildCollections/DynamicComponent';
import { FormLinkageRules } from './LinkageRules';
import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks';
import { VariableInput } from './VariableInput/VariableInput';
interface SchemaSettingsProps {
title?: any;
@ -544,6 +556,7 @@ SchemaSettings.ConnectDataBlocks = function ConnectDataBlocks(props: {
// eslint-disable-next-line prefer-const
let { targets = [], uid } = findFilterTargets(fieldSchema);
const compile = useCompile();
const { getAllCollectionsInheritChain } = useCollectionManager();
if (!inProvider) {
return null;
@ -608,14 +621,18 @@ SchemaSettings.ConnectDataBlocks = function ConnectDataBlocks(props: {
title={title}
value={target?.field || ''}
options={[
...block.associatedFields
.filter((field) => field.target === collection.name)
.map((field) => {
return {
label: compile(field.uiSchema.title) || field.name,
value: `${field.name}.${getTargetKey(field)}`,
};
}),
...getSupportFieldsByAssociation(getAllCollectionsInheritChain(collection.name), block).map((field) => {
return {
label: compile(field.uiSchema.title) || field.name,
value: `${field.name}.${getTargetKey(field)}`,
};
}),
...getSupportFieldsByForeignKey(collection, block).map((field) => {
return {
label: `${compile(field.uiSchema.title) || field.name} [${t('Foreign key')}]`,
value: field.name,
};
}),
{
label: t('Unconnected'),
value: '',
@ -1267,6 +1284,263 @@ SchemaSettings.EnableChildCollections = function EnableChildCollectionsItem(prop
);
};
SchemaSettings.DataFormat = function DateFormatConfig(props: { fieldSchema: Schema }) {
const { fieldSchema } = props;
const field = useField();
const form = useForm();
const { dn } = useDesignable();
const { t } = useTranslation();
const { getCollectionJoinField } = useCollectionManager();
const collectionField = getCollectionJoinField(fieldSchema?.['x-collection-field']) || {};
const isShowTime = fieldSchema?.['x-component-props']?.showTime;
const dateFormatDefaultValue =
fieldSchema?.['x-component-props']?.dateFormat ||
collectionField?.uiSchema?.['x-component-props']?.dateFormat ||
'YYYY-MM-DD';
const timeFormatDefaultValue =
fieldSchema?.['x-component-props']?.timeFormat || collectionField?.uiSchema?.['x-component-props']?.timeFormat;
return (
<SchemaSettings.ModalItem
title={t('Date display format')}
schema={
{
type: 'object',
properties: {
dateFormat: {
type: 'string',
title: '{{t("Date format")}}',
'x-component': ExpiresRadio,
'x-decorator': 'FormItem',
'x-decorator-props': {},
'x-component-props': {
className: css`
.ant-radio-wrapper {
display: flex;
margin: 5px 0px;
}
`,
defaultValue: 'dddd',
formats: ['MMMMM Do YYYY', 'YYYY-MM-DD', 'MM/DD/YY', 'YYYY/MM/DD', 'DD/MM/YYYY'],
},
default: dateFormatDefaultValue,
enum: [
{
label: DateFormatCom({ format: 'MMMMM Do YYYY' }),
value: 'MMMMM Do YYYY',
},
{
label: DateFormatCom({ format: 'YYYY-MM-DD' }),
value: 'YYYY-MM-DD',
},
{
label: DateFormatCom({ format: 'MM/DD/YY' }),
value: 'MM/DD/YY',
},
{
label: DateFormatCom({ format: 'YYYY/MM/DD' }),
value: 'YYYY/MM/DD',
},
{
label: DateFormatCom({ format: 'DD/MM/YYYY' }),
value: 'DD/MM/YYYY',
},
{
label: 'custom',
value: 'custom',
},
],
},
showTime: {
default:
isShowTime === undefined ? collectionField?.uiSchema?.['x-component-props']?.showTime : isShowTime,
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Show time")}}',
'x-reactions': [
`{{(field) => {
field.query('.timeFormat').take(f => {
f.display = field.value ? 'visible' : 'none';
});
}}}`,
],
},
timeFormat: {
type: 'string',
title: '{{t("Time format")}}',
'x-component': ExpiresRadio,
'x-decorator': 'FormItem',
'x-decorator-props': {
className: css`
margin-bottom: 0px;
`,
},
'x-component-props': {
className: css`
color: red;
.ant-radio-wrapper {
display: flex;
margin: 5px 0px;
}
`,
defaultValue: 'h:mm a',
formats: ['hh:mm:ss a', 'HH:mm:ss'],
timeFormat: true,
},
default: timeFormatDefaultValue,
enum: [
{
label: DateFormatCom({ format: 'hh:mm:ss a' }),
value: 'hh:mm:ss a',
},
{
label: DateFormatCom({ format: 'HH:mm:ss' }),
value: 'HH:mm:ss',
},
{
label: 'custom',
value: 'custom',
},
],
},
},
} as ISchema
}
onSubmit={(data) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
schema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props'] = {
...(fieldSchema['x-component-props'] || {}),
...data,
};
schema['x-component-props'] = fieldSchema['x-component-props'];
field.componentProps = fieldSchema['x-component-props'];
field.query(`.*.${fieldSchema.name}`).forEach((f) => {
f.componentProps = fieldSchema['x-component-props'];
});
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
);
};
const defaultInputStyle = css`
& > .nb-form-item {
flex: 1;
}
`;
export const findParentFieldSchema = (fieldSchema: Schema) => {
let parent = fieldSchema.parent;
while (parent) {
if (parent['x-component'] === 'CollectionField') {
return parent;
}
parent = parent.parent;
}
};
SchemaSettings.DefaultValue = function DefaultvalueConfigure(props) {
const variablesCtx = useVariablesCtx();
const currentSchema = useFieldSchema();
const fieldSchema = props?.fieldSchema ?? currentSchema;
const field = useField<Field>();
const { dn } = useDesignable();
const { t } = useTranslation();
let targetField;
const { getField } = useCollection();
const { getCollectionJoinField } = useCollectionManager();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
const fieldSchemaWithoutRequired = _.omit(fieldSchema, 'required');
if (collectionField?.target) {
targetField = getCollectionJoinField(
`${collectionField.target}.${fieldSchema['x-component-props']?.fieldNames?.label || 'id'}`,
);
}
const parentFieldSchema = collectionField?.interface === 'm2o' && findParentFieldSchema(fieldSchema);
const parentCollectionField = parentFieldSchema && getCollectionJoinField(parentFieldSchema?.['x-collection-field']);
const tableCtx = useTableBlockContext();
const isAllowContexVariable =
collectionField?.interface === 'm2m' ||
(parentCollectionField?.type === 'hasMany' && collectionField?.interface === 'm2o');
return (
<SchemaSettings.ModalItem
title={t('Set default value')}
components={{ ArrayCollapse, FormLayout, VariableInput }}
width={800}
schema={
{
type: 'object',
title: t('Set default value'),
properties: {
default: {
...(fieldSchemaWithoutRequired || {}),
'x-decorator': 'FormItem',
'x-component': 'VariableInput',
'x-component-props': {
...(fieldSchema?.['x-component-props'] || {}),
collectionField,
targetField,
collectionName: collectionField?.collectionName,
contextCollectionName: isAllowContexVariable && tableCtx.collection,
schema: collectionField?.uiSchema,
className: defaultInputStyle,
renderSchemaComponent: function Com(props) {
const s = _.cloneDeep(fieldSchemaWithoutRequired) || ({} as Schema);
s.title = '';
s['x-read-pretty'] = false;
s['x-disabled'] = false;
return (
<SchemaComponent
schema={{
...(s || {}),
'x-component-props': {
...s['x-component-props'],
onChange: props.onChange,
value: props.value,
defaultValue: getFieldDefaultValue(s, collectionField),
style: {
width: '100%',
verticalAlign: 'top',
minWidth: '200px',
},
},
}}
/>
);
},
},
name: 'default',
title: t('Default value'),
default: getFieldDefaultValue(fieldSchema, collectionField),
},
},
} as ISchema
}
onSubmit={(v) => {
const schema: ISchema = {
['x-uid']: fieldSchema['x-uid'],
};
if (field.value !== v.default) {
field.value = parseVariables(v.default, variablesCtx);
}
fieldSchema.default = v.default;
schema.default = v.default;
dn.emit('patch', {
schema,
currentSchema,
});
dn.refresh();
}}
/>
);
};
// 是否显示默认值配置项
export const isShowDefaultValue = (collectionField: CollectionFieldOptions, getInterface) => {
return (

View File

@ -1,6 +1,7 @@
import React, { useMemo } from 'react';
import { CollectionFieldOptions } from '../../collection-manager';
import { useCompile, Variable } from '../../schema-component';
import { useContextAssociationFields } from './hooks/useContextAssociationFields';
import { useUserVariable } from './hooks/useUserVariable';
type Props = {
@ -14,6 +15,7 @@ type Props = {
className?: string;
style?: React.CSSProperties;
collectionField?: CollectionFieldOptions;
contextCollectionName?: string;
};
export const VariableInput = (props: Props) => {
@ -25,9 +27,11 @@ export const VariableInput = (props: Props) => {
schema,
className,
collectionField,
contextCollectionName,
} = props;
const compile = useCompile();
const userVariable = useUserVariable({ schema, maxDepth: 1 });
const contextVariable = useContextAssociationFields({ schema, maxDepth: 2, contextCollectionName });
const scope = useMemo(() => {
const data = [
compile({
@ -47,11 +51,21 @@ export const VariableInput = (props: Props) => {
if (collectionField?.target === 'users') {
data.unshift(userVariable);
}
if (contextCollectionName) {
data.unshift(contextVariable);
}
return data;
}, []);
return (
<Variable.Input className={className} value={value} onChange={onChange} scope={scope} style={style}>
<Variable.Input
className={className}
value={value}
onChange={onChange}
scope={scope}
style={style}
changeOnSelect={contextCollectionName!==null}
>
<RenderSchemaComponent value={value} onChange={onChange} />
</Variable.Input>
);

View File

@ -0,0 +1,121 @@
import { error } from '@nocobase/utils/client';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useCompile, useGetFilterOptions } from '../../../schema-component';
import { FieldOption, Option } from '../type';
interface GetOptionsParams {
schema: any;
depth: number;
maxDepth?: number;
loadChildren?: (option: Option) => Promise<void>;
compile: (value: string) => any;
}
const getChildren = (
options: FieldOption[],
{ schema, depth, maxDepth, loadChildren, compile }: GetOptionsParams,
): Option[] => {
const result = options
.map((option): Option => {
if (!option.target) {
return {
key: option.name,
value: option.name,
label: compile(option.title),
// TODO: 现在是通过组件的名称来过滤能够被选择的选项,这样的坏处是不够精确,后续可以优化
// disabled: schema?.['x-component'] !== option.schema?.['x-component'],
isLeaf: true,
depth,
};
}
if (depth >= maxDepth) {
return null;
}
return {
key: option.name,
value: option.name,
label: compile(option.title),
isLeaf: true,
field: option,
depth,
loadChildren,
};
})
.filter(Boolean);
return result;
};
export const useContextAssociationFields = ({
schema,
maxDepth = 3,
contextCollectionName,
}: {
schema: any;
maxDepth?: number;
contextCollectionName: string;
}) => {
const { t } = useTranslation();
const compile = useCompile();
const getFilterOptions = useGetFilterOptions();
const loadChildren = (option: Option): Promise<void> => {
if (!option.field?.target) {
return new Promise((resolve) => {
error('Must be set field target');
option.children = [];
resolve(void 0);
});
}
const collectionName = option.field.target;
return new Promise((resolve) => {
setTimeout(() => {
const children =
getChildren(
getFilterOptions(collectionName).filter((v) => {
const isAssociationField = ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(v.type);
return isAssociationField;
}),
{
schema,
depth: option.depth + 1,
maxDepth,
loadChildren,
compile,
},
) || [];
if (children.length === 0) {
option.disabled = true;
option.children = [];
resolve();
return;
}
option.children = children;
resolve();
// 延迟 5 毫秒,防止阻塞主线程,导致 UI 卡顿
}, 5);
});
};
const result = useMemo(() => {
return {
label: t('Table selected records'),
value: '$context',
key: '$context',
isLeaf: false,
field: {
target: contextCollectionName,
},
depth: 0,
loadChildren,
} as Option;
}, [schema?.['x-component']]);
return result;
};

View File

@ -1,6 +1,6 @@
{
"name": "create-nocobase-app",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"main": "src/index.js",
"license": "Apache-2.0",
"dependencies": {

View File

@ -1,13 +1,13 @@
{
"name": "@nocobase/database",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"description": "",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"license": "Apache-2.0",
"dependencies": {
"@nocobase/logger": "0.11.1-alpha.2",
"@nocobase/utils": "0.11.1-alpha.2",
"@nocobase/logger": "0.11.1-alpha.3",
"@nocobase/utils": "0.11.1-alpha.3",
"async-mutex": "^0.3.2",
"cron-parser": "4.4.0",
"dayjs": "^1.11.8",

View File

@ -20,7 +20,9 @@ export class SyncRunner {
if (!parents) {
throw new Error(
`Inherit model ${inheritedCollection.name} can't be created without parents, parents option is ${lodash
`Inherit model ${
inheritedCollection.name
} can't be created without parents, parents option is ${lodash
.castArray(inheritedCollection.options.inherits)
.join(', ')}`,
);
@ -58,7 +60,7 @@ export class SyncRunner {
const columnDefault = sequenceNameResult[0][0]['column_default'];
if (!columnDefault) {
throw new Error(`Can't find sequence name of ${parent}`);
throw new Error(`Can't find sequence name of parent collection ${parent.options.name}`);
}
const regex = new RegExp(/nextval\('(.*)'::regclass\)/);

View File

@ -1,48 +1,45 @@
{
"name": "@nocobase/devtools",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"description": "",
"license": "Apache-2.0",
"main": "./src/index.js",
"dependencies": {
"@nocobase/build": "0.11.1-alpha.2",
"@nocobase/build": "0.11.1-alpha.3",
"@testing-library/react": "^12.1.5",
"@types/jest": "^26.0.0",
"@types/jest": "^29.0.0",
"@types/koa": "^2.13.4",
"@types/koa-bodyparser": "^4.3.4",
"@types/lodash": "^4.14.177",
"@types/node": "*",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0",
"concurrently": "^7.0.0",
"cross-env": "^7.0.3",
"eslint": "^8.39.0",
"eslint": "^8.45.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-markdown": "^3.0.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-react-hooks": "^4.6.0",
"jest": "^26.0.0",
"jest-codemods": "^0.19.1",
"jest": "^29.0.0",
"jest-cli": "^29.0.0",
"jest-dom": "^3.1.2",
"jest-localstorage-mock": "^2.3.0",
"jest-styled-components": "6.3.3",
"jest-watch-lerna-packages": "^1.1.0",
"jsdom": "^16.0.0",
"lerna": "^4.0.0",
"prettier": "^2.2.1",
"prettier": "^3.0.0",
"pretty-format": "^24.0.0",
"pretty-quick": "^3.1.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"rimraf": "^3.0.0",
"serve": "^13.0.2",
"ts-jest": "^26.0.0",
"ts-jest": "^29.0.0",
"ts-loader": "^7.0.4",
"ts-node": "9.1.1",
"ts-node-dev": "1.1.8",

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/logger",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"description": "nocobase logging library",
"license": "Apache-2.0",
"main": "./lib/index.js",

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/sdk",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"license": "Apache-2.0",
"main": "lib",
"module": "es/index.js",

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/server",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"main": "lib/index.js",
"types": "./lib/index.d.ts",
"license": "Apache-2.0",
@ -8,13 +8,13 @@
"@hapi/topo": "^6.0.0",
"@koa/cors": "^3.1.0",
"@koa/router": "^9.4.0",
"@nocobase/acl": "0.11.1-alpha.2",
"@nocobase/actions": "0.11.1-alpha.2",
"@nocobase/auth": "0.11.1-alpha.2",
"@nocobase/database": "0.11.1-alpha.2",
"@nocobase/logger": "0.11.1-alpha.2",
"@nocobase/resourcer": "0.11.1-alpha.2",
"@nocobase/utils": "0.11.1-alpha.2",
"@nocobase/acl": "0.11.1-alpha.3",
"@nocobase/actions": "0.11.1-alpha.3",
"@nocobase/auth": "0.11.1-alpha.3",
"@nocobase/database": "0.11.1-alpha.3",
"@nocobase/logger": "0.11.1-alpha.3",
"@nocobase/resourcer": "0.11.1-alpha.3",
"@nocobase/utils": "0.11.1-alpha.3",
"chalk": "^4.1.1",
"commander": "^9.2.0",
"dayjs": "^1.11.8",

View File

@ -18,6 +18,7 @@ import { createACL } from './acl';
import { AppManager } from './app-manager';
import { registerCli } from './commands';
import { createI18n, createResourcer, registerMiddlewares } from './helper';
import { Locale } from './locale';
import { Plugin } from './plugin';
import { InstallOptions, PluginManager } from './plugin-manager';
@ -167,6 +168,8 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
protected _authManager: AuthManager;
protected _locales: Locale;
protected _version: ApplicationVersion;
protected plugins = new Map<string, Plugin>();
@ -230,6 +233,10 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
return this._logger;
}
get locales() {
return this._locales;
}
get name() {
return this.options.name || 'main';
}
@ -298,6 +305,8 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
this._resourcer.use(this._acl.middleware(), { tag: 'acl', after: ['auth'] });
}
this._locales = new Locale(this);
registerMiddlewares(this, options);
if (options.registerActions !== false) {

View File

@ -0,0 +1 @@
export * from './locale';

View File

@ -0,0 +1,68 @@
import { Cache, createCache } from '@nocobase/cache';
import { lodash } from '@nocobase/utils';
import Application from '../application';
import { PluginManager } from '../plugin-manager';
import { getAntdLocale } from './antd';
import { getCronstrueLocale } from './cronstrue';
import { getResource } from './resource';
export class Locale {
app: Application;
cache: Cache;
defaultLang = 'en-US';
constructor(app: Application) {
this.app = app;
this.cache = createCache();
this.app.on('afterLoad', () => this.load());
}
load() {
this.getCacheResources(this.defaultLang);
}
async get(lang: string) {
return {
antd: await this.wrapCache(`locale:antd:${lang}`, () => getAntdLocale(lang)),
cronstrue: await this.wrapCache(`locale:cronstrue:${lang}`, () => getCronstrueLocale(lang)),
resources: await this.getCacheResources(lang),
};
}
async wrapCache(key: string, fn: () => any) {
const result = await this.cache.get(key);
if (result) {
return result;
}
const value = await fn();
if (lodash.isEmpty(value)) {
return value;
}
await this.cache.set(key, value);
return value;
}
async getCacheResources(lang: string) {
return await this.wrapCache(`locale:resources:${lang}`, () => this.getResources(lang));
}
getResources(lang: string) {
const resources = {};
const plugins = this.app.pm.getPlugins();
for (const name of plugins.keys()) {
try {
const packageName = PluginManager.getPackageName(name);
const res = getResource(packageName, lang);
if (res) {
resources[name] = { ...res };
}
} catch (err) {}
}
const res = getResource('@nocobase/client', lang);
if (res) {
resources['client'] = { ...(resources['client'] || {}), ...res };
}
return resources;
}
}

View File

@ -0,0 +1,27 @@
const arr2obj = (items: any[]) => {
const obj = {};
for (const item of items) {
Object.assign(obj, item);
}
return obj;
};
export const getResource = (packageName: string, lang: string) => {
const resources = [];
const prefixes = ['src', 'lib'];
for (const prefix of prefixes) {
try {
const file = `${packageName}/${prefix}/locale/${lang}`;
require.resolve(file);
const resource = require(file).default;
resources.push(resource);
} catch (error) {}
if (resources.length) {
break;
}
}
if (resources.length === 0 && lang.replace('-', '_') !== lang) {
return getResource(packageName, lang.replace('-', '_'));
}
return arr2obj(resources);
};

View File

@ -1,11 +1,11 @@
{
"name": "@nocobase/test",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"main": "lib/index.js",
"types": "./lib/index.d.ts",
"license": "Apache-2.0",
"dependencies": {
"@nocobase/server": "0.11.1-alpha.2",
"@nocobase/server": "0.11.1-alpha.3",
"@types/supertest": "^2.0.11",
"mockjs": "^1.1.0",
"mysql2": "^2.3.3",

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/utils",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"main": "lib/index.js",
"types": "./lib/index.d.ts",
"license": "Apache-2.0",
@ -11,6 +11,7 @@
"deepmerge": "^4.2.2",
"flat-to-nested": "^1.1.1",
"graphlib": "^2.1.8",
"multer": "^1.4.5-lts.1",
"object-path": "^0.11.8"
},
"peerDependencies": {

View File

@ -8,6 +8,7 @@ export * from './date';
export * from './dayjs';
export * from './forEach';
export * from './json-templates';
export * from './koa-multer';
export * from './merge';
export * from './mixin';
export * from './mixin/AsyncEmitter';
@ -19,4 +20,3 @@ export * from './requireModule';
export * from './toposort';
export * from './uid';
export { dayjs, lodash };

View File

@ -0,0 +1,58 @@
import originalMulter from 'multer';
function multer(options?) {
const m = originalMulter(options) as any;
makePromise(m, 'any');
makePromise(m, 'array');
makePromise(m, 'fields');
makePromise(m, 'none');
makePromise(m, 'single');
return m;
}
function makePromise(multer, name) {
if (!multer[name]) return;
const fn = multer[name];
multer[name] = function (...args) {
const middleware: any = Reflect.apply(fn, this, args);
return async (ctx, next) => {
await new Promise((resolve, reject) => {
middleware(ctx.req, ctx.res, (err) => {
if (err) return reject(err);
if ('request' in ctx) {
if (ctx.req.body) {
ctx.request.body = ctx.req.body;
delete ctx.req.body;
}
if (ctx.req.file) {
ctx.request.file = ctx.req.file;
ctx.file = ctx.req.file;
delete ctx.req.file;
}
if (ctx.req.files) {
ctx.request.files = ctx.req.files;
ctx.files = ctx.req.files;
delete ctx.req.files;
}
}
resolve(ctx);
});
});
return next();
};
};
}
multer.diskStorage = originalMulter.diskStorage;
multer.memoryStorage = originalMulter.memoryStorage;
export { multer as koaMulter };

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "权限控制",
"description": "A simple access control based on roles, resources and actions",
"description.zh-CN": "基于角色、资源和操作的权限控制。",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"license": "AGPL-3.0",
"main": "./lib/server/index.js",
"files": [
@ -19,13 +19,13 @@
"client.d.ts"
],
"devDependencies": {
"@nocobase/acl": "0.11.1-alpha.2",
"@nocobase/actions": "0.11.1-alpha.2",
"@nocobase/client": "0.11.1-alpha.2",
"@nocobase/database": "0.11.1-alpha.2",
"@nocobase/server": "0.11.1-alpha.2",
"@nocobase/test": "0.11.1-alpha.2",
"@nocobase/utils": "0.11.1-alpha.2",
"@nocobase/acl": "0.11.1-alpha.3",
"@nocobase/actions": "0.11.1-alpha.3",
"@nocobase/client": "0.11.1-alpha.3",
"@nocobase/database": "0.11.1-alpha.3",
"@nocobase/server": "0.11.1-alpha.3",
"@nocobase/test": "0.11.1-alpha.3",
"@nocobase/utils": "0.11.1-alpha.3",
"@types/jsonwebtoken": "^8.5.8",
"jsonwebtoken": "^8.5.1",
"react": "^18.2.0",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "API keys",
"description": "Allow users to use API key to access NocoBase's api",
"description.zh-CN": "允许用户使用 API key 访问 NocoBase 的 api",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"license": "AGPL-3.0",
"main": "./lib/server/index.js",
"files": [
@ -21,13 +21,13 @@
"devDependencies": {
"@formily/react": "2.2.26",
"@formily/shared": "2.2.26",
"@nocobase/actions": "0.11.1-alpha.2",
"@nocobase/client": "0.11.1-alpha.2",
"@nocobase/database": "0.11.1-alpha.2",
"@nocobase/resourcer": "0.11.1-alpha.2",
"@nocobase/server": "0.11.1-alpha.2",
"@nocobase/test": "0.11.1-alpha.2",
"@nocobase/utils": "0.11.1-alpha.2",
"@nocobase/actions": "0.11.1-alpha.3",
"@nocobase/client": "0.11.1-alpha.3",
"@nocobase/database": "0.11.1-alpha.3",
"@nocobase/resourcer": "0.11.1-alpha.3",
"@nocobase/server": "0.11.1-alpha.3",
"@nocobase/test": "0.11.1-alpha.3",
"@nocobase/utils": "0.11.1-alpha.3",
"antd": "^5.6.4",
"dayjs": "^1.11.8",
"i18next": "^22.4.9",

View File

@ -16,6 +16,7 @@ const locale = {
'7 Days': '7 天',
'30 Days': '30 天',
'90 Days': '90 天',
'Role not found': '角色不存在',
};
export default locale;

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/plugin-audit-logs",
"version": "0.11.1-alpha.2",
"version": "0.11.1-alpha.3",
"displayName": "audit-logs",
"displayName.zh-CN": "审计日志",
"description": "audit logs plugin",
@ -23,10 +23,10 @@
"@formily/antd-v5": "1.1.0-beta.4",
"@formily/react": "2.2.26",
"@formily/shared": "2.2.26",
"@nocobase/client": "0.11.1-alpha.2",
"@nocobase/database": "0.11.1-alpha.2",
"@nocobase/server": "0.11.1-alpha.2",
"@nocobase/test": "0.11.1-alpha.2",
"@nocobase/client": "0.11.1-alpha.3",
"@nocobase/database": "0.11.1-alpha.3",
"@nocobase/server": "0.11.1-alpha.3",
"@nocobase/test": "0.11.1-alpha.3",
"react": "^18.2.0",
"react-i18next": "^11.15.1"
},

View File

@ -0,0 +1,3 @@
export default {
'Details of changes': 'Détails des changements',
};

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