mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 21:49:25 +08:00
Merge branch 'main' into T-1155
This commit is contained in:
commit
c5169dde3a
@ -1,4 +1,9 @@
|
||||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true,
|
||||
"browser": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
|
@ -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 }}
|
61
.github/workflows/docker-hub.yml
vendored
61
.github/workflows/docker-hub.yml
vendored
@ -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 }}
|
18
.github/workflows/nocobase-test-backend.yml
vendored
18
.github/workflows/nocobase-test-backend.yml
vendored
@ -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
|
||||
|
98
CHANGELOG.md
98
CHANGELOG.md
@ -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
|
||||
|
13
Dockerfile
13
Dockerfile
@ -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/
|
||||
|
||||
|
@ -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"]
|
||||
|
@ -67,7 +67,7 @@ NocoBase 采用插件化架构,所有新功能都可以通过开发和安装
|
||||
|
||||
如果你需要商业版本和商业服务,欢迎通过邮件联系我们:hello@nocobase.com
|
||||
|
||||
也可以添加我们的微信,沟通商业合作或者加入微信群:
|
||||
也可以添加我们的微信,沟通商业合作或者加入用户交流群:
|
||||
|
||||

|
||||
|
||||
|
@ -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': {
|
||||
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__/',
|
||||
|
@ -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,
|
||||
|
21
package.json
21
package.json
@ -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": {}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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": {
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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": {
|
||||
|
2
packages/core/cache/package.json
vendored
2
packages/core/cache/package.json
vendored
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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'], {
|
||||
|
@ -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);
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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' },
|
||||
]);
|
||||
});
|
||||
});
|
@ -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)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -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"
|
||||
};
|
||||
|
711
packages/core/client/src/locale/fr_FR.ts
Normal file
711
packages/core/client/src/locale/fr_FR.ts
Normal 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"
|
||||
};
|
@ -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":"テンプレートのデータ範囲の指定",
|
||||
}
|
||||
|
@ -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":"表格中选中的记录"
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -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']);
|
||||
|
@ -5,7 +5,6 @@ const useStyles = genStyleHook('nb-action', (token) => {
|
||||
|
||||
return {
|
||||
[componentCls]: {
|
||||
'.renderButton': {
|
||||
position: 'relative',
|
||||
'&:hover': { '> .general-schema-designer': { display: 'block' } },
|
||||
'&.nb-action-link': {
|
||||
@ -44,13 +43,6 @@ const useStyles = genStyleHook('nb-action', (token) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'.popover': {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -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,7 +115,6 @@ export const Action: ComposedAction = observer(
|
||||
};
|
||||
|
||||
return wrapSSR(
|
||||
<div className={`${componentCls} ${hashId}`}>
|
||||
<ActionContextProvider
|
||||
button={renderButton()}
|
||||
visible={visible}
|
||||
@ -131,8 +130,7 @@ export const Action: ComposedAction = observer(
|
||||
{!popover && renderButton()}
|
||||
{!popover && <div onClick={(e) => e.stopPropagation()}>{props.children}</div>}
|
||||
{element}
|
||||
</ActionContextProvider>
|
||||
</div>,
|
||||
</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' },
|
||||
);
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
<RecordProvider record={value}>
|
||||
<RecursionField onlyRenderProperties basePath={field.address.concat(index)} schema={fieldSchema} />
|
||||
</RecordProvider>
|
||||
<Divider />
|
||||
</React.Fragment>
|
||||
);
|
||||
|
@ -97,6 +97,7 @@ export const SubTable: any = observer(
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
isSubTable={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -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"
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
if (defaultTemplate.key === 'duplicate') {
|
||||
handleTemplateDataChange(defaultTemplate.dataId, defaultTemplate);
|
||||
}
|
||||
});
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
@ -122,10 +115,16 @@ 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)
|
||||
const handleTemplateChange = useCallback(async (value, option) => {
|
||||
setTargetTemplate(value);
|
||||
setTemplateData(null);
|
||||
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) {
|
||||
// 切换之前先把之前的数据清空
|
||||
@ -143,25 +142,42 @@ export const Templates = ({ style = {}, form }) => {
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
} else {
|
||||
form?.reset();
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!enabled || !display) {
|
||||
return null;
|
||||
}
|
||||
const template = templateOptions?.find((v) => v.key === targetTemplate);
|
||||
|
||||
return (
|
||||
<div style={wrapperStyle}>
|
||||
<Space wrap>
|
||||
<label style={labelStyle}>{t('Data template')}: </label>
|
||||
<Select
|
||||
popupMatchSelectWidth={false}
|
||||
options={templates}
|
||||
options={templateOptions}
|
||||
fieldNames={{ label: 'title', value: 'key' }}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
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
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -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();
|
||||
|
@ -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]: {
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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} />;
|
||||
};
|
||||
|
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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();
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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} />;
|
||||
};
|
@ -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);
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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 的数据就没有意义了,需要清空
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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 };
|
@ -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) => {
|
||||
...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 (
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
};
|
@ -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": {
|
||||
|
@ -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",
|
||||
|
@ -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\)/);
|
||||
|
@ -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",
|
||||
|
@ -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": {
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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) {
|
||||
|
1
packages/core/server/src/locale/index.ts
Normal file
1
packages/core/server/src/locale/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './locale';
|
68
packages/core/server/src/locale/locale.ts
Normal file
68
packages/core/server/src/locale/locale.ts
Normal 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;
|
||||
}
|
||||
}
|
27
packages/core/server/src/locale/resource.ts
Normal file
27
packages/core/server/src/locale/resource.ts
Normal 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);
|
||||
};
|
@ -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",
|
||||
|
@ -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": {
|
||||
|
@ -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 };
|
||||
|
||||
|
58
packages/core/utils/src/koa-multer.ts
Normal file
58
packages/core/utils/src/koa-multer.ts
Normal 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 };
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -16,6 +16,7 @@ const locale = {
|
||||
'7 Days': '7 天',
|
||||
'30 Days': '30 天',
|
||||
'90 Days': '90 天',
|
||||
'Role not found': '角色不存在',
|
||||
};
|
||||
|
||||
export default locale;
|
@ -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"
|
||||
},
|
||||
|
3
packages/plugins/audit-logs/src/locale/fr-FR.ts
Normal file
3
packages/plugins/audit-logs/src/locale/fr-FR.ts
Normal 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
Loading…
x
Reference in New Issue
Block a user