diff --git a/.github/workflows/build-pro-image.yml b/.github/workflows/build-pro-image.yml new file mode 100644 index 000000000..a29539799 --- /dev/null +++ b/.github/workflows/build-pro-image.yml @@ -0,0 +1,101 @@ +name: Build Pro Image + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + branches: + - '**' +jobs: + build-and-push: + if: github.event.pull_request.head.repo.fork != true + runs-on: ubuntu-latest + services: + verdaccio: + image: verdaccio/verdaccio:latest + ports: + - 4873:4873 + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }} + submodules: true + - name: Checkout pro-plugins + uses: actions/checkout@v3 + with: + repository: nocobase/pro-plugins + ref: feat/dataSource-manager + path: packages/pro-plugins + ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }} + - name: rm .git + run: rm -rf packages/pro-plugins/.git && git config --global user.email "you@example.com" && git config --global user.name "Your Name" && git add -A && git commit -m "tmp commit" + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + driver-opts: network=host + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + nocobase/nocobase + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: Login to Aliyun Container Registry + uses: docker/login-action@v2 + with: + registry: ${{ secrets.ALI_DOCKER_REGISTRY }} + username: ${{ secrets.ALI_DOCKER_USERNAME }} + password: ${{ secrets.ALI_DOCKER_PASSWORD }} + + - name: Set tags + id: set-tags + run: | + echo "::set-output name=tags::${{ secrets.ALI_DOCKER_REGISTRY }}/${{ steps.meta.outputs.tags }}-pro" + - name: Set variables + run: | + target_directory="./packages/pro-plugins/@nocobase" + subdirectories=$(find "$target_directory" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | tr '\n' ' ') + trimmed_variable=$(echo "$subdirectories" | xargs) + packageNames="@nocobase/${trimmed_variable// / @nocobase/}" + pluginNames="${trimmed_variable//plugin-/}" + BEFORE_PACK_NOCOBASE="yarn add @nocobase/plugin-notifications @nocobase/plugin-disable-pm-add $packageNames -W" + APPEND_PRESET_LOCAL_PLUGINS="notifications,disable-pm-add,${pluginNames// /,}" + echo "var1=$BEFORE_PACK_NOCOBASE" >> $GITHUB_OUTPUT + echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT + id: vars + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + file: Dockerfile + build-args: | + VERDACCIO_URL=http://localhost:4873/ + COMMIT_HASH=${GITHUB_SHA} + PLUGINS_DIRS=pro-plugins + BEFORE_PACK_NOCOBASE=${{ steps.vars.outputs.var1 }} + APPEND_PRESET_LOCAL_PLUGINS=${{ steps.vars.outputs.var2 }} + push: true + tags: ${{ steps.set-tags.outputs.tags }} + - name: Deploy NocoBase + env: + IMAGE_TAG: ${{ steps.meta.outputs.tags }} + run: | + echo $IMAGE_TAG + export APP_NAME=$(echo $IMAGE_TAG | cut -d ":" -f 2)-pro + echo $APP_NAME + curl --retry 2 --location --request POST "${{secrets.NOCOBASE_DEPLOY_HOST}}$APP_NAME" \ + --header 'Content-Type: application/json' \ + -d "{ + \"tag\": \"$APP_NAME\", + \"dialect\": \"postgres\" + }" diff --git a/.github/workflows/nocobase-test-backend.yml b/.github/workflows/nocobase-test-backend.yml index 8cf462462..5be681806 100644 --- a/.github/workflows/nocobase-test-backend.yml +++ b/.github/workflows/nocobase-test-backend.yml @@ -8,7 +8,6 @@ on: push: branches: - main - - develop paths: - 'package.json' - 'packages/core/acl/**' diff --git a/.github/workflows/nocobase-test-e2e.yml b/.github/workflows/nocobase-test-e2e.yml index 8e7ff6d6f..3edbbecf4 100644 --- a/.github/workflows/nocobase-test-e2e.yml +++ b/.github/workflows/nocobase-test-e2e.yml @@ -67,7 +67,7 @@ jobs: __E2E__: true - run: npx playwright install chromium --with-deps - name: Test with postgres - run: yarn e2e p-test --stop-on-error + run: yarn e2e p-test env: __E2E__: true APP_ENV: production @@ -78,4 +78,4 @@ jobs: DB_USER: nocobase DB_PASSWORD: password DB_DATABASE: nocobase - timeout-minutes: 100 + timeout-minutes: 120 diff --git a/.vscode/launch.json b/.vscode/launch.json index 8069f3847..8ffcb1664 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -26,25 +26,12 @@ "request": "launch", "name": "Debug Tests", "runtimeExecutable": "yarn", - "runtimeArgs": ["run", "--inspect-brk", "test", "${file}"], - "skipFiles": ["/**"], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "windows": { - "runtimeArgs": ["run", "--inspect-brk", "test", "/${fileBasename}"] - } - }, - { - "type": "node", - "request": "launch", - "name": "Debug Client Tests", - "runtimeExecutable": "yarn", - "runtimeArgs": ["run", "test:client", "${file}"], + "runtimeArgs": ["run", "test", "${file}"], "skipFiles": ["/**", "**/node_modules/**", "**/dist/**", "**/lib/**", "**/es/**"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "windows": { - "runtimeArgs": ["run", "test:client", "${relativeFile}"] + "runtimeArgs": ["run", "test", "${relativeFile}"] } }, { @@ -52,12 +39,12 @@ "request": "launch", "name": "Debug E2E Tests", "runtimeExecutable": "yarn", - "runtimeArgs": ["test:e2e", "${file}"], - "skipFiles": ["/**"], + "runtimeArgs": ["e2e", "test", "${file}"], + "skipFiles": ["/**", "**/node_modules/**", "**/dist/**", "**/lib/**", "**/es/**"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "windows": { - "runtimeArgs": ["test:e2e", "${fileBasename}"] + "runtimeArgs": ["e2e", "test", "${fileBasename}"] } }, { diff --git a/README.md b/README.md index 0570c8e52..b91669d81 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ NocoBase is in early stage of development and is subject to frequent changes, pl ## Recent major updates +- [v0.20:Support for multiple data sources - 2024/03/03](https://docs-cn.nocobase.com/welcome/release/v0200-changelog) - [v0.19: Application process optimization - 2024/01/08](https://blog.nocobase.com/posts/release-v019/) - [v0.18: Establish a sound testing system - 2023/12/21](https://blog.nocobase.com/posts/release-v018/) - [v0.17: New SchemaInitializer and SchemaSettings - 2023/12/11](https://blog.nocobase.com/posts/release-v017/) diff --git a/README.zh-CN.md b/README.zh-CN.md index 8e8552412..5de82de03 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -8,6 +8,7 @@ NocoBase 正处在早期开发阶段,可能变动频繁,请谨慎用于生 ## 最近重要更新 +- [v0.20:支持多数据源 - 2024/03/03](https://docs-cn.nocobase.com/welcome/release/v0200-changelog) - [v0.19:应用流程优化 - 2024/01/08](https://blog-cn.nocobase.com/posts/release-v019/) - [v0.18:建立健全的测试体系 - 2023/12/21](https://blog-cn.nocobase.com/posts/release-v018/) - [v0.17:全新的 SchemaInitializer 和 SchemaSettings - 2023/12/11](https://blog-cn.nocobase.com/posts/release-v017/) diff --git a/package.json b/package.json index 546b0c1f8..e9017dcb8 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "ts": "nocobase test:server", "tc": "nocobase test:client", "doc": "nocobase doc", + "doc:cn": "nocobase doc --lang=zh-CN", "postinstall": "nocobase postinstall", "lint": "eslint .", "clean": "nocobase clean", diff --git a/packages/core/acl/src/acl-available-strategy.ts b/packages/core/acl/src/acl-available-strategy.ts index f70f8020f..f6ba69597 100644 --- a/packages/core/acl/src/acl-available-strategy.ts +++ b/packages/core/acl/src/acl-available-strategy.ts @@ -1,5 +1,6 @@ import lodash from 'lodash'; import { ACL } from './acl'; + type StrategyValue = false | '*' | string | string[]; export interface AvailableStrategyOptions { @@ -49,7 +50,7 @@ export class ACLAvailableStrategy { return true; } - if (this.actionsAsObject?.hasOwnProperty(actionName)) { + if (Object.prototype.hasOwnProperty.call(this.actionsAsObject || {}, actionName)) { const predicateName = this.actionsAsObject[actionName]; if (predicateName) { return lodash.cloneDeep(predicate[predicateName]); diff --git a/packages/core/acl/src/acl-role.ts b/packages/core/acl/src/acl-role.ts index c85ffee25..7413e8760 100644 --- a/packages/core/acl/src/acl-role.ts +++ b/packages/core/acl/src/acl-role.ts @@ -186,6 +186,7 @@ export class ACLRole { const resource = this.resources.get(resourceName); let action = null; + if (resource) { action = resource.getAction(actionName); } diff --git a/packages/core/actions/src/__tests__/index.ts b/packages/core/actions/src/__tests__/index.ts index 5e52ef18e..fcf54308c 100644 --- a/packages/core/actions/src/__tests__/index.ts +++ b/packages/core/actions/src/__tests__/index.ts @@ -1,12 +1,10 @@ -import Database, { CollectionOptions, DatabaseOptions, mockDatabase } from '@nocobase/database'; +import Database, { CollectionOptions, mockDatabase } from '@nocobase/database'; import { Handlers, ResourceOptions, Resourcer } from '@nocobase/resourcer'; -import merge from 'deepmerge'; import Koa from 'koa'; import bodyParser from 'koa-bodyparser'; import qs from 'qs'; import supertest, { SuperAgentTest } from 'supertest'; import db2resource from '../../../server/src/middlewares/db2resource'; -import { uid } from '@nocobase/utils'; interface ActionParams { fields?: string[]; diff --git a/packages/core/actions/src/actions/move.ts b/packages/core/actions/src/actions/move.ts index ec9b69780..994a92d66 100644 --- a/packages/core/actions/src/actions/move.ts +++ b/packages/core/actions/src/actions/move.ts @@ -12,7 +12,7 @@ import { import { getRepositoryFromParams } from '../utils'; export async function move(ctx: Context, next) { - const repository = getRepositoryFromParams(ctx); + const repository = ctx.databaseRepository || getRepositoryFromParams(ctx); const { sourceId, targetId, sortField, targetScope, sticky, method } = ctx.action.params; diff --git a/packages/core/app/client/.umirc.ts b/packages/core/app/client/.umirc.ts index 1b0cd40bd..95761bad1 100644 --- a/packages/core/app/client/.umirc.ts +++ b/packages/core/app/client/.umirc.ts @@ -18,6 +18,7 @@ indexGenerator.generate(); export default defineConfig({ title: 'Loading...', + devtool: process.env.NODE_ENV === 'development' ? 'source-map' : false, favicons: ['/favicon/favicon.ico'], metas: [{ name: 'viewport', content: 'initial-scale=0.1' }], links: [ diff --git a/packages/core/auth/src/__tests__/auth-manager.test.ts b/packages/core/auth/src/__tests__/auth-manager.test.ts index 6482aaa2b..d84db75fa 100644 --- a/packages/core/auth/src/__tests__/auth-manager.test.ts +++ b/packages/core/auth/src/__tests__/auth-manager.test.ts @@ -59,7 +59,7 @@ describe('auth-manager', () => { app = mockServer({ registerActions: true, acl: true, - plugins: ['users', 'auth', 'acl'], + plugins: ['users', 'auth', 'acl', 'data-source-manager'], }); // app.plugin(ApiKeysPlugin); diff --git a/packages/core/auth/src/auth-manager.ts b/packages/core/auth/src/auth-manager.ts index 0c8c8e907..a75b0e13c 100644 --- a/packages/core/auth/src/auth-manager.ts +++ b/packages/core/auth/src/auth-manager.ts @@ -96,7 +96,7 @@ export class AuthManager { middleware() { return async (ctx: Context & { auth: Auth }, next: Next) => { const token = ctx.getBearerToken(); - if (token && (await ctx.app.authManager.jwt.blacklist.has(token))) { + if (token && (await ctx.app.authManager.jwt.blacklist?.has(token))) { return ctx.throw(401, ctx.t('token is not available')); } diff --git a/packages/core/build/src/build.ts b/packages/core/build/src/build.ts index 201ff256f..b8f625c95 100755 --- a/packages/core/build/src/build.ts +++ b/packages/core/build/src/build.ts @@ -20,7 +20,8 @@ import { Package } from '@lerna/package'; import { tarPlugin } from './tarPlugin' export async function build(pkgs: string[]) { - process.env.NODE_ENV = 'production'; + const isDev = process.argv.includes('--development'); + process.env.NODE_ENV = isDev ? 'development' : 'production'; const packages = getPackages(pkgs); if (packages.length === 0) { @@ -135,7 +136,8 @@ function runScript(args: string[], cwd: string, envs: Record = { env: { ...process.env, ...envs, - NODE_ENV: 'production', + sourcemap: process.argv.includes('--sourcemap') ? 'sourcemap' : undefined, + NODE_ENV: process.env.NODE_ENV || 'production', }, }); } diff --git a/packages/core/build/src/buildClient.ts b/packages/core/build/src/buildClient.ts index 5f16c6d83..b8259bcdf 100644 --- a/packages/core/build/src/buildClient.ts +++ b/packages/core/build/src/buildClient.ts @@ -32,14 +32,14 @@ export function buildEsm(cwd: string, userConfig: UserConfig, sourcemap: boolean const outDir = path.resolve(cwd, 'es'); return viteBuild( userConfig.modifyViteConfig({ - mode: 'production', + mode: process.env.NODE_ENV || 'production', define: { - 'process.env.NODE_ENV': JSON.stringify('production'), + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), 'process.env.__TEST__': false, 'process.env.__E2E__': process.env.__E2E__ ? true : false, }, build: { - minify: false, + minify: process.env.NODE_ENV === 'production', outDir, cssCodeSplit: true, emptyOutDir: true, @@ -78,13 +78,13 @@ export async function buildLib( await viteBuild( userConfig.modifyViteConfig({ - mode: 'production', + mode: process.env.NODE_ENV || 'production', esbuild: { format: 'cjs', }, build: { outDir, - minify: false, + minify: process.env.NODE_ENV === 'production', sourcemap, lib: { entry: path.join(cwd, 'es/index.ts'), diff --git a/packages/core/build/src/buildPlugin.ts b/packages/core/build/src/buildPlugin.ts index 166f41821..601476ee0 100644 --- a/packages/core/build/src/buildPlugin.ts +++ b/packages/core/build/src/buildPlugin.ts @@ -310,15 +310,15 @@ export async function buildPluginClient(cwd: string, userConfig: UserConfig, sou const outputFileName = 'index.js'; await viteBuild(userConfig.modifyViteConfig({ - mode: 'production', + mode: process.env.NODE_ENV || 'production', define: { - 'process.env.NODE_ENV': JSON.stringify('production'), + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), 'process.env.__TEST__': false, 'process.env.__E2E__': process.env.__E2E__ ? true : false, }, logLevel: 'warn', build: { - minify: true, + minify: process.env.NODE_ENV === 'production', outDir, cssCodeSplit: false, emptyOutDir: true, diff --git a/packages/core/cli/src/commands/p-test.js b/packages/core/cli/src/commands/p-test.js index db9c20d7e..05bcaa06e 100644 --- a/packages/core/cli/src/commands/p-test.js +++ b/packages/core/cli/src/commands/p-test.js @@ -39,7 +39,7 @@ async function runApp(dir, index = 0) { stdio: 'inherit', env: { ...config, - CI: true, + CI: process.env.CI, __E2E__: true, APP_BASE_URL: undefined, LOGGER_LEVEL: 'error', diff --git a/packages/core/client/.dumi/tsconfig.json b/packages/core/client/.dumi/tsconfig.json new file mode 100644 index 000000000..a32dd4f23 --- /dev/null +++ b/packages/core/client/.dumi/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.json", + "include": ["**/*"] +} diff --git a/packages/core/client/.dumirc.ts b/packages/core/client/.dumirc.ts index 2f5ce67ee..ae514893b 100644 --- a/packages/core/client/.dumirc.ts +++ b/packages/core/client/.dumirc.ts @@ -13,6 +13,7 @@ export default defineConfig({ alias: { ...umiConfig.alias, }, + fastRefresh: false, // 热更新会导致 Context 丢失,不开启 // ssr: {}, // exportStatic: { // ignorePreRenderError: true @@ -104,6 +105,106 @@ export default defineConfig({ }, ], }, + { + title: 'Data Source', + type: 'group', + children: [ + { + title: 'DataSourceManager', + link: '/core/data-source/data-source-manager', + }, + { + title: 'DataSourceManagerProvider', + link: '/core/data-source/data-source-manager-provider', + }, + { + title: 'DataSource', + link: '/core/data-source/data-source', + }, + { + title: 'DataSourceProvider', + link: '/core/data-source/data-source-provider', + }, + { + title: 'CollectionManager', + link: '/core/data-source/collection-manager', + }, + { + title: 'CollectionManagerProvider', + link: '/core/data-source/collection-manager-provider', + }, + { + title: 'CollectionTemplateManager', + link: '/core/data-source/collection-template-manager', + }, + { + title: 'CollectionTemplate', + link: '/core/data-source/collection-template', + }, + { + title: 'Collection', + link: '/core/data-source/collection', + }, + { + title: 'CollectionProvider', + link: '/core/data-source/collection-provider', + }, + { + title: 'CollectionMixins', + link: '/core/data-source/collection-mixins', + }, + { + title: 'CollectionField', + link: '/core/data-source/collection-field', + }, + { + title: 'CollectionFieldInterfaceManager', + link: '/core/data-source/collection-field-interface-manager', + }, + { + title: 'CollectionFieldInterface', + link: '/core/data-source/collection-field-interface', + }, + { + title: 'AssociationProvider', + link: '/core/data-source/association-provider', + }, + { + title: 'ExtendCollectionsProvider', + link: '/core/data-source/extend-collections-provider', + }, + ] + }, + { + title: 'DataBlock', + type: 'group', + children: [ + // { + // title: '临时', + // link: '/core/data-block/tmp', + // }, + { + title: 'Record', + link: '/core/data-block/collection-record', + }, + { + title: 'CollectionRecordProvider', + link: '/core/data-block/collection-record-provider', + }, + { + title: 'DataBlockProvider', + link: '/core/data-block/data-block-provider', + }, + { + title: 'DataBlockResourceProvider', + link: '/core/data-block/data-block-resource-provider', + }, + { + title: 'DataBlockRequestProvider', + link: '/core/data-block/data-block-request-provider', + }, + ] + } ], // '/ui-schema': [ // { diff --git a/packages/core/client/docs/en-US/core/application/application.md b/packages/core/client/docs/en-US/core/application/application.md index 77d44d288..b33fe9bbf 100644 --- a/packages/core/client/docs/en-US/core/application/application.md +++ b/packages/core/client/docs/en-US/core/application/application.md @@ -8,7 +8,7 @@ ```tsx | pure export interface ApplicationOptions { - apiClient?: APIClientOptions; + apiClient?: APIClientOptions | APIClient; ws?: WebSocketClientOptions | boolean; i18n?: i18next; providers?: (ComponentType | ComponentAndProps)[]; @@ -19,6 +19,7 @@ export interface ApplicationOptions { schemaSettings?: SchemaSetting[]; schemaInitializers?: SchemaInitializer[]; loadRemotePlugins?: boolean; + dataSourceManager?: DataSourceManagerOptions; } ``` @@ -33,7 +34,7 @@ export interface ApplicationOptions { - schemaSettings:Schema 设置工具,具体参考:[SchemaSettingsManager](/core/ui-schema/schema-initializer-manager) - schemaInitializers:Schema 添加工具,具体参考:[SchemaInitializerManager](/core/ui-schema/schema-initializer-manager) - loadRemotePlugins:用于控制是否加载远程插件,默认为 `false`,即不加载远程插件(方便单测和 DEMO 环境)。 - + - dataSourceManager:数据源管理器,具体参考:[DataSourceManager](/core/data-source/data-source-manager) - 示例 ```tsx @@ -108,6 +109,10 @@ class Application { 详细介绍,请参考:[SchemaInitializerManager](/core/ui-schema/schema-initializer-manager) +### app.dataSourceManager + +详细介绍,请参考:[dataSourceManager](/core/data-source/data-source-manager) + ## 实例方法 ### app.getRootComponent() diff --git a/packages/core/client/docs/en-US/core/application/plugin.md b/packages/core/client/docs/en-US/core/application/plugin.md index 839bfb964..5bfcfe42c 100644 --- a/packages/core/client/docs/en-US/core/application/plugin.md +++ b/packages/core/client/docs/en-US/core/application/plugin.md @@ -34,6 +34,10 @@ class Plugin { return this.app.schemaSettingsManager; } + get dataSourceManager() { + return this.app.dataSourceManager; + } + async afterAdd() {} async beforeLoad() {} diff --git a/packages/core/client/docs/en-US/core/data-block/collection-record-provider.md b/packages/core/client/docs/en-US/core/data-block/collection-record-provider.md new file mode 100644 index 000000000..0cfad105f --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/collection-record-provider.md @@ -0,0 +1,259 @@ +# CollectionRecordProvider + +用于提供 [CollectionRecord](./collection-record) 实例。 + +## 组件 + +- 类型 + +```tsx | pure +interface CollectionRecordProviderProps { + isNew?: boolean; + record?: CollectionRecord | DataType; + parentRecord?: CollectionRecord | DataType; + /** + * 当前记录所属的 collection name + */ + collectionName?: string; +} +``` + +- 详解 + +参数的具体说明参见 [CollectionRecord](./collection-record)。 + +需要说明的是 `record` 和 `parentRecord` 即可以是普通的对象,也可以是 [CollectionRecord](./collection-record) 实例,但最终会转为 `CollectionRecord` 实例,并通过 context 传递给子组件。 + +## 示例 + +- record 参数为 CollectionRecord 实例 + +```tsx | pure +import { CollectionRecord, CollectionRecordProvider } from '@nocobase/client'; + +const record = new CollectionRecord({ data: { id: 1, name: 'foo' } }); + + +// 最终向子组件传递的数据为: props.record +``` + +```tsx +import { CollectionRecord, useCollectionRecord, CollectionRecordProvider } from '@nocobase/client'; + +const record = new CollectionRecord({ data: { id: 1, name: 'foo' } }); + +const Demo = () => { + const record = useCollectionRecord(); + return
{JSON.stringify(record, null, 2)}
; +} + +export default () => +``` + +- record 参数为普通对象 + +```tsx | pure + +// 最终向子组件传递的数据为: const record = new CollectionRecord({ data: { id: 1, name: 'foo' } }); +``` + +```tsx +import { useCollectionRecord, CollectionRecordProvider } from '@nocobase/client'; + +const Demo = () => { + const record = useCollectionRecord(); + return
{JSON.stringify(record, null, 2)}
; +} + +export default () => +``` + +- record 参数为 CollectionRecord 实例且带有父记录 + +```tsx | pure +const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } }); +const record = new CollectionRecord({ data: { id: 1, name: 'foo' }, parentRecord }); + + +// 最终向子组件传递的数据为: props.record +``` + +```tsx +import { CollectionRecord, useCollectionRecord, CollectionRecordProvider } from '@nocobase/client'; + +const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } }); +const record = new CollectionRecord({ data: { id: 1, name: 'foo' }, parentRecord }); + +const Demo = () => { + const record = useCollectionRecord(); + return
{JSON.stringify(record, null, 2)}
; +} + +export default () => +``` + +- record 参数为 CollectionRecord 实例,父记录通过 `parentRecord` 参数传递 + +```tsx | pure +const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } }); +const record = new CollectionRecord({ data: { id: 1, name: 'foo' } }); + + + +// 首先设置父记录:record.setParentRecord(parentRecord); +// 最终向子组件传递的数据为:record(带有父记录) +``` + +```tsx +import { CollectionRecord, useCollectionRecord, CollectionRecordProvider } from '@nocobase/client'; + +const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } }); +const record = new CollectionRecord({ data: { id: 1, name: 'foo' } }); + +const Demo = () => { + const record = useCollectionRecord(); + return
{JSON.stringify(record, null, 2)}
; +} + +export default () => +``` + +- record 参数为普通对象,父记录也是普通对象 + +```tsx | pure + + +// 首先实例化父记录:const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } }); +// 然后实例化记录:const record = new CollectionRecord({ data: { id: 1, name: 'foo' } }); +// 最后设置父记录:record.setParentRecord(parentRecord); +// 最终向子组件传递的数据为:record(带有父记录) +``` + + +```tsx +import { useCollectionRecord, CollectionRecordProvider } from '@nocobase/client'; + +const Demo = () => { + const record = useCollectionRecord(); + return
{JSON.stringify(record, null, 2)}
; +} + +export default () => +``` + +## Hooks + +### useCollectionRecord() + +用于获取 `CollectionRecordProvider` 组件传递的数据记录。 + +- 示例 + +```tsx | pure +const record = useCollectionRecord(); + +console.log(record, record.data, record.parentRecord, record.parentRecord.data); +``` + +```tsx +import { useCollectionRecord, CollectionRecordProvider } from '@nocobase/client'; + +const Demo = () => { + const record = useCollectionRecord(); + return
+
record:
{JSON.stringify(record, null, 2)}
+
record.data:
{JSON.stringify(record.data, null, 2)}
+
record.parentRecord:
{JSON.stringify(record.parentRecord, null, 2)}
+
record.parentRecord.data:
{JSON.stringify(record.parentRecord.data, null, 2)}
+
; +} + +export default () => +``` + +### useCollectionRecordData() + +直接获取 CollectionRecord 的 `data` 属性,等同于 `useCollectionRecord().data`。 + +- 示例 + +```tsx | pure +const data = useCollectionRecordData(); +const record = useCollectionRecord(); +console.log(data === record.data); +``` + +```tsx +import { useCollectionRecord, useCollectionRecordData, CollectionRecordProvider } from '@nocobase/client'; + +const Demo = () => { + const data = useCollectionRecordData(); + const record = useCollectionRecord(); + return
+
data === record.data: { JSON.stringify(data === record.data) }
+
data:
{JSON.stringify(data, null, 2)}
+
record.data:
{JSON.stringify(record.data, null, 2)}
+
; +} + +export default () => +``` + +### useCollectionParentRecord() + +直接获取 CollectionRecord 的数据 `parentRecord`,等同于 `useCollectionRecord().parentRecord`。 + +- 示例 + +```tsx | pure +const parentRecord = useCollectionParentRecord(); +const record = useCollectionRecord(); +console.log(parentRecord === record.parentRecord); +``` + +```tsx +import { useCollectionRecord, useCollectionParentRecord, CollectionRecordProvider } from '@nocobase/client'; + +const Demo = () => { + const record = useCollectionRecord(); + const parentRecord = useCollectionParentRecord(); + return
+
parentRecord === record.parentRecord: { JSON.stringify(parentRecord === record.parentRecord) }
+
parentRecord:
{JSON.stringify(parentRecord, null, 2)}
+
record.parentRecord:
{JSON.stringify(record.parentRecord, null, 2)}
+
; +} + +export default () => +``` + + +### useCollectionParentRecordData() + +直接获取 CollectionRecord 的数据 `parentRecord.data`,等同于 `useCollectionRecord().parentRecord.data`。 + +- 示例 + +```tsx | pure +const record = useCollectionRecord(); +const parentData = useCollectionParentRecordData(); +const parentRecord = useCollectionParentRecord(); +console.log(parentData === parentRecord.data === record.parentRecord.data); +``` + +```tsx +import { useCollectionRecord, CollectionRecordProvider, useCollectionParentRecordData, useCollectionParentRecord } from '@nocobase/client'; + +const Demo = () => { + const record = useCollectionRecord(); + const parentData = useCollectionParentRecordData(); + const parentRecord = useCollectionParentRecord(); + return
+
parentData:
{JSON.stringify(parentData, null, 2)}
+
parentRecord.data:
{JSON.stringify(parentRecord.data, null, 2)}
+
record.parentRecord.data:
{JSON.stringify(record.parentRecord.data, null, 2)}
+
; +} + +export default () => +``` diff --git a/packages/core/client/docs/en-US/core/data-block/collection-record.md b/packages/core/client/docs/en-US/core/data-block/collection-record.md new file mode 100644 index 000000000..d2bc6ffa4 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/collection-record.md @@ -0,0 +1,194 @@ +# CollectionRecord + +数据表中的一条记录。 + +## 类型 + +```tsx | pure +interface CollectionRecordOptions { + isNew?: boolean; + data?: DataType; + parentRecord?: CollectionRecord; + /** + * 当前记录所属的 collection name + */ + collectionName?: string; +} + +class CollectionRecord { + public isNew?: boolean; + public data?: DataType; + public parentRecord?: CollectionRecord; + public collectionName?: string; + constructor(options: CollectionRecordOptions) {} + + setData(data: DataType) { + this.data = data; + } + + setParentRecord(parentRecord: CollectionRecord) { + this.parentRecord = parentRecord; + } +} +``` + +## 详解 + +### CollectionRecord 基本概念 + +CollectionRecord 类用于提供数据记录,通常情况下对应着后端数据表中的一条记录。以用户表为例,其一条数据对应的 CollectionRecord 类如下: + +```tsx | pure +const useCollectionRecord = new CollectionRecord({ + data: { + "id": 1, + "roleId": 10, + "appLang": null, + "createdById": null, + "email": "test@nocobase.com", + "nickname": "Admin", + "phone": null, + "systemSettings": {}, + "updatedById": null, + "username": "nocobase", + "createdAt": "2023-12-04T09:42:52.953Z", + "updatedAt": "2023-12-04T09:42:52.953Z", + } +}); +``` + +### CollectionRecord 和 Collection 的关系 + +CollectionRecord 是指的数据,而 Collection 则是表结构。对于上面的用户表,其对应的 Collection 如下: + +```tsx | pure +const usersCollection = new Collection({ + name: 'users', + fields: [ + { + type: 'bigInt', + name: 'id', + }, + { + type: 'string', + name: 'username', + }, + { + type: 'integer', + name: 'age', + }, + { + "name": "email", + "type": "string", + }, + // .... + ], +}); +``` + +### 父子关系和关系字段 + +对于[关系字段](https://docs.nocobase.com/development/server/collections/association-fields),例如用户和角色的关系,在用户表中会有一个 `roleId` 字段,其值为角色表中的 `id`,当我们通过 `users.roleId` 字段查询用户的角色时: + +```bash | pure +GET /api/users/1/roles:get/10 +``` + +其中 `1` 为用户的 `id`,`10` 为角色的 `id`,我们可以得到用户的角色数据: + +```tsx | pure +const roleRecord = new CollectionRecord({ + data: { + "id": 10, + "name": "member", + "title": "test role", + "strategy": { + "actions": [ + "view", + "update:own", + "destroy:own", + "create" + ] + }, + "createdAt": "2023-03-30T07:53:10.924Z", + "updatedAt": "2023-12-15T02:51:43.577Z", + } +}) +``` + +其中 `users` id 为 1 的记录我们称之为父记录: + +```tsx | pure +roleRecord.setParentRecord(userRecord); +``` + +### 新记录 + +对于新表单,我们可以通过 `isNew` 属性来标识: + +```tsx | pure +const record = new CollectionRecord({ + isNew: true, +}); +``` + +## 示例 + +### 基本使用 + +```tsx | pure +import { CollectionRecord } from '@nocobase/client'; + +const record = new CollectionRecord({ + data: { + name: 'foo', + } +}); +``` + +### 创建空记录 + +```tsx | pure +import { CollectionRecord } from '@nocobase/client'; + +const record = new CollectionRecord({ + isNew: true, +}); +``` + +### 设置 parentRecord + +方式1: 通过构造函数设置 + +```tsx | pure +const parentRecord = new CollectionRecord({ + data: { + foo: 'foo', + } +}); + +const record = new CollectionRecord({ + data: { + name: 'bar', + }, + parentRecord, +}); +``` + +方式2: 通过 `setParentRecord` 方法设置 + +```tsx | pure +const parentRecord = new CollectionRecord({ + data: { + foo: 'foo', + } +}); + +const record = new CollectionRecord({ + data: { + name: 'bar', + } +}); + +record.setParentRecord(parentRecord); +``` diff --git a/packages/core/client/docs/en-US/core/data-block/data-block-provider.md b/packages/core/client/docs/en-US/core/data-block/data-block-provider.md new file mode 100644 index 000000000..6aeaec049 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/data-block-provider.md @@ -0,0 +1,246 @@ +# DataBlockProvider + +## 区块类型 + +区块分为简单区块和包含各种数据的区块。 + +### 简单区块 + +简单区块例如 Markdown 区块。 + +![](./images/markdown-block.png) + +它只有文本内容,没有其他更多复杂数据,且文本内容是存储在 `schema` 中的,没有存储在数据库中。 + +```json {5}| pure +{ + "type": "void", + "x-component": "Markdown.Void", + "x-component-props": { + "content": "markdown content" + }, +} +``` + +### 数据区块 + +数据区块是指区块的数据存储在服务端的数据表中,例如 Table 组件。 + +![](./images/data-block.png) + +Table 中的字段信息及列表数据,都是存储在数据库中的。 + + +## DataBlockProvider 介绍 + +为了方便对数据区块的数据进行管理,我们提供了 `DataBlockProvider` 组件,其内部封装了: + +- `DataBlockProvider`:封装了下面的所有组件,并提供了区块属性 + - [CollectionProvider](/core/data-source/collection-provider) / [AssociationProvider](/core/data-source/association-provider): 根据 `DataBlockProvider` 提供的上下文信息,查询对应数据表数据及关系字段信息并传递 + - [BlockResourceProvider](/core/data-block/data-block-resource-provider): 根据 `DataBlockProvider` 提供的上下文信息,构建区块 [Resource](https://docs.nocobase.com/api/sdk#resource-action) API,用于区块数据的增删改查 + - [BlockRequestProvider](/core/data-block/data-block-request-provider): 根据 `DataBlockProvider` 提供的上下文信息,自动调用 `BlockResourceProvider` 提供的 `resource.get()` 或 `resource.list()` 发起请求,得到区块数据,并传递 + - [CollectionRecordProvider](/core/data-source/record-provider): 对于 `resource.get()` 场景,会自动嵌套 `CollectionRecordProvider` 并将 `resource.get()` 请求结果传递下去,`resource.list()` 场景则需要自行使用 `CollectionRecordProvider` 提供数据记录 + +```tsx | pure +const DataBlockProvider = (props) => { + return + + / + + + {action !== 'list' && + {props.children} + } + + / + + + +} +``` + +上述组件封装到 `DataBlockProvider` 的内部,只需要使用 `DataBlockProvider` 即可自行得到上述数据。 + +### 使用方式 + +其主要使用在区块的 schema [x-decorator](https://docs.nocobase.com/development/client/ui-schema/what-is-ui-schema#x-decorator) 中,例如: + +```js {5}| pure +{ + type: 'void', + name: 'hello-block', + 'x-component': 'CardItem', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + action: 'list', + tableProps: { + bordered: true, + } + }, + 'x-use-decorator-props': 'useDynamicDataBlockProps', +} +``` + +### 完整示例 + + + +## 属性 + +### 静态属性和动态属性 + +- schema 中的 `x-decorator-props` 称为静态属性,它是一个普通对象,记录区块的配置信息 +- schema 中的 `x-use-decorator-props` 中的属性称为动态属性,它是一个 React hook,可用于获取例如 URL 上的 ID,或者父级的 context 数据 + +当两者都存在时,会进行深度合并,作为 `DataBlockProvider` 的属性。 + +### 属性详解 + +```ts | pure +interface AllDataBlockProps { + collection?: string; + association?: string; + sourceId?: string | number; + record?: Record; + action?: 'list' | 'get'; + filterByTk?: string; + params?: Record; + parentRecord?: Record; + [index: string]: any; +} +``` + +- collection(`x-decorator-props`):区块的 collection 表名,用于获取区块的字段信息和区块数据 +- association(`x-decorator-props`):区块的关系字段名,用于获取区块的关系字段信息和关系字段数据 +- dataSource(`x-decorator-props`): 数据源,具体可参考 [Data Modeling](https://docs.nocobase.com/manual/data-modeling) +- action(`x-decorator-props`):区块的请求类型,`list` 或 `get` +- params(`x-decorator-props` 和 `x-use-decorator-props`):区块的请求参数,同时存在于 +- filterByTk(`x-use-decorator-props`):相当于 `params.filterByTk`,可理解为 `id`,用于获取单条数据 +- sourceId(`x-use-decorator-props`):区块的 sourceId,配合 `association` 使用,用于获取区块的关系字段数据 +- record(`x-use-decorator-props`):当提供 `record` 时,会使用 `record` 作为区块的数据,不发起请求 +- parentRecord(`x-use-decorator-props`):当提供 `parentRecord` 时,会使用 `parentRecord` 作为关系字段的表数据,不发起请求 + +```tsx | pure +const DataBlockProvider = (props) => { + return + + / + + + {action !== 'list' && + {props.children} + } + + + + + +} +``` + +### 属性组合和场景 + +这些属性根据不同的场景,共有 8 中情况: + +- collection + - 创建:`collection` + - 获取单条数据:`collection` + `action: get` + `params` + - 获取列表数据:`collection` + `action: list` + `params` + - 使用 `record` 作为数据:`collection` + `record` + +对于 *获取单条数据* 和 *获取列表数据* `params` 非必须。 + +- association + - 创建:`association` + `sourceId` + - 获取单条数据:`association` + `sourceId` + `action: get` + `params` + `parentRecord` + - 获取列表数据:`association` + `sourceId` + `action: list` + `params` + `parentRecord` + - 使用 `record` 作为数据:`association` + `sourceId` + `record` + `parentRecord` + + +对于 *获取单条数据* 和 *获取列表数据* `params` 和 `parentRecord` 非必须,当没有 `parentRecord` 会根据 `association` 查询到对应的 `collection`,然后再根据 `collection` 查询到对应的 `parentRecord`。 + +### 属性获取和修改 + +#### useDataBlock() + +可用于获取和修改 `DataBlockProvider` 的属性。 + +- 类型 + +```tsx | pure +interface Result { + props: AllDataBlockProps & T; + dn: Designable; +} +const useDataBlock: () => Result +``` + +- 详解 + +`props` 就对应着上面的 `AllDataBlockProps`。 +`dn` 是 `Designable` 对象,可用于修改 `DataBlockProvider` 的 UI schema,详细见 [Designable](/core/ui-schema/designable)。 + +- 示例 + +```tsx | pure +const { props, dn } = useDataBlock<{ tableProps: { bordered?: boolean } }>(); + +// 获取 +const checked = props.tableProps.bordered; + +// 修改 +dn.deepMerge({ + 'x-decorator-props': { + tableProps: { + bordered: !checked, + }, + }, +}); +``` + +#### useDataBlockProps() + +相当于 `useDataBlock().props`。 + +```tsx | pure +const props = useDataBlockProps<{ tableProps: { bordered?: boolean } }>(); + +const checked = props.tableProps.bordered; +``` + +## 示例 + +### collection + +#### Table list + + + +#### Form get & update + + + +#### Form create + + + +#### Form record & update + + + +### association + +association 与 collection 类似,只是需要提供 `sourceId`,我们以 `Table list` 为例。 + +#### Table list & sourceId + + + +#### Table list & parentRecord + +如果不提供 `sourceId`,则需要提供 `parentRecord`,我们以 `Table list` 为例。 + + + diff --git a/packages/core/client/docs/en-US/core/data-block/data-block-request-provider.md b/packages/core/client/docs/en-US/core/data-block/data-block-request-provider.md new file mode 100644 index 000000000..48b5801b3 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/data-block-request-provider.md @@ -0,0 +1,139 @@ +# DataBlockRequestProvider + +其内部获取到 [BlockResourceProvider](/core/data-block/data-block-resource-provider) 提供的 `resource`,根据 [BlockProvider](/core/data-block/data-block-provider) 提供的参数,自动调用 `resource.get()` 或者 `resource.list()` 获取的区块的数据,并通过 context 传递下去。 + +## 请求参数 + +请求参数是获取 `DataBlockProvider` 提供中的 `params` 和 `filterByTk`。 + +```ts | pure +const schema = { + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + 'collection': 'users', + 'action': 'list', + // 静态参数 + params: { + pageSize: 10, + } + }, + // 动态参数 + 'x-use-decorator-props': 'useDynamicDataBlockProps', +} + +const useDynamicDataBlockProps: UseDataBlockProps<'CollectionList'> = () => { + return { + params: { + size: 15, + } + } +} +``` + +会自动调用 `resource.list()` 获取数据,发起 `GET /api/users:list?pageSize=10&size=15` 的请求。 + +## Hooks + +### useDataBlockRequest() + +用于获取请求对象,一般用区块组件中。 + +```tsx | pure +const MyTable = () => { + const { data, loading } = useDataBlockRequest(); + + return ( + + ) +} +``` + +## Record + +### Get 请求 + +对于 `get` 请求,当获取到 `data` 数据后,会通过 `CollectionRecordProvider` 提供 `record` 对象,用于获取当前区块的数据。 + +```ts | pure +const schema = { + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + 'collection': 'users', + 'action': 'get', // get 请求 + }, + // 动态参数 + 'x-use-decorator-props': 'useDynamicFormProps', +} + +const useDynamicDataBlockProps: UseDataBlockProps<'CollectionGet'> = () => { + return { + params: { + filterByTk: 1, + } + } +} +``` + +会自动调用 `resource.get()` 获取数据,发起 `GET /api/users:get/1` 的请求,并通过 `CollectionRecordProvider` 提供上下文。 + +```tsx | pure +const { data } = useDataBlockRequest(); +const record = useCollectionRecord(); // record 上下文数据 + +// 相等 +record.data === data; +``` + +### List 请求 + +对于 `list` 请求则不会提供 `record` 对象,需要自己通过 `` 设置上下文。 + +```tsx | pure +const MyTable = () => { + const { data } = useDataBlockRequest(); + + return ( +
{ + return ( + + + + ) + }, + }, + ]} + pagination={{ + total: data?.meta.total, + pageSize: data?.meta.pageSize, + page: data?.meta.page, + }} + /> + ) +} + +const MyAction = () => { + const record = useCollectionRecord(); + return ( + + ) +} +``` diff --git a/packages/core/client/docs/en-US/core/data-block/data-block-resource-provider.md b/packages/core/client/docs/en-US/core/data-block/data-block-resource-provider.md new file mode 100644 index 000000000..e94246b6c --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/data-block-resource-provider.md @@ -0,0 +1,36 @@ +# DataBlockResourceProvider + +根据 `DataBlockProvider` 中的 `collection`、`association`、`sourceId` 等属性,构建好 [resource](https://docs.nocobase.com/api/sdk#resource-action) 对象,方便子组件对区块数据的增删改查操作,其内置在 [DataBlockProvider](/core/data-block/data-block-provider) 中 + + +## useDataBlockResource + +用于获取当前数据块的 resource 对象。 + +- 类型 + +```ts | pure +function useDataBlockResource(): IResource +``` + +- 示例 + +```ts | pure +const resource = useDataBlockResource(); + +const onSubmit = async (values) => { + // 创建 + await resource.create({ values }); +} +``` + +```ts | pure +const resource = useDataBlockResource(); + +const onDelete = async () => { + // 删除 + await resource.destroy(); +} +``` + +可运行示例 diff --git a/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/association-table-list-and-parent-record.tsx b/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/association-table-list-and-parent-record.tsx new file mode 100644 index 000000000..27af6235f --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/association-table-list-and-parent-record.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { Table, TableProps } from 'antd'; +import { SchemaComponent, UseDataBlockProps, useDataBlockRequest, withDynamicSchemaProps } from '@nocobase/client'; +import { ISchema } from '@formily/json-schema'; + +import { createApp } from '../../../data-source/demos/createApp'; + +const collection = 'users'; +const associationField = 'roles'; + +const association = `${collection}.${associationField}`; +const action = 'list'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-use-decorator-props': 'useBlockDecoratorProps', + 'x-decorator-props': { + association, + action, + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + }, + }, +}; + +const MyTable = withDynamicSchemaProps(Table); + +function useTableProps(): TableProps { + const { data, loading } = useDataBlockRequest(); + return { + loading, + dataSource: data?.data || [], + columns: [ + { + title: 'Name', + dataIndex: 'name', + }, + { + title: 'Title', + dataIndex: 'title', + }, + { + title: 'Description', + dataIndex: 'description', + }, + ], + }; +} + +const useBlockDecoratorProps: UseDataBlockProps<'CollectionList'> = () => { + const parentRecord = { + id: 1, + username: 'Tom', + }; + return { + parentRecord, + }; +}; + +const Demo = () => { + return ; +}; + +const mocks = { + [`${collection}/1/${associationField}:${action}`]: { + data: [ + { + name: 'admin', + title: 'Admin', + description: 'Admin description', + }, + { + name: 'developer', + title: 'Developer', + description: 'Developer description', + }, + ], + }, +}; + +const Root = createApp( + Demo, + { + components: { MyTable }, + scopes: { useTableProps, useBlockDecoratorProps }, + }, + mocks, +); + +export default Root; diff --git a/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/association-table-list-and-source-id.tsx b/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/association-table-list-and-source-id.tsx new file mode 100644 index 000000000..a7594c7b5 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/association-table-list-and-source-id.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { Select, Table, TableProps } from 'antd'; +import { SchemaComponent, UseDataBlockProps, useDataBlockRequest, withDynamicSchemaProps } from '@nocobase/client'; +import { ISchema } from '@formily/json-schema'; + +import { createApp } from '../../../data-source/demos/createApp'; +import useUrlState from '@ahooksjs/use-url-state'; + +const collection = 'users'; +const associationField = 'roles'; + +const association = `${collection}.${associationField}`; +const action = 'list'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-use-decorator-props': 'useBlockDecoratorProps', + 'x-decorator-props': { + association, + action, + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + }, + }, +}; + +const MyTable = withDynamicSchemaProps(Table); + +function useTableProps(): TableProps { + const { data, loading } = useDataBlockRequest(); + return { + loading, + dataSource: data?.data || [], + columns: [ + { + title: 'Name', + dataIndex: 'name', + }, + { + title: 'Title', + dataIndex: 'title', + }, + { + title: 'Description', + dataIndex: 'description', + }, + ], + }; +} + +const useBlockDecoratorProps: UseDataBlockProps<'CollectionList'> = () => { + const [state] = useUrlState({ userId: '1' }); + return { + sourceId: state.userId, + }; +}; + +const Demo = () => { + const [state, setState] = useUrlState({ userId: '1' }); + return ( + <> + + + + ); +}; + +const mocks = { + [`${collection}/1/${associationField}:${action}`]: { + data: [ + { + name: 'admin', + title: 'Admin', + description: 'Admin description', + }, + { + name: 'developer', + title: 'Developer', + description: 'Developer description', + }, + ], + }, + [`${collection}/2/${associationField}:${action}`]: { + data: [ + { + name: 'developer', + title: 'Developer', + description: 'Developer description', + }, + { + name: 'tester', + title: 'Tester', + description: 'Tester description', + }, + ], + }, + [`${collection}:get/1`]: { + id: 1, + username: 'Tom', + }, + [`${collection}:get/2`]: { + id: 1, + username: 'Jack', + }, +}; + +const Root = createApp( + Demo, + { + components: { MyTable }, + scopes: { useTableProps, useBlockDecoratorProps }, + }, + mocks, +); + +export default Root; diff --git a/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/collection-form-create.tsx b/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/collection-form-create.tsx new file mode 100644 index 000000000..078c7dd7a --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/collection-form-create.tsx @@ -0,0 +1,95 @@ +import React, { FC } from 'react'; +import { Button, Form, FormProps, Input, InputNumber, notification } from 'antd'; +import { SchemaComponent, useDataBlockResource, withDynamicSchemaProps } from '@nocobase/client'; +import { ISchema } from '@formily/json-schema'; + +import { createApp } from '../../../data-source/demos/createApp'; + +interface DemoFormFieldType { + id: number; + username: string; + age: number; +} +type DemoFormProps = FormProps; +const DemoForm: FC = withDynamicSchemaProps((props) => { + return ( +
+ + label="Username" + name="username" + rules={[{ required: true, message: 'Please input your username!' }]} + > + + + + label="Age" + name="age" + rules={[{ required: true, message: 'Please input your age!' }]} + > + + + + + + + ); +}); + +function useDemoFormProps(): DemoFormProps { + const resource = useDataBlockResource(); + const onFinish = async (values: DemoFormFieldType) => { + console.log('values', values); + await resource.create({ + values, + }); + notification.success({ + message: 'Save successfully!', + }); + }; + + return { + onFinish, + }; +} + +const collection = 'users'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: collection, + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'object', + 'x-component': 'DemoForm', + 'x-use-component-props': 'useDemoFormProps', + }, + }, +}; + +const Demo = () => { + return ; +}; + +const mocks = { + [`${collection}:create`]: (config) => { + console.log('config.data', config.data); + return [200, { msg: 'ok' }]; + }, +}; +const Root = createApp( + Demo, + { + components: { DemoForm }, + scopes: { useDemoFormProps }, + }, + mocks, +); + +export default Root; diff --git a/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/collection-form-get-and-update.tsx b/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/collection-form-get-and-update.tsx new file mode 100644 index 000000000..1383a5180 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/collection-form-get-and-update.tsx @@ -0,0 +1,158 @@ +import React, { FC, useEffect } from 'react'; +import { Button, Form, FormProps, Input, InputNumber, Select, notification } from 'antd'; +import { + SchemaComponent, + UseDataBlockProps, + useDataBlockResource, + useCollectionRecordData, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { ISchema } from '@formily/json-schema'; +import useUrlState from '@ahooksjs/use-url-state'; + +import { createApp } from '../../../data-source/demos/createApp'; + +interface DemoFormFieldType { + id: number; + username: string; + age: number; +} +type DemoFormProps = FormProps; +const DemoForm: FC = withDynamicSchemaProps((props) => { + return ( +
+ + label="Username" + name="username" + rules={[{ required: true, message: 'Please input your username!' }]} + > + + + + label="Age" + name="age" + rules={[{ required: true, message: 'Please input your age!' }]} + > + + + + + + + ); +}); + +function useDemoFormProps(): DemoFormProps { + const data = useCollectionRecordData(); + const resource = useDataBlockResource(); + + const [form] = Form.useForm(); + + useEffect(() => { + form.setFieldsValue(data); + }, [data, form]); + + const onFinish = async (values: DemoFormFieldType) => { + console.log('values', values); + await resource.update({ + filterByTk: data.id, + values, + }); + notification.success({ + message: 'Save successfully!', + }); + }; + + return { + initialValues: data, + preserve: true, + onFinish, + form, + }; +} +const useBlockDecoratorProps: UseDataBlockProps<'CollectionGet'> = () => { + const [state] = useUrlState({ id: '1' }); + return { + filterByTk: state.id, + }; +}; + +const collection = 'users'; +const action = 'get'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-use-decorator-props': 'useBlockDecoratorProps', + 'x-decorator-props': { + collection: collection, + action: action, + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'object', + 'x-component': 'DemoForm', + 'x-use-component-props': 'useDemoFormProps', + }, + }, +}; + +const Demo = () => { + const [state, setState] = useUrlState({ id: '1' }); + + return ( + <> + + + + ); +}; + +const mocks = { + [`${collection}:${action}`]: function (config) { + const { filterByTk } = config.params; + return { + data: + Number(filterByTk) === 1 + ? { + id: 1, + username: 'Bamboo', + age: 18, + } + : { + id: 2, + username: 'Mary', + age: 25, + }, + }; + }, + [`${collection}:update`]: function (config) { + console.log('config.data', config.data); + return { + data: 'ok', + }; + }, +}; +const Root = createApp( + Demo, + { + components: { DemoForm }, + scopes: { useDemoFormProps, useBlockDecoratorProps }, + }, + mocks, +); + +export default Root; diff --git a/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/collection-form-record-and-update.tsx b/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/collection-form-record-and-update.tsx new file mode 100644 index 000000000..2cd8518e0 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/collection-form-record-and-update.tsx @@ -0,0 +1,137 @@ +import React, { FC, useEffect } from 'react'; +import { Button, Form, FormProps, Input, InputNumber, notification } from 'antd'; +import { + CollectionRecordProvider, + SchemaComponent, + UseDataBlockProps, + useDataBlockResource, + useCollectionRecordData, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { ISchema } from '@formily/json-schema'; + +import { createApp } from '../../../data-source/demos/createApp'; + +interface DemoFormFieldType { + id: number; + username: string; + age: number; +} +type DemoFormProps = FormProps; +const DemoForm: FC = withDynamicSchemaProps((props) => { + return ( +
+ + label="Username" + name="username" + rules={[{ required: true, message: 'Please input your username!' }]} + > + + + + label="Age" + name="age" + rules={[{ required: true, message: 'Please input your age!' }]} + > + + + + + + + ); +}); + +function useDemoFormProps(): DemoFormProps { + const data = useCollectionRecordData(); + const resource = useDataBlockResource(); + + const [form] = Form.useForm(); + + useEffect(() => { + form.setFieldsValue(data); + }, [data, form]); + + const onFinish = async (values: DemoFormFieldType) => { + console.log('values', values); + await resource.update({ + filterByTk: data.id, + values, + }); + notification.success({ + message: 'Save successfully!', + }); + }; + + return { + initialValues: data, + preserve: true, + onFinish, + form, + }; +} +const useFormBlockDecoratorProps: UseDataBlockProps<'CollectionRecord'> = () => { + const record = useCollectionRecordData(); + return { + record, + }; +}; + +const collection = 'users'; +const action = 'get'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-use-decorator-props': 'useFormBlockDecoratorProps', + 'x-decorator-props': { + collection: collection, + action: action, + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'object', + 'x-component': 'DemoForm', + 'x-use-component-props': 'useDemoFormProps', + }, + }, +}; + +const recordData = { + id: 1, + username: 'Bamboo', + age: 18, +}; + +const Demo = () => { + return ( + <> + + + + + ); +}; + +const mocks = { + [`${collection}:update`]: function (config) { + console.log('config.data', config.data); + return { + data: 'ok', + }; + }, +}; +const Root = createApp( + Demo, + { + components: { DemoForm }, + scopes: { useDemoFormProps, useFormBlockDecoratorProps }, + }, + mocks, +); + +export default Root; diff --git a/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/collection-table-list.tsx b/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/collection-table-list.tsx new file mode 100644 index 000000000..278152be4 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/collection-table-list.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { Table, TableProps } from 'antd'; +import { SchemaComponent, useDataBlockRequest, withDynamicSchemaProps } from '@nocobase/client'; +import { ISchema } from '@formily/json-schema'; + +import { createApp } from '../../../data-source/demos/createApp'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: 'users', + action: 'list', + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', // 动态 table 属性 + }, + }, +}; + +const MyTable = withDynamicSchemaProps(Table); + +function useTableProps(): TableProps { + const { data, loading } = useDataBlockRequest(); + return { + loading, + dataSource: data?.data || [], + columns: [ + { + title: 'UserName', + dataIndex: 'username', + }, + { + title: 'NickName', + dataIndex: 'nickname', + }, + { + title: 'Email', + dataIndex: 'email', + }, + ], + }; +} + +const Demo = () => { + return ; +}; + +const Root = createApp(Demo, { + components: { MyTable }, + scopes: { useTableProps }, +}); + +export default Root; diff --git a/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/complete-demo.tsx b/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/complete-demo.tsx new file mode 100644 index 000000000..93bfcfa3a --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/demos/data-block-provider/complete-demo.tsx @@ -0,0 +1,162 @@ +import React, { useMemo } from 'react'; +import { + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + useDataBlockRequest, + useCollectionManager, + useCollection, + useCompile, + useDataBlockProps, + useDataBlock, + useSchemaInitializer, + useSchemaInitializerRender, + withDynamicSchemaProps, +} from '@nocobase/client'; + +import { createApp } from '../../../data-source/demos/createApp'; +import { Table, TableProps } from 'antd'; +import { ISchema } from '@formily/json-schema'; +import { observer, useFieldSchema } from '@formily/react'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'MyInitializer', +}; + +const MyTable = withDynamicSchemaProps(Table); + +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const compile = useCompile(); + const collection = useCollection(); + const columns = useMemo(() => { + return collection.getFields().map((field) => { + return { + title: compile(field.uiSchema?.title || field.name), + dataIndex: field.name, + }; + }); + }, [collection, compile]); + return { + ...tableProps, + loading, + dataSource: data?.data || [], + columns, + }; +} + +const MyTableSettings = new SchemaSettings({ + name: 'MyTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + ], +}); + +const TableDataBlockInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = ({ item }) => { + insert({ + type: 'void', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: item.value, + action: 'list', + tableProps: { + bordered: true, // 用于设置是否显示边框 + }, + }, + 'x-settings': 'MyTableSettings', + 'x-component': 'CardItem', + 'x-toolbar-props': { + draggable: false, + }, + properties: { + table: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', // 动态 table 属性 + }, + }, + }); + }; + const compile = useCompile(); + const collectionManager = useCollectionManager(); + const collectionMenuItems = useMemo( + () => + collectionManager.getCollections().map((collection) => { + return { + label: compile(collection.getOption('title')), + value: collection.getOption('name'), + }; + }), + [collectionManager, compile], + ); + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Add Block', + // 插入位置 + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +const Root = () => { + return ; +}; + +const App = createApp(Root, { + components: { MyTable, Page }, + scopes: { useTableProps }, + schemaSettings: [MyTableSettings], + schemaInitializers: [myInitializer], +}); + +export default App; diff --git a/packages/core/client/docs/en-US/core/data-block/demos/settings/demo1.tsx b/packages/core/client/docs/en-US/core/data-block/demos/settings/demo1.tsx new file mode 100644 index 000000000..fe3852422 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/demos/settings/demo1.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { + SchemaComponent, + SchemaSettings, + useDataBlockRequest, + useDataBlock, + withDynamicSchemaProps, +} from '@nocobase/client'; + +import { createApp } from '../../../data-source/demos/createApp'; +import { Table, TableProps } from 'antd'; +import { ISchema } from '@formily/json-schema'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: 'users', + action: 'list', + bordered: true, // 用于设置是否显示边框 + }, + 'x-settings': 'MyTableSettings', + 'x-component': 'CardItem', + 'x-toolbar-props': { + draggable: false, + }, + properties: { + demo: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', // 动态 table 属性 + }, + }, +}; + +const MyTable = withDynamicSchemaProps(Table); + +function useTableProps(): TableProps { + // 获取 + const { props } = useDataBlock(); + const { bordered } = props; + const { data, loading } = useDataBlockRequest(); + return { + bordered, + loading, + dataSource: data?.data || [], + columns: [ + { + title: 'Name', + dataIndex: 'name', + }, + { + title: 'Age', + dataIndex: 'age', + }, + { + title: 'Address', + dataIndex: 'address', + }, + ], + }; +} + +const MyTableSettings = new SchemaSettings({ + name: 'MyTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + // 获取 + const { props: blockSettingsProps, dn } = useDataBlock(); + const { bordered } = blockSettingsProps; + + return { + title: 'Bordered', + checked: bordered, + onChange: (checked) => { + // 修改 + dn.deepMerge({ 'x-decorator-props': { bordered: checked } }); + }, + }; + }, + }, + ], +}); + +const Demo = () => { + return ; +}; + +const Root = createApp(Demo, { + components: { MyTable }, + scopes: { useTableProps }, + schemaSettings: [MyTableSettings], +}); + +export default Root; diff --git a/packages/core/client/docs/en-US/core/data-block/images/data-block.png b/packages/core/client/docs/en-US/core/data-block/images/data-block.png new file mode 100644 index 000000000..0ac7ee8e8 Binary files /dev/null and b/packages/core/client/docs/en-US/core/data-block/images/data-block.png differ diff --git a/packages/core/client/docs/en-US/core/data-block/images/markdown-block.png b/packages/core/client/docs/en-US/core/data-block/images/markdown-block.png new file mode 100644 index 000000000..9c9c351be Binary files /dev/null and b/packages/core/client/docs/en-US/core/data-block/images/markdown-block.png differ diff --git a/packages/core/client/docs/en-US/core/data-block/tmp-demos/collections.ts b/packages/core/client/docs/en-US/core/data-block/tmp-demos/collections.ts new file mode 100644 index 000000000..82ba795b8 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/tmp-demos/collections.ts @@ -0,0 +1,128 @@ +export const mainCollections = [ + { + name: 'users', + title: 'Users', + fields: [ + { + key: 'id', + name: 'id', + interface: 'id', + primaryKey: true, + uiSchema: { + type: 'number', + title: 'ID', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + rawTitle: 'ID', + }, + }, + { + uiSchema: { + type: 'string', + title: 'Nickname', + 'x-component': 'Input', + }, + name: 'nickname', + type: 'string', + interface: 'input', + }, + { + uiSchema: { + type: 'string', + title: 'Email', + 'x-component': 'Input', + required: true, + }, + key: 'rrskwjl5wt1', + name: 'email', + type: 'string', + interface: 'email', + unique: true, + }, + { + name: 'favoriteColor', + type: 'string', + interface: 'color', + uiSchema: { + type: 'string', + 'x-component': 'ColorPicker', + default: '#1677FF', + title: 'Favorite Color', + }, + }, + ], + }, + { + name: 'roles', + title: 'Roles', + fields: [ + { + uiSchema: { + type: 'string', + title: 'Role UID', + 'x-component': 'Input', + }, + name: 'name', + type: 'uid', + interface: 'input', + primaryKey: true, + }, + { + uiSchema: { + type: 'string', + title: 'Role name', + 'x-component': 'Input', + }, + name: 'title', + type: 'string', + interface: 'input', + }, + { + name: 'description', + type: 'string', + }, + ], + }, +]; + +export const TestDBCollections = [ + { + name: 'test1', + title: 'Test1', + fields: [ + { + name: 'id', + interface: 'id', + primaryKey: true, + uiSchema: { + type: 'number', + title: 'ID', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + }, + }, + { + name: 'field1', + interface: 'input', + uiSchema: { + type: 'string', + 'x-component': 'Input', + title: 'Field1', + }, + }, + { + name: 'field2', + interface: 'number', + uiSchema: { + 'x-component-props': { + step: '1', + stringMode: true, + }, + 'x-component': 'InputNumber', + title: 'Field2', + }, + defaultValue: 1, + }, + ], + }, +]; diff --git a/packages/core/client/docs/en-US/core/data-block/tmp-demos/createApp.tsx b/packages/core/client/docs/en-US/core/data-block/tmp-demos/createApp.tsx new file mode 100644 index 000000000..ebbc8ec12 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/tmp-demos/createApp.tsx @@ -0,0 +1 @@ +import { Application } from '@nocobase/client'; diff --git a/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo1.tsx b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo1.tsx new file mode 100644 index 000000000..8c3f6a54f --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo1.tsx @@ -0,0 +1,41 @@ +import { Table } from 'antd'; +import React from 'react'; + +const MyTable = () => { + const dataSource = [ + { + key: '1', + name: 'Mike', + age: 32, + address: '10 Downing Street', + }, + { + key: '2', + name: 'John', + age: 42, + address: '10 Downing Street', + }, + ]; + + const columns = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, + { + title: 'Age', + dataIndex: 'age', + key: 'age', + }, + { + title: 'Address', + dataIndex: 'address', + key: 'address', + }, + ]; + + return
; +}; + +export default MyTable; diff --git a/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo10.tsx b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo10.tsx new file mode 100644 index 000000000..844315f33 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo10.tsx @@ -0,0 +1,412 @@ +import { Button, Drawer, Space, Table, TableProps } from 'antd'; +import React, { useEffect, useMemo, useState } from 'react'; +import { + CardItem, + CollectionField, + ColorFieldInterface, + ColorPicker, + DataBlockProvider, + EmailFieldInterface, + FormItem, + IdFieldInterface, + Input, + InputFieldInterface, + InputNumber, + NumberFieldInterface, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + SchemaToolbar, + useCollection, + useCompile, + useDataBlock, + useDataBlockProps, + useDataBlockRequest, + useDataSourceManager, + useSchemaInitializer, + useSchemaInitializerRender, + useSchemaToolbar, + useSchemaToolbarRender, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, RecursionField, Schema, observer, useField, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const MyTable = withDynamicSchemaProps(Table, { displayName: 'MyTable' }); + +const TableColumn = observer(() => { + const field = useField(); + return
{field.title}
; +}); + +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const collection = useCollection(); + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + + return { + title: , + dataIndex: collectionField.name, + render(value, record, index) { + return ( + + ); + }, + }; + }); + }, [collection]); + + return { + ...tableProps, + loading, + dataSource, + columns, + }; +} + +const myTableSettings = new SchemaSettings({ + name: 'myTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const CreateAction = () => { + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + + const compile = useCompile(); + const collection = useCollection(); + const title = compile(collection.title); + const fieldSchema = useFieldSchema(); + + const { render } = useSchemaToolbarRender(fieldSchema); + return ( + <> +
+ {render()} + +
+ +

Some contents...

+
+ + ); +}; + +const createActionSettings = new SchemaSettings({ + name: 'createActionSettings', + items: [ + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const RefreshAction = () => { + const { refresh } = useDataBlockRequest(); + return ; +}; + +const ActionBar = ({ children }) => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + + return ( +
+ + {children} + {render()} + +
+ ); +}; + +const CreateActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'CreateAction', + 'x-settings': 'createActionSettings', + }); + }; + return ; +}; + +const RefreshActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'RefreshAction', + }); + }; + return ; +}; + +const tableActionInitializers = new SchemaInitializer({ + name: 'tableActionInitializers', + title: 'Configure actions', + icon: 'SettingOutlined', + style: { + marginLeft: 8, + }, + items: [ + { + type: 'item', + name: 'addNew', + Component: CreateActionInitializer, + }, + { + type: 'item', + name: 'refresh', + Component: RefreshActionInitializer, + }, + ], +}); + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-settings': 'myTableSettings', + 'x-decorator': 'DataBlockProvider', + 'x-toolbar': 'MyToolbar', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + tableProps: { + bordered: true, + }, + }, + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-initializer': 'tableActionInitializers', + }, + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + + return ; +}; + +const MyToolbar = (props) => { + const collection = useCollection(); + const compile = useCompile(); + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable, TableColumn, MyToolbar, ActionBar, CreateAction, RefreshAction }); + this.app.schemaInitializerManager.add(myInitializer); + this.app.schemaSettingsManager.add(myTableSettings); + this.app.schemaSettingsManager.add(createActionSettings); + this.app.addScopes({ useTableProps }); + this.app.schemaInitializerManager.add(tableActionInitializers); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + InputNumber, + Input, + CollectionField, + ColorPicker, + FormItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + fieldInterfaces: [ + IdFieldInterface, + InputFieldInterface, + EmailFieldInterface, + ColorFieldInterface, + NumberFieldInterface, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo11.tsx b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo11.tsx new file mode 100644 index 000000000..e362ddf03 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo11.tsx @@ -0,0 +1,492 @@ +import { Button, Drawer, Space, Table, TableProps } from 'antd'; +import React, { useEffect, useMemo, useState } from 'react'; +import { + CardItem, + CollectionField, + ColorFieldInterface, + ColorPicker, + DataBlockProvider, + EmailFieldInterface, + FormItem, + IdFieldInterface, + Input, + InputFieldInterface, + InputNumber, + NumberFieldInterface, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaInitializerSwitch, + SchemaSettings, + SchemaToolbar, + useCollection, + useCompile, + useCurrentSchema, + useDataBlock, + useDataBlockProps, + useDataBlockRequest, + useDataSourceManager, + useDesignable, + useSchemaInitializer, + useSchemaInitializerItem, + useSchemaInitializerRender, + useSchemaToolbarRender, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, RecursionField, observer, useField, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const MyTable = withDynamicSchemaProps(Table, { displayName: 'MyTable' }); + +const TableColumn = observer(() => { + const field = useField(); + return
{field.title}
; +}); + +function useTableColumns() { + const schema = useFieldSchema(); + const filed = useField(); + const { designable } = useDesignable(); + const { render } = useSchemaInitializerRender(schema['x-initializer'], schema['x-initializer-props']); + const columns = schema.mapProperties((tableField: any, name) => { + return { + title: , + dataIndex: name, + key: name, + width: 200, + render(value, record, index) { + return ; + }, + }; + }); + + const tableColumns = useMemo(() => { + return [ + ...columns, + { + title: render(), + dataIndex: 'TABLE_COLUMN_INITIALIZER', + key: 'TABLE_COLUMN_INITIALIZER', + width: 200, + render: designable ? () =>
: null, + }, + ]; + }, [columns, render, designable]); + + return tableColumns; +} + +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useTableColumns(); + + return { + ...tableProps, + loading, + dataSource, + columns, + }; +} + +const myTableSettings = new SchemaSettings({ + name: 'myTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const CreateAction = () => { + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + + const compile = useCompile(); + const collection = useCollection(); + const title = compile(collection.title); + const fieldSchema = useFieldSchema(); + + const { render } = useSchemaToolbarRender(fieldSchema); + return ( + <> +
+ {render()} + +
+ +

Some contents...

+
+ + ); +}; + +const createActionSettings = new SchemaSettings({ + name: 'createActionSettings', + items: [ + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const RefreshAction = () => { + const { refresh } = useDataBlockRequest(); + return ; +}; + +const ActionBar = ({ children }) => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + + return ( +
+ + {children} + {render()} + +
+ ); +}; + +const CreateActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'CreateAction', + 'x-settings': 'createActionSettings', + }); + }; + return ; +}; + +const RefreshActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'RefreshAction', + }); + }; + return ; +}; + +const tableActionInitializers = new SchemaInitializer({ + name: 'tableActionInitializers', + title: 'Configure actions', + icon: 'SettingOutlined', + style: { + marginLeft: 8, + }, + items: [ + { + type: 'item', + name: 'addNew', + Component: CreateActionInitializer, + }, + { + type: 'item', + name: 'refresh', + Component: RefreshActionInitializer, + }, + ], +}); + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-settings': 'myTableSettings', + 'x-decorator': 'DataBlockProvider', + 'x-toolbar': 'MyToolbar', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + tableProps: { + bordered: true, + }, + }, + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-initializer': 'tableActionInitializers', + }, + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + 'x-initializer': 'tableColumnInitializers', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + + return ; +}; + +const useTableColumnInitializerFields = () => { + const collection = useCollection(); + return collection.fields.map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + return { + type: 'item', + name: collectionField.name, + title: collectionField?.uiSchema?.title || collectionField.name, + Component: 'TableCollectionFieldInitializer', + schema: tableFieldSchema, + }; + }); +}; + +export const TableCollectionFieldInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { insert } = useSchemaInitializer(); + const { remove } = useDesignable(); + const schema = useFieldSchema(); + const exists = !!schema.properties?.[itemConfig.name]; + return ( + { + if (exists) { + return remove(schema.properties?.[itemConfig.name]); + } + insert(itemConfig.schema); + }} + /> + ); +}; + +const tableColumnInitializers = new SchemaInitializer({ + name: 'tableColumnInitializers', + insertPosition: 'beforeEnd', + icon: 'SettingOutlined', + title: 'Configure columns', + items: [ + { + name: 'displayFields', + type: 'itemGroup', + title: 'Display fields', + useChildren: useTableColumnInitializerFields, + }, + ], +}); + +const MyToolbar = (props) => { + const collection = useCollection(); + const compile = useCompile(); + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ + MyTable, + TableColumn, + MyToolbar, + ActionBar, + CreateAction, + RefreshAction, + TableCollectionFieldInitializer, + }); + this.app.schemaInitializerManager.add(myInitializer); + this.app.schemaSettingsManager.add(myTableSettings); + this.app.addScopes({ useTableProps }); + this.app.schemaInitializerManager.add(tableActionInitializers); + this.app.schemaInitializerManager.add(tableColumnInitializers); + this.app.schemaSettingsManager.add(createActionSettings); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + InputNumber, + Input, + CollectionField, + ColorPicker, + FormItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + fieldInterfaces: [ + IdFieldInterface, + InputFieldInterface, + EmailFieldInterface, + ColorFieldInterface, + NumberFieldInterface, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo12.tsx b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo12.tsx new file mode 100644 index 000000000..1fd5d9a67 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo12.tsx @@ -0,0 +1,532 @@ +import { Button, Drawer, Space, Table, TableProps } from 'antd'; +import React, { useEffect, useMemo, useState } from 'react'; +import { + CardItem, + CollectionField, + ColorFieldInterface, + ColorPicker, + DataBlockProvider, + EmailFieldInterface, + FormItem, + IdFieldInterface, + Input, + InputFieldInterface, + InputNumber, + NumberFieldInterface, + Plugin, + CollectionRecordProvider, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaInitializerSwitch, + SchemaSettings, + SchemaToolbar, + useCollection, + useCompile, + useCurrentSchema, + useDataBlock, + useDataBlockProps, + useDataBlockRequest, + useDataSource, + useDataSourceManager, + useDesignable, + useCollectionRecord, + useSchemaInitializer, + useSchemaInitializerItem, + useSchemaInitializerRender, + useSchemaToolbarRender, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, RecursionField, observer, useField, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const MyTable = withDynamicSchemaProps(Table, { displayName: 'MyTable' }); + +const TableColumn = observer(() => { + const field = useField(); + return
{field.title}
; +}); + +const ColumnView = observer( + () => { + const record = useCollectionRecord(); + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + return ( + <> + + +
{JSON.stringify(record, null, 2)}
+
+ + ); + }, + { displayName: 'ColumnView' }, +); + +function useTableColumns() { + const schema = useFieldSchema(); + const filed = useField(); + const { designable } = useDesignable(); + const { render } = useSchemaInitializerRender(schema['x-initializer'], schema['x-initializer-props']); + const columns = schema.mapProperties((tableField: any, name) => { + return { + title: , + dataIndex: name, + key: name, + render(value, record, index) { + return ; + }, + }; + }); + const tableColumns = useMemo(() => { + return [ + ...columns, + { + title: 'Actions', + dataIndex: 'actions', + key: 'actions', + render: (value, record) => { + return ( + + + + + + ); + }, + }, + { + title: render(), + dataIndex: 'TABLE_COLUMN_INITIALIZER', + key: 'TABLE_COLUMN_INITIALIZER', + render: designable ? () =>
: null, + }, + ]; + }, [columns, render, designable]); + + return tableColumns; +} + +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useTableColumns(); + + return { + ...tableProps, + loading, + dataSource, + columns, + }; +} + +const myTableSettings = new SchemaSettings({ + name: 'myTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const CreateAction = () => { + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + + const compile = useCompile(); + const collection = useCollection(); + const title = compile(collection.title); + const fieldSchema = useFieldSchema(); + + const { render } = useSchemaToolbarRender(fieldSchema); + return ( + <> +
+ {render()} + +
+ +

Some contents...

+
+ + ); +}; + +const createActionSettings = new SchemaSettings({ + name: 'createActionSettings', + items: [ + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const RefreshAction = () => { + const { refresh } = useDataBlockRequest(); + return ; +}; + +const ActionBar = ({ children }) => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + + return ( +
+ + {children} + {render()} + +
+ ); +}; + +const CreateActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'CreateAction', + 'x-settings': 'createActionSettings', + }); + }; + return ; +}; + +const RefreshActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'RefreshAction', + }); + }; + return ; +}; + +const tableActionInitializers = new SchemaInitializer({ + name: 'tableActionInitializers', + title: 'Configure actions', + icon: 'SettingOutlined', + style: { + marginLeft: 8, + }, + items: [ + { + type: 'item', + name: 'addNew', + Component: CreateActionInitializer, + }, + { + type: 'item', + name: 'refresh', + Component: RefreshActionInitializer, + }, + ], +}); + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-settings': 'myTableSettings', + 'x-decorator': 'DataBlockProvider', + 'x-toolbar': 'MyToolbar', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + tableProps: { + bordered: true, + }, + }, + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-initializer': 'tableActionInitializers', + }, + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + 'x-initializer': 'tableColumnInitializers', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + + return ; +}; + +const useTableColumnInitializerFields = () => { + const collection = useCollection(); + return collection.fields.map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + return { + type: 'item', + name: collectionField.name, + title: collectionField?.uiSchema?.title || collectionField.name, + Component: 'TableCollectionFieldInitializer', + schema: tableFieldSchema, + }; + }); +}; + +export const TableCollectionFieldInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { insert } = useSchemaInitializer(); + const { remove } = useDesignable(); + const schema = useFieldSchema(); + const exists = !!schema.properties?.[itemConfig.name]; + return ( + { + if (exists) { + return remove(schema.properties?.[itemConfig.name]); + } + insert(itemConfig.schema); + }} + /> + ); +}; + +const tableColumnInitializers = new SchemaInitializer({ + name: 'tableColumnInitializers', + insertPosition: 'beforeEnd', + icon: 'SettingOutlined', + title: 'Configure columns', + items: [ + { + name: 'displayFields', + type: 'itemGroup', + title: 'Display fields', + useChildren: useTableColumnInitializerFields, + }, + ], +}); + +const MyToolbar = (props) => { + const collection = useCollection(); + const compile = useCompile(); + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ + MyTable, + TableColumn, + MyToolbar, + ActionBar, + CreateAction, + RefreshAction, + TableCollectionFieldInitializer, + }); + this.app.schemaInitializerManager.add(myInitializer); + this.app.schemaSettingsManager.add(myTableSettings); + this.app.addScopes({ useTableProps }); + this.app.schemaInitializerManager.add(tableActionInitializers); + this.app.schemaInitializerManager.add(tableColumnInitializers); + this.app.schemaSettingsManager.add(createActionSettings); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + InputNumber, + Input, + CollectionField, + ColorPicker, + FormItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + fieldInterfaces: [ + IdFieldInterface, + InputFieldInterface, + EmailFieldInterface, + ColorFieldInterface, + NumberFieldInterface, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo2.tsx b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo2.tsx new file mode 100644 index 000000000..f487115da --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo2.tsx @@ -0,0 +1,82 @@ +import { Table } from 'antd'; +import React from 'react'; +import { CardItem, Plugin, SchemaComponent } from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; + +const MyTable = () => { + const dataSource = [ + { + key: '1', + name: 'Mike', + age: 32, + address: '10 Downing Street', + }, + { + key: '2', + name: 'John', + age: 42, + address: '10 Downing Street', + }, + ]; + + const columns = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, + { + title: 'Age', + dataIndex: 'age', + key: 'age', + }, + { + title: 'Address', + dataIndex: 'address', + key: 'address', + }, + ]; + + return
; +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable }); + } +} + +const tableSchema = { + name: 'demo', + type: 'void', + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + }, + }, +}; + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + CardItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, +}); + +app.router.add('root', { + path: '/', + Component: Root, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo3.tsx b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo3.tsx new file mode 100644 index 000000000..1d899d6fb --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo3.tsx @@ -0,0 +1,147 @@ +import { Table } from 'antd'; +import React, { useCallback } from 'react'; +import { + CardItem, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + useSchemaInitializer, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, observer, useFieldSchema } from '@formily/react'; + +const MyTable = () => { + const dataSource = [ + { + key: '1', + name: 'Mike', + age: 32, + address: '10 Downing Street', + }, + { + key: '2', + name: 'John', + age: 42, + address: '10 Downing Street', + }, + ]; + + const columns = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, + { + title: 'Age', + dataIndex: 'age', + key: 'age', + }, + { + title: 'Address', + dataIndex: 'address', + key: 'address', + }, + ]; + + return
; +}; + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = useCallback( + ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }, + [insert, setVisible], + ); + + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable }); + this.app.schemaInitializerManager.add(myInitializer); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo4.tsx b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo4.tsx new file mode 100644 index 000000000..7c327227b --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo4.tsx @@ -0,0 +1,180 @@ +import { Table } from 'antd'; +import React, { useMemo } from 'react'; +import { + CardItem, + DataBlockProvider, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + useCollection, + useDataBlockRequest, + useDataSourceManager, + useSchemaInitializer, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, observer, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const MyTable = () => { + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const collection = useCollection(); + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { + return { + title: collectionField.uiSchema?.title || collectionField.name, + dataIndex: collectionField.name, + }; + }); + }, [collection]); + + return
; +}; + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + }, + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable }); + this.app.schemaInitializerManager.add(myInitializer); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo5.tsx b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo5.tsx new file mode 100644 index 000000000..51b09f7bc --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo5.tsx @@ -0,0 +1,236 @@ +import { Table } from 'antd'; +import React, { useEffect, useMemo } from 'react'; +import { + CardItem, + CollectionField, + ColorFieldInterface, + ColorPicker, + DataBlockProvider, + EmailFieldInterface, + FormItem, + IdFieldInterface, + Input, + InputFieldInterface, + InputNumber, + NumberFieldInterface, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + useCollection, + useDataBlockRequest, + useDataSourceManager, + useSchemaInitializer, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, RecursionField, observer, useField, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const TableColumn = observer(() => { + const field = useField(); + return
{field.title}
; +}); + +const MyTable = () => { + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const collection = useCollection(); + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + + return { + title: , + dataIndex: collectionField.name, + render(value, record, index) { + return ( + + ); + }, + }; + }); + }, [collection]); + + return
; +}; + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + }, + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable, TableColumn }); + this.app.schemaInitializerManager.add(myInitializer); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + InputNumber, + Input, + CollectionField, + ColorPicker, + FormItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + fieldInterfaces: [ + IdFieldInterface, + InputFieldInterface, + EmailFieldInterface, + ColorFieldInterface, + NumberFieldInterface, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo6.tsx b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo6.tsx new file mode 100644 index 000000000..a07197d3f --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo6.tsx @@ -0,0 +1,280 @@ +import { Table, TableProps } from 'antd'; +import React, { useEffect, useMemo } from 'react'; +import { + CardItem, + CollectionField, + ColorFieldInterface, + ColorPicker, + DataBlockProvider, + EmailFieldInterface, + FormItem, + IdFieldInterface, + Input, + InputFieldInterface, + InputNumber, + NumberFieldInterface, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + useCollection, + useDataBlock, + useDataBlockProps, + useDataBlockRequest, + useDataSourceManager, + useSchemaInitializer, + useSchemaInitializerRender, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, RecursionField, observer, useField, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const MyTable = withDynamicSchemaProps(Table, { displayName: 'MyTable' }); + +const TableColumn = observer(() => { + const field = useField(); + return
{field.title}
; +}); + +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const collection = useCollection(); + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + + return { + title: , + dataIndex: collectionField.name, + render(value, record, index) { + return ( + + ); + }, + }; + }); + }, [collection]); + + return { + ...tableProps, + loading, + dataSource, + columns, + rowKey: 'id', + }; +} + +const myTableSettings = new SchemaSettings({ + name: 'myTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-settings': 'myTableSettings', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + tableProps: { + bordered: true, + }, + }, + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable, TableColumn }); + this.app.schemaInitializerManager.add(myInitializer); + this.app.schemaSettingsManager.add(myTableSettings); + this.app.addScopes({ useTableProps }); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + InputNumber, + Input, + CollectionField, + ColorPicker, + FormItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + fieldInterfaces: [ + IdFieldInterface, + InputFieldInterface, + EmailFieldInterface, + ColorFieldInterface, + NumberFieldInterface, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo7.tsx b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo7.tsx new file mode 100644 index 000000000..6acb8e01c --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo7.tsx @@ -0,0 +1,291 @@ +import { Table, TableProps } from 'antd'; +import React, { useEffect, useMemo } from 'react'; +import { + CardItem, + CollectionField, + ColorFieldInterface, + ColorPicker, + DataBlockProvider, + EmailFieldInterface, + FormItem, + IdFieldInterface, + Input, + InputFieldInterface, + InputNumber, + NumberFieldInterface, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + SchemaToolbar, + useCollection, + useCompile, + useDataBlock, + useDataBlockProps, + useDataBlockRequest, + useDataSource, + useDataSourceManager, + useSchemaInitializer, + useSchemaInitializerRender, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, RecursionField, observer, useField, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const MyTable = withDynamicSchemaProps(Table, { displayName: 'MyTable' }); + +const TableColumn = observer(() => { + const field = useField(); + return
{field.title}
; +}); + +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const collection = useCollection(); + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + + return { + title: , + dataIndex: collectionField.name, + render(value, record, index) { + return ( + + ); + }, + }; + }); + }, [collection]); + + return { + ...tableProps, + loading, + dataSource, + columns, + }; +} + +const myTableSettings = new SchemaSettings({ + name: 'myTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-settings': 'myTableSettings', + 'x-decorator': 'DataBlockProvider', + 'x-toolbar': 'MyToolbar', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + tableProps: { + bordered: true, + }, + }, + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + + return ; +}; + +const MyToolbar = (props) => { + const collection = useCollection(); + const compile = useCompile(); + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable, TableColumn, MyToolbar }); + this.app.schemaInitializerManager.add(myInitializer); + this.app.schemaSettingsManager.add(myTableSettings); + this.app.addScopes({ useTableProps }); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + InputNumber, + Input, + CollectionField, + ColorPicker, + FormItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + fieldInterfaces: [ + IdFieldInterface, + InputFieldInterface, + EmailFieldInterface, + ColorFieldInterface, + NumberFieldInterface, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo8.tsx b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo8.tsx new file mode 100644 index 000000000..edb6b3310 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo8.tsx @@ -0,0 +1,351 @@ +import { Button, Drawer, Space, Table, TableProps } from 'antd'; +import React, { useEffect, useMemo, useState } from 'react'; +import { + CardItem, + CollectionField, + ColorFieldInterface, + ColorPicker, + DataBlockProvider, + EmailFieldInterface, + FormItem, + IdFieldInterface, + Input, + InputFieldInterface, + InputNumber, + NumberFieldInterface, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + SchemaToolbar, + useCollection, + useCompile, + useDataBlock, + useDataBlockProps, + useDataBlockRequest, + useDataSource, + useDataSourceManager, + useSchemaInitializer, + useSchemaInitializerRender, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, RecursionField, observer, useField, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const MyTable = withDynamicSchemaProps(Table, { displayName: 'MyTable' }); + +const TableColumn = observer(() => { + const field = useField(); + return
{field.title}
; +}); + +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const collection = useCollection(); + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + + return { + title: , + dataIndex: collectionField.name, + render(value, record, index) { + return ( + + ); + }, + }; + }); + }, [collection]); + + return { + ...tableProps, + loading, + dataSource, + columns, + }; +} + +const myTableSettings = new SchemaSettings({ + name: 'myTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const CreateAction = () => { + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + + const compile = useCompile(); + const collection = useCollection(); + const title = compile(collection.title); + + return ( + <> + + +

Some contents...

+
+ + ); +}; + +const RefreshAction = () => { + const { refresh } = useDataBlockRequest(); + return ; +}; + +const ActionBar = () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + + return ( +
+ + + + {render()} + +
+ ); +}; + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-settings': 'myTableSettings', + 'x-decorator': 'DataBlockProvider', + 'x-toolbar': 'MyToolbar', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + tableProps: { + bordered: true, + }, + }, + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-initializer': 'tableActionInitializers', + }, + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + + return ; +}; + +const MyToolbar = (props) => { + const collection = useCollection(); + const compile = useCompile(); + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable, TableColumn, MyToolbar, ActionBar }); + this.app.schemaInitializerManager.add(myInitializer); + this.app.schemaSettingsManager.add(myTableSettings); + this.app.addScopes({ useTableProps }); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + InputNumber, + Input, + CollectionField, + ColorPicker, + FormItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + fieldInterfaces: [ + IdFieldInterface, + InputFieldInterface, + EmailFieldInterface, + ColorFieldInterface, + NumberFieldInterface, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo9.tsx b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo9.tsx new file mode 100644 index 000000000..f5cf4261b --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/tmp-demos/demo9.tsx @@ -0,0 +1,394 @@ +import { Button, Drawer, Space, Table, TableProps } from 'antd'; +import React, { useEffect, useMemo, useState } from 'react'; +import { + CardItem, + CollectionField, + ColorFieldInterface, + ColorPicker, + DataBlockProvider, + EmailFieldInterface, + FormItem, + IdFieldInterface, + Input, + InputFieldInterface, + InputNumber, + NumberFieldInterface, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + SchemaToolbar, + useCollection, + useCompile, + useDataBlock, + useDataBlockProps, + useDataBlockRequest, + useDataSource, + useDataSourceManager, + useSchemaInitializer, + useSchemaInitializerRender, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, RecursionField, observer, useField, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const MyTable = withDynamicSchemaProps(Table, { displayName: 'MyTable' }); + +const TableColumn = observer(() => { + const field = useField(); + return
{field.title}
; +}); + +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const collection = useCollection(); + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + + return { + title: , + dataIndex: collectionField.name, + render(value, record, index) { + return ( + + ); + }, + }; + }); + }, [collection]); + + return { + ...tableProps, + loading, + dataSource, + columns, + }; +} + +const myTableSettings = new SchemaSettings({ + name: 'myTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const CreateAction = () => { + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + + const compile = useCompile(); + const collection = useCollection(); + const title = compile(collection.title); + + return ( + <> + + +

Some contents...

+
+ + ); +}; + +const RefreshAction = () => { + const { refresh } = useDataBlockRequest(); + return ; +}; + +const ActionBar = ({ children }) => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + + return ( +
+ + {children} + {render()} + +
+ ); +}; + +const CreateActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'CreateAction', + }); + }; + return ; +}; + +const RefreshActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'RefreshAction', + }); + }; + return ; +}; + +const tableActionInitializers = new SchemaInitializer({ + name: 'tableActionInitializers', + title: 'Configure actions', + icon: 'SettingOutlined', + style: { + marginLeft: 8, + }, + items: [ + { + type: 'item', + name: 'addNew', + Component: CreateActionInitializer, + }, + { + type: 'item', + name: 'refresh', + Component: RefreshActionInitializer, + }, + ], +}); + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-settings': 'myTableSettings', + 'x-decorator': 'DataBlockProvider', + 'x-toolbar': 'MyToolbar', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + tableProps: { + bordered: true, + }, + }, + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-initializer': 'tableActionInitializers', + }, + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + + return ; +}; + +const MyToolbar = (props) => { + const collection = useCollection(); + const compile = useCompile(); + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable, TableColumn, MyToolbar, ActionBar, CreateAction, RefreshAction }); + this.app.schemaInitializerManager.add(myInitializer); + this.app.schemaSettingsManager.add(myTableSettings); + this.app.addScopes({ useTableProps }); + this.app.schemaInitializerManager.add(tableActionInitializers); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + InputNumber, + Input, + CollectionField, + ColorPicker, + FormItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + fieldInterfaces: [ + IdFieldInterface, + InputFieldInterface, + EmailFieldInterface, + ColorFieldInterface, + NumberFieldInterface, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/data-block/tmp-demos/mockData.ts b/packages/core/client/docs/en-US/core/data-block/tmp-demos/mockData.ts new file mode 100644 index 000000000..59bff781e --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/tmp-demos/mockData.ts @@ -0,0 +1,79 @@ +import type { Application } from '@nocobase/client'; +import MockAdapter from 'axios-mock-adapter'; + +const mockData = { + 'users:list': { + data: [ + { + id: '1', + nickname: 'Jack Ma', + email: 'test@gmail.com', + favoriteColor: '#1677FF', + }, + { + id: '2', + email: 'jim@gmail.com', + nickname: 'Jim Green', + favoriteColor: '#36C', + }, + { + id: '3', + nickname: 'Tom Cat', + email: 'tom@gmail.com', + favoriteColor: '#F0F', + }, + ], + }, + 'users:destroy': { + result: 'ok', + }, + 'roles:list': { + data: [ + { + name: 'root', + title: 'Root', + description: 'Root', + }, + { + name: 'admin', + title: 'Admin', + description: 'Admin description', + }, + ], + }, + 'test1:list': { + data: [ + { + id: '1', + field1: 'aaa', + field2: 1, + }, + { + id: '2', + field1: 'bbb', + field2: 2, + }, + { + id: '3', + field1: 'ccc', + field2: 3, + }, + ], + }, +}; + +export function mock(app: Application) { + const mock = new MockAdapter(app.apiClient.axios); + + Object.entries(mockData).forEach(([url, data]) => { + mock.onGet(url).reply(async (config) => { + return [200, data]; + }); + mock.onPost(url).reply(async (config) => { + return [200, data]; + }); + mock.onDelete(url).reply(async (config) => { + return [200, data]; + }); + }); +} diff --git a/packages/core/client/docs/en-US/core/data-block/tmp.md b/packages/core/client/docs/en-US/core/data-block/tmp.md new file mode 100644 index 000000000..49807329c --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-block/tmp.md @@ -0,0 +1,1149 @@ +# 临时 + + +## 区块介绍 + + +## 区块扩展 + +### 1. 先简单定义一个 Table 组件 + +```tsx | pure +const MyTable = () => { + const dataSource = [ + { + key: '1', + name: 'Mike', + age: 32, + address: '10 Downing Street', + }, + { + key: '2', + name: 'John', + age: 42, + address: '10 Downing Street', + }, + ]; + + const columns = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, + { + title: 'Age', + dataIndex: 'age', + key: 'age', + }, + { + title: 'Address', + dataIndex: 'address', + key: 'address', + }, + ]; + + return
; +} +``` + + + +### 2. 使用 UI Schema 的方式将组件渲染到页面上 + +先将组件注册到 NocoBase 应用中 + +```tsx | pure +import { Plugin } from '@nocobase/client'; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable }) + } +} +``` + +然后定义 UI Schema,并通过 `SchemaComponent` 渲染到页面上。 + +```tsx | pure +const tableSchema = { + name: 'demo', + type: 'void', + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + }, + }, +} + +const Root = () => { + return +} + +app.router.add('root', { + path: '/', + Component: Root, +}) +``` + + + +关于 UI Schema 的更多信息请参考 [UI Schema](/core/ui-schema/schema-component) 和 [UI Schema Quick Start](https://docs-cn.nocobase.com/development/client/ui-schema/quick-start) + +其中 `CardItem` 是 NocoBase 的内置组件,相当于外层的容器。 + +### 3. 使用 `SchemaInitializer` 将组件动态添加到页面上 + +首先我们将 `Root` 组件更换 Schema 为 `rootSchema`。 + +```tsx | pure +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +const Root = () => { + return ; +}; +``` + +其中 `Page` 的定义如下: + +```tsx | pure +import { observer } from "@formily/react"; + +const AddBlockButton = observer(() => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); +}, { displayName: 'AddBlockButton' }); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); +``` + +其中: + +- `useSchemaInitializerRender()`:用于获取 `SchemaInitializer` 的渲染函数,具体用法请参考 [useSchemaInitializerRender](/core/ui-schema/schema-initializer#useschemainitializer)。 +- `useFieldSchema()`:用于获取当前字段的 schema,具体用法请参考 [useFieldSchema](/core/ui-schema/designable#usefieldschema)。 +- `observer`:用于将函数组件转换为响应式组件,具体用法请参考 [observer](https://react.formilyjs.org/api/shared/observer)。 + +`myInitializer` 的定义如下: + +```tsx | pure +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = useCallback(({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }, [insert, setVisible]); + + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); +``` + +我们将 `tableSchema` 放到了 `TableDataBlockInitializer` 中,当点击 `Add Block` 时,会将 `tableSchema` 插入到页面上。 + +关于 SchemaInitializer 的更多信息请参考 [SchemaInitializer](/core/ui-schema/schema-initializer) 和 [SchemaInitializer Quick Start](https://docs-cn.nocobase.com/development/client/ui-schema/quick-start#schemainitializer)。 + +最后将 `myInitializer` 注册到 NocoBase 应用中。 + +```diff +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable }) ++ this.app.schemaInitializerManager.add(myInitializer); + } +} +``` + + + +在真正的插件开发过程中只需要定义和添加 `myInitializer` 和 `MyTable` 即可,`Page` 和 `Root` 是 NocoBase 内置的组件,无需重复定义。 + + +### 4. 从数据表中读取数据 + +上述 Table 中的数据是固定数据,我们需要动态的获取数据并渲染。 + +```diff ++ function useCollectionMenuItems() { ++ const dataSourceManager = useDataSourceManager(); ++ const allCollections = dataSourceManager.getAllCollections(); ++ const menus = useMemo( ++ () => allCollections.map((item) => { ++ const { key, displayName, collections } = item; ++ return { ++ name: key, ++ label: displayName, ++ type: 'subMenu', ++ children: collections.map((collection) => { ++ return { ++ name: collection.name, ++ label: collection.title, ++ collection: collection.name, ++ dataSource: key, ++ } ++ }) ++ }; ++ }), ++ [allCollections], ++ ); ++ ++ return menus; ++ } + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', ++ 'x-decorator': 'DataBlockProvider', ++ 'x-decorator-props': { ++ collection: item.collection, ++ dataSource: item.dataSource, ++ action: 'list', ++ }, + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + }, + }, + } + insert(tableSchema); + setVisible(false); + }; + ++ const collectionMenuItems = useCollectionMenuItems(); + return ; +}; +``` + +其中: + +- [DataBlockProvider](/core/data-block/data-block-provider) 是 NocoBase 的内置组件,主要作用有4个: + - 提供 Collection 表结构信息 + - 提供 Collection 表的数据 + - 提供对数据增删改查的 [Resource](https://docs.nocobase.com/api/sdk#action-api) 对象 + - 提供区块的配置信息 + +- item:是当前点击的菜单对象,包括 `collection` 和 `dataSource`。 +- `useCollectionMenuItems`:是一个自定义的 hook,用于获取所有的 Collection 表信息,返回一个菜单数组。 + - `dataSourceManager`:用于获取数据源管理器,具体用法请参考 [useDataSourceManager](/core/data-source/data-source-manager-provider#usedatasourcemanager)。 + + +然后我们修改 `MyTable` 组件 + +```diff +const MyTable = () => { +- const dataSource = [ +- { +- key: '1', +- name: 'Mike', +- age: 32, +- address: '10 Downing Street', +- }, +- { +- key: '2', +- name: 'John', +- age: 42, +- address: '10 Downing Street', +- }, +- ]; +- +- const columns = [ +- { +- title: 'Name', +- dataIndex: 'name', +- key: 'name', +- }, +- { +- title: 'Age', +- dataIndex: 'age', +- key: 'age', +- }, +- { +- title: 'Address', +- dataIndex: 'address', +- key: 'address', +- }, +- ]; + + ++ const { data, loading } = useDataBlockRequest(); ++ const dataSource = useMemo(() => data?.data || [], [data]); + ++ const collection = useCollection(); ++ const columns = useMemo(() => { ++ return collection.getFields().map((collectionField) => { ++ return { ++ title: compile(collectionField.uiSchema?.title || collectionField.name), ++ dataIndex: collectionField.name, ++ }; ++ }); ++ }, [collection, compile]); + + return
; +} +``` + +其中: + +- `useDataBlockRequest()`:由 [DataBlockProvider](/core/data-block/data-block-provider) 组件里面的 [DataBlockRequestProvider](/core/data-block/data-block-request-provider) 提供,用于获取对应的数据表结构。 +- `useCollection()`:由 [DataBlockProvider](/core/data-block/data-block-provider) 组件里面的 [DataBlockRequestProvider](/core/data-source/collection-provider) 提供,用于获取当前 Collection 的信息。 + + + +### 5. 将字段渲染为自定义组件 + +上面一步仅是把数据渲染成普通的字符串,还需要进一步把字段渲染成不同的组件。 + +我们以 `favoriteColor` 字段为例,其数据结构如下: + +```json +{ + "name": "favoriteColor", + "type": "string", + "interface": "color", + "uiSchema": { + "type": "string", + "x-component": "ColorPicker", + "default": "#1677FF", + "title": "Favorite Color" + } +} +``` + +其和渲染相关的是 `uiSchema` 字段和 `interface` 字段,`interface` 相当于一些公共的配置,`uiSchema` 相当于一些私有的配置,更多信息请参考 [CollectionField](/core/data-source/collection-field)。 + +```diff ++ useEffect(() => { ++ field.value = dataSource; ++ }, [dataSource]); + + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { ++ const tableFieldSchema = { + name: collectionField.name, ++ type: 'void', ++ title: collectionField.uiSchema?.title || collectionField.name, ++ 'x-component': 'TableColumn', ++ properties: { ++ [collectionField.name]: { ++ 'x-component': 'CollectionField', ++ 'x-read-pretty': true, ++ 'x-decorator-props': { ++ labelStyle: { ++ display: 'none', ++ }, ++ }, ++ }, ++ }, ++ }; ++ ++ return { +- title: collectionField.uiSchema?.title || collectionField.name, ++ title: , + dataIndex: collectionField.name, ++ render(value, record, index) { ++ return ; ++ }, ++ }; + }); + }, [collection]); +``` + +```diff ++ const TableColumn = observer(() => { ++ const field = useField(); ++ return
{field.title}
; ++ }); + +class MyPlugin extends Plugin { + async load() { + // ... ++ this.app.addComponents({ TableColumn }); + } +} +``` + +其中: + +`field.value = dataSource` 用于将数据赋值给 `MyTable` 这一层的 `field`,相当于: + +```diff +const tableSchema = { + // ... + properties: { + [uid()]: { + type: 'array', ++ value: dataSource, + 'x-component': 'MyTable', + }, + }, +}; +``` + +然后使用 `RecursionField` 组件将字段渲染成自定义组件,因为是数组,所以需要加上 `index`,这样子组件才能正确的读取到数据。 + +关于 `RecursionField` 的更多信息请参考 [RecursionField](https://react.formilyjs.org/api/components/recursion-fieldeld)。 + + + +### 6. 区块属性配置 + +首先我们先修改 `tableSchema`。 + +```diff + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', ++ 'x-settings': 'myTableSettings', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', ++ tableProps: { ++ bordered: true, ++ }, + }, + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', ++ 'x-use-component-props': 'useTableProps', + }, + }, +} +``` + +其中: + +- `x-settings`:用于指定区块的配置信息,具体用法请参考 [SchemaSettings](/core/ui-schema/schema-settings)。 +- `tableProps`:Table 的属性 +- `x-use-component-props`:用于动态读取的 UI Schema 属性 + +关于 `useTableProps` 的定义,我们先将 `MyTable` 的属性单独抽离为一个 Hook。 + +```tsx | pure +function useTableProps(): TableProps { + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const collection = useCollection(); + + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + + return { + title: , + dataIndex: collectionField.name, + render(value, record, index) { + return ; + }, + }; + }); + }, [collection]); + + return { + loading, + dataSource, + columns, + }; +} +``` + +然后增加如下内容: + +```diff +function useTableProps(): TableProps { ++ const { tableProps } = useDataBlockProps(); + // ... + + return { ++ ...tableProps, + loading, + dataSource, + columns, + }; +} +``` + +其中: + +- `useDataBlockProps()`:用于获取区块的配置信息,具体用法请参考 [useDataBlockProps](/core/data-block/data-block-provider#usedatablockprops)。 + + +再将 `MyTable` 组件使用 `withDynamicSchemaProps` 包裹。 + +```tsx | pure +const MyTable = withDynamicSchemaProps(Table, { displayName: 'MyTable' }); +``` + +`withDynamicSchemaProps` 的作用是能读取 Schema 中的 `x-use-component-props` 属性,将其解析并执行,然后将结果传递给组件。 + +然后我们实现 `myTableSettings`: + +```tsx | pure +const myTableSettings = new SchemaSettings({ + name: 'myTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + { + type: 'remove', + name: 'remove', + } + ], +}); +``` + +然后我们将 `myTableSettings` 和 `useTableProps` 注册到 NocoBase 应用中。 + +```diff +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable }) + this.app.schemaInitializerManager.add(myInitializer); ++ this.app.schemaSettingsManager.add(myTableSettings); ++ this.app.addScopes({ useTableProps }); + } +} +``` + +以上各个部分的关系如下: + +- `tableSchema`:`tableProps` 存储属性配置信息 +- `DataBlockProvider`:读取 `tableSchema` 的配置,并通过 `useDataBlockProps` 传递子组件 +- `useTableProps`:通过 `useDataBlockProps` 获取 `DataBlockProvider` 提供的配置 +- `MyTable`:通过 `withDynamicSchemaProps` 自动读取到 `'x-use-component-props': 'useTableProps'` 进行编译,执行 `useTableProps()` 获取到配置传递给 `Table` +- `myTableSettings`:通过 `useDataBlock()` 并修改配置 `DataBlockProvider` 提供的配置 + +然后在右上角的 `Settings` 中可以看到 `Bordered` 选项,可以动态修改 `Table` 的 `bordered` 属性。 + + + + +### 7. 区块 Toolbar 配置 + +我们还可以给区块增加 Toolbar 配置,让其在左上角显示所属的 Collection 表信息。 + +我们先修改 `tableSchema`。 + +```diff + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', ++ 'x-toolbar': 'MyToolbar', + // ... +} +``` + +然后我们实现 `MyToolbar`。 + +```tsx | pure +const MyToolbar = (props) => { + const collection = useCollection(); + const compile = useCompile(); + return +} +``` + +然后我们将 `MyToolbar` 注册到 NocoBase 应用中。 + +```diff +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable }) + this.app.schemaInitializerManager.add(myInitializer); + this.app.schemaSettingsManager.add(myTableSettings); + this.app.addScopes({ useTableProps }); ++ this.app.addComponents({ MyToolbar }); + } +} +``` + +这样左上角 Toolbar 就会显示所属的 Collection 表信息。 + +关于 `SchemaToolbar` 的更多信息请参考 [SchemaToolbar](/core/ui-schema/schema-toolbar)。 + + + +### 8. 增加顶部操作按钮 + +我们可以给区块增加顶部操作按钮,比如增加 `Add New` 和 `Refresh` 新增列表数据按钮。 + +我们先修改 `tableSchema`。 + +```diff + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + // ... + properties: { ++ actions: { ++ type: 'void', ++ 'x-component': 'ActionBar', ++ }, + // ... + } +} +``` + +然后我们实现 `ActionBar`。 + +```tsx | pure +const CreateAction = () => { + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + + const compile = useCompile(); + const collection = useCollection(); + const title = compile(collection.title); + + return <> + + +

Some contents...

+
+ +} + +const RefreshAction = () => { + const { refresh } = useDataBlockRequest(); + return +} + +const ActionBar = () => { + return ( +
+ + + + +
+ ) +}; +``` + +其中: + +- `useDataBlockRequest()`: 用于获取区块的请求对象,具体用法请参考 [useDataBlockRequest](/core/data-block/data-block-request-provider#usedatablockrequest)。 + +然后我们将 `ActionBar` 注册到 NocoBase 应用中。 + +```diff +class MyPlugin extends Plugin { + async load() { + // ... ++ this.app.addComponents({ ActionBar }); + } +} +``` + + + +### 9. 动态配置顶部操作按钮 + +上面的 `ActionBar` 里面的 `CreateAction` 和 `RefreshAction` 是固定的,我们可以将其配置动态化。 + +首先修改 `tableSchema`。 + +```diff + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + // ... + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', ++ 'x-initializer': 'tableActionInitializers', + }, + // ... + } +} +``` + +然后我们实现 `tableActionInitializers`。 + +```tsx | pure +const CreateActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'CreateAction', + }); + }; + return ; +} + + +const RefreshActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'RefreshAction', + }); + }; + return ; +} + +const tableActionInitializers = new SchemaInitializer({ + name: 'tableActionInitializers', + title: "Configure actions", + icon: 'SettingOutlined', + style: { + marginLeft: 8, + }, + items: [ + { + type: 'item', + name: 'addNew', + title: "Add New", + Component: CreateActionInitializer, + }, + { + type: 'item', + name: 'refresh', + title: "Refresh", + Component: RefreshActionInitializer, + }, + ] +}) +``` + +然后修改 `ActionBar`: + +```diff +const ActionBar = ({ children }) => { ++ const fieldSchema = useFieldSchema(); ++ const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + + return ( +
+ ++ {children} ++ {render()} + +
+ ) +}; +``` + +然后我们将 `tableActionInitializers` 和 `CreateAction`、`RefreshAction` 注册到 NocoBase 应用中。 + +```diff +class MyPlugin extends Plugin { + async load() { + // ... ++ this.app.addComponents({ CreateAction, RefreshAction }); ++ this.app.schemaInitializerManager.add(tableActionInitializers); + } +} +``` + + + +### 10. 操作按钮的配置 + +目前的操作按钮只能添加,不能删除或者配置 ,我们可以通过 `SchemaSettings` 来配置。 + +这里以 `Add New` 按钮为例。首先我们修改 `CreateActionInitializer`。 + +```diff +const CreateActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'CreateAction', ++ 'x-settings': 'createActionSettings', + }); + }; + return ; +}; +``` + +然后我们实现 `createActionSettings`。 + +```tsx | pure +const createActionSettings = new SchemaSettings({ + name: 'createActionSettings', + items: [ + { + type: 'remove', + name: 'remove', + }, + ], +}) +``` + +然后我们将 `createActionSettings` 注册到 NocoBase 应用中。 + +```diff +class MyPlugin extends Plugin { + async load() { + // ... ++ this.app.schemaSettingsManager.add(createActionSettings); + } +} +``` + +最后将 `createActionSettings` 渲染出来。 + +```diff +const CreateAction = () => { + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + + const compile = useCompile(); + const collection = useCollection(); + const title = compile(collection.title); + ++ const fieldSchema = useFieldSchema(); ++ const { render } = useSchemaToolbarRender(fieldSchema); + + return ( + <> +- ++
++ {render()} ++ ++
+ +

Some contents...

+
+ + ); +}; +``` + +需要注意的是,要将 `render()` 和 `Add New` 包裹在一个 `div` 中,这样 `render()` 才能正确的渲染。 + + + +### 11. 动态配置 table 列 + +首先修改 `tableSchema`。 + +```diff +const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + // ... + properties: { + action: { + // ... + }, + [uid()]: { + type: 'array', + 'x-component': 'MyTable', ++ 'x-initializer': 'tableColumnInitializers', + 'x-use-component-props': 'useTableProps', + }, + } +} +``` + +然后我们实现 `tableColumnInitializers`。 + +```tsx | pure +const tableColumnInitializers = new SchemaInitializer({ + name: 'tableColumnInitializers', + insertPosition: 'beforeEnd', + icon: 'SettingOutlined', + title: 'Configure columns', + items: [ + { + name: 'displayFields', + type: 'itemGroup', + title: 'Display fields', + useChildren: useTableColumnInitializerFields, + } + ], +}) + +const useTableColumnInitializerFields = () => { + const collection = useCollection(); + return collection.fields.map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + return { + type: 'item', + name: collectionField.name, + title: collectionField?.uiSchema?.title || collectionField.name, + Component: 'TableCollectionFieldInitializer', + schema: tableFieldSchema, + }; + }); +}; + +export const TableCollectionFieldInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { insert } = useSchemaInitializer(); + const { remove } = useDesignable(); + const schema = useFieldSchema(); + const exists = !!schema.properties?.[itemConfig.name]; + return ( + { + if (exists) { + return remove(schema.properties?.[itemConfig.name]); + } + insert(itemConfig.schema); + }} + /> + ); +}; +``` + +然后我们修改 `useTableProps`,增加 `useTableColumns`: + +```diff +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); ++ const columns = useTableColumns(); + + return { + ...tableProps, + loading, + dataSource, + columns, + }; +} +``` + +```tsx | pure +function useTableColumns() { + const schema = useFieldSchema(); + const filed = useField(); + const { designable } = useDesignable(); + const { render } = useSchemaInitializerRender(schema['x-initializer'], schema['x-initializer-props']); + const columns = schema.mapProperties((tableField: any, name) => { + return { + title: , + dataIndex: name, + key: name, + width: 200, + render(value, record, index) { + return ; + }, + }; + }); + + const tableColumns = useMemo(() => { + return [ + ...columns, + { + title: render(), + dataIndex: 'TABLE_COLUMN_INITIALIZER', + key: 'TABLE_COLUMN_INITIALIZER', + width: 200, + render: designable ? () =>
: null, + }, + ]; + }, [columns, render, designable]); + + return tableColumns; +} +``` + +然后我们将 `tableColumnInitializers` 和 `TableCollectionFieldInitializer` 注册到 NocoBase 应用中。 + +```diff +class MyPlugin extends Plugin { + async load() { + // ... ++ this.app.schemaInitializerManager.add(tableColumnInitializers); ++ this.app.addComponents({ TableCollectionFieldInitializer }); + } +} +``` + + + +### 12. 增加操作列 + +修改 `useTableColumns`: + +```diff +function useTableColumns() { + // ... + const tableColumns = useMemo(() => { + return [ + ...columns, ++ { ++ title: 'Actions', ++ dataIndex: 'actions', ++ key: 'actions', ++ render: (value, record) => { ++ return ( ++ ++ ++ ++ ++ ++ ); ++ }, ++ }, + { + title: render(), + dataIndex: 'TABLE_COLUMN_INITIALIZER', + key: 'TABLE_COLUMN_INITIALIZER', + render: designable ? () =>
: null, + }, + ]; + }, [columns, render, designable]); + + return tableColumns; +} +``` + +然后我们实现 `ColumnView`。 + +```tsx | pure + +const ColumnView = observer( + () => { + const record = useCollectionRecord(); + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + return ( + <> + + +
{JSON.stringify(record, null, 2)}
+
+ + ); + }, + { displayName: 'ColumnView' }, +); +``` + + diff --git a/packages/core/client/docs/en-US/core/data-source/association-provider.md b/packages/core/client/docs/en-US/core/data-source/association-provider.md new file mode 100644 index 000000000..97da3d3ab --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/association-provider.md @@ -0,0 +1,13 @@ +# AssociationProvider + +用于传递关联数据字段以及对应的数据表信息,等同于 `CollectionFieldProvider` + `CollectionProvider`。 + +```tsx | pure +const AssociationProvider = + + {children} + + +``` + +关于关系字段的更多信息,请参考 [关系字段](https://docs.nocobase.com/development/server/collections/association-fields) 以及 [CollectionField](/core/data-source/collection-field)。 diff --git a/packages/core/client/docs/en-US/core/data-source/collection-field-interface-manager.md b/packages/core/client/docs/en-US/core/data-source/collection-field-interface-manager.md new file mode 100644 index 000000000..b589784e5 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/collection-field-interface-manager.md @@ -0,0 +1,171 @@ +# CollectionFieldInterfaceManager + +主要是用来管理 [CollectionFieldInterface](./collection-field-interface.md) 和 [CollectionFieldInterfaceGroups](#collectionfieldinterfacegroups),其被 [DataSourceManager](./data-source-manager) 管理。 + + +## CollectionFieldInterfaceGroups + +CollectionFieldInterfaceGroups 是用来对数据表字段进行分组的。 + +![Field Groups](./images/field-groups.png) + +## 实例方法 + +### field interface + +#### addFieldInterfaces() + +添加 field interface。 + +- 类型 + +```tsx | pure +class CollectionFieldInterfaceManager { + addFieldInterfaces(fieldInterfaces: CollectionFieldInterface[]): void +} +``` + +- 示例 + +```tsx | pure +class CheckboxFieldInterface extends CollectionFieldInterface { + name = 'checkbox'; + type = 'object'; + group = 'choices'; + title = '{{t("Checkbox")}}'; + // ... +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.collectionFieldInterfaceManager.addFieldInterfaces([CheckboxFieldInterface]); + // or + this.app.dataSourceManager.addFieldInterfaces([CheckboxFieldInterface]); + } +} +``` + +#### getFieldInterface() + +获取 field interface。 + +- 类型 + +```tsx | pure +class CollectionFieldInterfaceManager { + getFieldInterface(name: string): T; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const fieldInterface = this.app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterface('checkbox'); // checkboxFieldInterface + } +} +``` + +#### getFieldInterfaces() + +获取所有的 field interface。 + +- 类型 + +```tsx | pure +class CollectionFieldInterfaceManager { + getFieldInterfaces(): CollectionFieldInterface[]; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const fieldInterfaces = this.app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterfaces(); + } +} +``` + +### field interface group + +#### addFieldInterfaceGroups() + +添加 field interface group。 + +- 类型 + +```tsx | pure +class CollectionFieldInterfaceManager { + addFieldInterfaceGroups(fieldGroups: Record): void; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.collectionFieldInterfaceManager.addFieldInterfaceGroups({ + 'test': { + label: 'Test', + order: 1, + } + }); + + // or + this.app.dataSourceManager.addFieldInterfaceGroups({ + 'test': { + label: 'Test', + order: 1, + } + }); + } +} +``` + +#### getFieldInterfaceGroups() + +获取所有的 field interface group。 + +- 类型 + +```tsx | pure +class CollectionFieldInterfaceManager { + getFieldInterfaceGroups(): Record; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const fieldInterfaceGroups = this.app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterfaceGroups(); // { 'test': { label: 'Test', order: 1 } } + } +} +``` + +#### getFieldInterfaceGroup() + +获取 field interface group。 + +- 类型 + +```tsx | pure +class CollectionFieldInterfaceManager { + getFieldInterfaceGroup(name: string): { label: string; order?: number }; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const fieldInterfaceGroup = this.app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterfaceGroup('test'); // { label: 'Test', order: 1 } + } +} +``` diff --git a/packages/core/client/docs/en-US/core/data-source/collection-field-interface.md b/packages/core/client/docs/en-US/core/data-source/collection-field-interface.md new file mode 100644 index 000000000..bb2c1aebf --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/collection-field-interface.md @@ -0,0 +1,94 @@ +# CollectionFieldInterface + +用于创建数据字段。 + +![](./images/collection-field-interface.png) + +```ts +class CollectionFieldInterface { + app: Application; + collectionManager: CollectionManager; + + name: string; + group: string; + title?: string; + description?: string; + order?: number; + default?: { + type: string; + uiSchema?: ISchema; + [key: string]: any; + }; + sortable?: boolean; + availableTypes?: string[]; + hasDefaultValue?: boolean; + isAssociation?: boolean; + operators?: any[]; + filterable?: { + operators?: any[]; + children?: any[]; + [key: string]: any; + }; + titleUsable?: boolean; + + validateSchema(fieldSchema: ISchema): Record + usePathOptions(field: CollectionFieldOptions): any + schemaInitialize(schema: ISchema, data: any): void + + getOption(key: K): CollectionFieldInterfaceOptions[K] + getOptions(): CollectionFieldInterfaceOptions; + setOptions(options: CollectionFieldInterfaceOptions): void; +} +``` + +其需要结合 [CollectionManager](./collection-field-interface-manager.md) 使用。 + +```ts +class EmailFieldInterface extends CollectionFieldInterface { + name = 'email'; + type = 'object'; + group = 'basic'; + order = 4; + title = '{{t("Email")}}'; + sortable = true; + // ... +} + +class MyPlugin extends Plugin { + load() { + this.app.dataSourceManager.addFieldInterfaces([ EmailFieldInterface ]); + } +} +``` + +## 实例属性 + +### name + +唯一标识符。 + +### group + +分组。 + +### title + +标题。 + +### default + +配置表单默认值字段 schema。 + +![](./images/collection-field-interface-form.png) + + +## 实例方法 + +### collectionFieldInterface.validateSchema(fieldSchema) + + +### collectionFieldInterface.usePathOptions(field: CollectionFieldOptions) + + +### collectionFieldInterface.schemaInitialize(schema: ISchema, data: any) + diff --git a/packages/core/client/docs/en-US/core/data-source/collection-field.md b/packages/core/client/docs/en-US/core/data-source/collection-field.md new file mode 100644 index 000000000..3f64b94a6 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/collection-field.md @@ -0,0 +1,164 @@ +# CollectionField + +NocoBase 将字段的 schema 分为 2 部分,一部分在 schema 中,一部分在 collection 中。例如: + +```tsx | pure +const schema = { + properties: { + username: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + }, + } +} + +const collection = { + fields: [ + { + type: 'string', + name: 'username', + interface: 'input', + uiSchema: { + title: 'UserName', + type: 'string', + 'x-component': 'Input', + required: true, + description: 'description1', + } as ISchema, + } + ], +} +``` + +两者通过 `name: username` 联系起来,`CollectionField` 会自动读取 schema `name` 属性,并根据 `name` 属性查找 collection 中对应的 `uiSchema` 属性,然后拼接到 schema 中,进行渲染。 + +这样做的好处是,对于同一个字段创建的内容,可以在不同的地方共享同一个 schema,当 schema 变化时,只需要修改一处即可。比如通过上面的 `title: "UserName"` 假设变化 `title: "Name"` 则所有使用到此字段的地方都会变化。 + + + +## CollectionFieldOptions + +字段的配置项。 + +```ts +interface CollectionFieldOptions { + name?: any; + collectionName?: string; + sourceKey?: string; + uiSchema?: ISchema; + target?: string; + + [key: string]: any; +} +``` + +### 普通字段和关系字段 + +字段有 2 种情况,一种是普通字段,一种是 [关系字段](https://docs-cn.nocobase.com/development/server/collections/association-fields)。 + +关系字段是指,字段的值是另一个 collection 的数据,例如 `users` 和 `roles` 两个 collection,`users` 中有一个字段 `roles`,其值是 `roles` collection 的数据,那么 `roles` 就是一个关系字段。 + +普通字段的示例如下: + +```json +{ + "key": "ootprgkoawo", + "name": "email", + "type": "string", + "interface": "email", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "unique": true, + "uiSchema": { + "type": "string", + "title": "{{t(\"Email\")}}", + "x-component": "Input", + "x-validator": "email", + "required": true + } +} +``` + +关系字段的示例如下: + +```json +{ + "key": "t09bauwm0wb", + "name": "roles", + "type": "belongsToMany", + "interface": "m2m", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "target": "roles", + "foreignKey": "userId", + "otherKey": "roleName", + "onDelete": "CASCADE", + "sourceKey": "id", + "targetKey": "name", + "through": "rolesUsers", + "uiSchema": { + "type": "array", + "title": "{{t(\"Roles\")}}", + "x-component": "AssociationField", + "x-component-props": { + "multiple": true, + "fieldNames": { + "label": "title", + "value": "name" + } + } + } +} +``` + +相对于普通字段,关系字段多了以下属性: + +- xx +- xx + +### 全部字段说明 + +- `name`:字段名称 +- `collectionName`:数据表名称 +- `sourceKey`:当字段为关系字段时,对应的关系字段名称。 + +TODO:补全 + +## Hooks + +### useCollectionField() + +用于获取字段信息。 + +```tsx | pure +const collection = { + fields: [ + { + type: 'string', + name: 'username', + interface: 'input', + uiSchema: { + title: 'UserName', + type: 'string', + 'x-component': 'Input', + required: true, + description: 'description1', + } as ISchema, + } + ], +} + +const { uiSchema } = useCollectionField() +const required = uiSchema?.required +``` + +其通常在 [SchemaSettings](/core/ui-schema/schema-settings) 中使用,用来获取和修改字段的属性。 + + + + + diff --git a/packages/core/client/docs/en-US/core/data-source/collection-manager-provider.md b/packages/core/client/docs/en-US/core/data-source/collection-manager-provider.md new file mode 100644 index 000000000..b37a6a27f --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/collection-manager-provider.md @@ -0,0 +1,52 @@ +# CollectionManagerProvider + +用于提供 [CollectionManager](/core/data-source/collection-manager) 实例。 + +## 组件 + +- 类型 + +```tsx | pure +interface CollectionManagerProviderProps { + instance?: CollectionManager; + dataSource?: string; + children?: ReactNode; +} +``` + +- 参数详解 + - `dataSource` - 数据源名称,如果为空,则会取默认数据源。 + - `instance` - CollectionManager 实例,如果没有,则会取 `dataSource` 对应的 collectionManager。 + +- 示例 + +```tsx | pure +const collectionManager = new CollectionManager(); + +const Demo = () => { + return ( + +
...
+
+ ); +}; +``` + +## Hooks + +### useCollectionManager() + +用于获取 `CollectionManagerProvider` 传递的实例。 + +- 示例 + +```tsx | pure +const Demo = () => { + const collectionManager = useCollectionManager(); + const collections = collectionManager.getCollections() + + return
+
{JSON.stringify(collections, null, 2)}
+
; +}; +``` diff --git a/packages/core/client/docs/en-US/core/data-source/collection-manager.md b/packages/core/client/docs/en-US/core/data-source/collection-manager.md new file mode 100644 index 000000000..5df92575d --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/collection-manager.md @@ -0,0 +1,175 @@ +# CollectionManager + +用于管理 [Collection](./collection.md),其被 [DataSource](./data-source.md) 管理。 + +## 实例方法 + +### addCollections(collections) + +添加数据表。 + +- 类型 + +```tsx | pure +class CollectionManager { + addCollections(collections: CollectionOptions[]): void +} +``` + +- 示例 + +```tsx | pure +const userCollectionOptions = { + "name": "users", + "title": "Users", + fields: [ + // ... + ], +}; + +collectionManager.addCollections([userCollectionOptions]); +``` + +### setCollections(collections) + +重置数据表,会先移除所有数据表,然后再调用 `addCollections()` 添加数据表。 + +- 类型 + +```tsx | pure +class CollectionManager { + setCollections(collections: CollectionOptions[]): void +} +``` + +### reAddCollections(collections) + +由于 [CollectionTemplate](./collection-template.md) 或者 [CollectionMixins](./collection-mixins.md) 的添加会影响 Collection 的实例化,所以提供了重新添加数据表的方法。 + +- 类型 + +```tsx | pure +class CollectionManager { + reAddCollections(collectionInstances: Collection[]): void +} +``` + +- 示例 + +```tsx | pure +const userCollectionInstance = collectionManager.getCollection('users'); +collectionManager.reAddCollections([userCollectionInstance]); +``` + +### getCollections(predicate?) + +获取数据表。 + +- 类型 + +```tsx | pure +class CollectionManager { + getCollections(predicate?: (collection: Collection) => boolean) +} +``` + +- 示例 + +```tsx | pure +collectionManager.getCollections(); // [ userCollection ] + +collectionManager.getCollections(collection => collection.name === 'posts'); // [ postCollection ] +``` + + +### getCollection(path) + +获取数据表。 + +- 类型 + +```tsx | pure +class CollectionManager { + getCollection(path: string): (Mixins & Collection) | undefined +} +``` + +- 详细解释 + - `path` 参数可以是数据表名称,也可以是[关系字段](https://docs.nocobase.com/development/server/collections/association-fields)路径。 + - `path: 'users'`: 获取 `users` 数据表 + - `path: 'users.posts'`: 获取 `users` 数据表的 `posts` 关联字段对应的数据表,即 `postCollection` + +- 示例 + +```tsx | pure +collectionManager.getCollection('users'); // userCollection + +collectionManager.getCollection('users.posts'); // postCollection +collectionManager.getCollection('users.profileId'); // profileCollection +``` + +结合 Mixin 使用: + +```tsx | pure +const collection = collectionManager.getCollection('users'); +const collection = collectionManager.getCollection('users'); +``` + +### getCollectionFields(collectionName) + +获取数据表字段列表。 + +- 类型 + +```tsx | pure +class CollectionManager { + getCollectionFields(collectionName: string): CollectionFieldOptions[]; +} +``` + +- 示例 + +```tsx | pure +collectionManager.getCollectionFields('users'); // [ { name: 'username', type: 'string', title: 'Username', .. }, { name: 'password', type: 'password', title: 'Password', .. } ] +``` + +### getCollectionName(path) + +获取数据表名称。 + +- 类型 + +```tsx | pure +class CollectionManager { + getCollectionName(path: string: GetCollectionOptions): string | undefined; +} +``` + +- 示例 + +```tsx | pure +collectionManager.getCollectionName('users'); // 'users' + +collectionManager.getCollectionName('users.profiles'); // 'profiles' +``` + + +### getCollectionField(path) + +获取数据表字段。 + +- 类型 + +```tsx | pure +class CollectionManager { + getCollectionField(path: string: GetCollectionOptions): CollectionFieldOptions | undefined; +} +``` + +- 示例 + +```tsx | pure +collectionManager.getCollectionField('users.username'); // { name: 'username', type: 'string', title: 'Username', .. } + +collectionManager.getCollectionField('users.roles.name'); // 获取 roles 关联字段对应的 roles 表中的 name 字段 +``` diff --git a/packages/core/client/docs/en-US/core/data-source/collection-mixins.md b/packages/core/client/docs/en-US/core/data-source/collection-mixins.md new file mode 100644 index 000000000..6512fccc3 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/collection-mixins.md @@ -0,0 +1,57 @@ + +## Collection Mixins + +Collection Mixins 是为扩展 Collection 类提供的一种机制,可以通过 `dataSourceManager.addCollectionMixins()` 添加 Collection Mixins。 + +### 定义和注册 + +```tsx | pure +import { Collection, Plugin } from '@nocobase/client'; + +class TestMixin extends Collection { + test() { + const { name } = this.options; + return 'test '+ name; + } +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.addCollectionMixins([TestMixin]); + } +} +``` + +### 使用 + +- 使用方式1:在 `CollectionManager` 实例上调用 `getCollection()` 获取指定 `Collection` 实例。 + +```tsx | pure +const Demo = () => { + const cm = useCollectionManager(); + const userCollection = cm.getCollection('users'); + + userCollection.test(); // 'test users' +} +``` + +- 使用方式2:调用 `useCollection()` 获取当前上下文的数据表信息。 + +```tsx | pure +const Demo = () => { + const collection = useCollection(); + collection.test(); // 'test users' +} +``` + +### 多个 Mixins 的使用 + +如果添加了 Mixins,可通过如下方式获得类型提示: + +```tsx | pure +const Demo = () => { + const collection = useCollection(); +} +``` + + diff --git a/packages/core/client/docs/en-US/core/data-source/collection-provider.md b/packages/core/client/docs/en-US/core/data-source/collection-provider.md new file mode 100644 index 000000000..141187df8 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/collection-provider.md @@ -0,0 +1,63 @@ +# CollectionProvider + +用于提供 [Collection](/core/data-source/collction) 实例。 + +## 组件 + +### CollectionProvider + +- 类型 + +```tsx | pure +interface CollectionProviderProps { + name: string; + dataSource?: string; + children?: ReactNode; +} +``` + +- 详解 + +组件会根据 `name` 去 [CollectionManager](/core/data-source/collection-manager) 中查询数据表信息,如果查询不到,则会不进行渲染。 + +`dataSource` 用于指定数据表所在的[命名空间](/core/data-source/collection-manager#datasource),如果不指定,则默认命名空间。 + +- 示例 + +```tsx | pure +import { CollectionProvider } from '@nocobase/client'; + +const MyComponent = () => { + return ( + +
...
+
+ ) +} +``` + + +## Hooks + +### useCollection() + +用于获取 `CollectionProvider` 传递的 `Collection` 实例。 + +```tsx | pure +const collection = useCollection() + +console.log(collection instanceof Collection) // true +console.log(collection); +``` + +结合 Mixin 使用: + +```tsx | pure +const collection = useCollection() +const collection = useCollection() +``` + +## 示例 + + + diff --git a/packages/core/client/docs/en-US/core/data-source/collection-template-manager.md b/packages/core/client/docs/en-US/core/data-source/collection-template-manager.md new file mode 100644 index 000000000..1ff0bffea --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/collection-template-manager.md @@ -0,0 +1,86 @@ +# CollectionTemplateManager + +用于管理 [CollectionTemplate](./collection-template),其被 [DataSourceManager](./data-source-manager) 管理。 + +## 实例方法 + +### addCollectionTemplates() + +用于添加 collection template。 + +- 类型 + +```tsx | pure +class CollectionTemplateManager { + addCollectionTemplates(templates: CollectionTemplate[]): void; +} +``` + +- 示例 + +```tsx | pure +class SqlCollectionTemplate extends CollectionTemplate { + name = 'sql'; + type = 'object'; + title = '{{t("SQL collection")}}'; + configurableProperties = { + // ... + } +} + +class TreeCollectionTemplate extends CollectionTemplate { + name = 'tree'; + type = 'object'; + title = '{{t("Tree collection")}}'; + configurableProperties = { + // ... + } +} + +class MyPlugin extends Plugin { + async load() { + this.dataSourceManager.collectionTemplateManager.addCollectionTemplates([ SqlCollectionTemplate, TreeCollectionTemplate ]); + + // or + this.dataSourceManager.addCollectionTemplates([ SqlCollectionTemplate, TreeCollectionTemplate ]); + } +} +``` + +### getCollectionTemplate() + +用于获取 collection template。 + +- 类型 + +```tsx | pure +class CollectionTemplateManager { + getCollectionTemplate(name: string): CollectionTemplate; +} +``` + +- 示例 + +```tsx | pure +collectionManager.getCollectionTemplate(); // generalCollectionTemplate + +collectionManager.getCollectionTemplate('tree'); // treeCollectionTemplate +``` + +### getCollectionTemplates() + +用于获取所有 collection templates。 + +- 类型 + +```tsx | pure +class CollectionTemplateManager { + getCollectionTemplates(): CollectionTemplate[]; +} +``` + +- 示例 + +```tsx | pure +collectionManager.getCollectionTemplates(); // [ generalCollectionTemplate, treeCollectionTemplate, sqlCollectionTemplate ] +``` diff --git a/packages/core/client/docs/en-US/core/data-source/collection-template.md b/packages/core/client/docs/en-US/core/data-source/collection-template.md new file mode 100644 index 000000000..eb248e519 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/collection-template.md @@ -0,0 +1,249 @@ +# CollectionTemplate + +用于创建数据表的模板。 + +![](./images/collection-template.png) + +```ts +interface AvailableFieldInterfacesInclude { + include?: any[]; +} + +interface AvailableFieldInterfacesExclude { + exclude?: any[]; +} + +interface CollectionTemplateDefaultOptions { + /** + * 自动生成 id + * @default true + * */ + autoGenId?: boolean; + /** 创建人 */ + createdBy?: boolean; + /** 最后更新人 */ + updatedBy?: boolean; + /** 创建日期 */ + createdAt?: boolean; + /** 更新日期 */ + updatedAt?: boolean; + /** 可排序 */ + sortable?: boolean; + /* 树结构 */ + tree?: string; + /* 日志 */ + logging?: boolean; + /** 继承 */ + inherits?: string | string[]; + /* 字段列表 */ + fields?: CollectionOptions['fields']; +} + +class CollectionTemplate { + app: Application; + collectionManager: CollectionManager; + + name: string; + Collection?: typeof Collection; + transform?: (collection: CollectionOptions, app: Application) => CollectionOptions; + title?: string; + color?: string; + /** 排序 */ + order?: number; + /** 默认配置 */ + default?: CollectionTemplateDefaultOptions; + events?: any; + /** UI 可配置的 CollectionOptions 参数(添加或编辑的 Collection 表单的字段) */ + configurableProperties?: Record; + /** 当前模板可用的字段类型 */ + availableFieldInterfaces?: AvailableFieldInterfacesInclude | AvailableFieldInterfacesExclude; + /** 是否分割线 */ + divider?: boolean; + /** 模板描述 */ + description?: string; + /**配置字段中的操作按钮 */ + configureActions?: Record; + //是否禁止删除字段 + forbidDeletion?: boolean; +} +``` + +其需要结合 [CollectionManager](./collection-template-manager.md) 使用。 + +```ts +import { Plugin, Collection, CollectionTemplate } from '@nocobase/client'; + +class SqlCollection extends Collection { + otherMethods() { + // ... + } +} + +class SqlCollectionTemplate extends CollectionTemplate { + name = 'sql'; + Collection = SqlCollection; // 自定义的数据表类 + title = '{{t("SQL collection")}}'; + order = 4; + color = 'yellow'; + default = { + fields: [], + }; + configurableProperties = { + // ... + } +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.collectionTemplateManager.addCollectionTemplates([ SqlCollectionTemplate ]); + + // or + this.app.dataSourceManager.addCollectionTemplates([ SqlCollectionTemplate ]); + } +} +``` + +## 实例属性 + +### name + +模板的唯一标识符。 + + +### Collection + +模板对应的数据表类。 + +在创建数据表后,Collection 会有 [template 字段](/core/data-source/collection#collectionoptions),用于标识该数据表是由哪个模板创建的。 + +当通过 `collectionManager.addCollections()` 添加数据表对象时,会先读取 `collection.template` 字段,然后通过 `collectionManager.getCollectionTemplate(collection.template)` 获取到 `collectionTemplate`。 + +读取 `collectionTemplate.Collection` 字段,并通过 `new collectionTemplate.Collection(collection)` 创建对应的实例。 + +如果不传递 `Collection`,则会通过 `new Collection(collection)` 创建对应的实例。 + +```ts +class SqlCollection extends Collection { + otherMethods() { + // ... + } +} + +class SqlCollectionTemplate extends CollectionTemplate { + name = 'sql'; + Collection = SqlCollection; // 自定义的数据表类 + // ... +} + +const userCollection = { + name: 'users', + template: 'sql', + // ... +} + +// 内部会调用 new SqlCollection(userCollection) +``` + +### title + +模板的标题。 + +### color + +模板的颜色。 + +### order + +模板的排序。 + +### events + +- `beforeSubmit`:提交前触发 + + +### configurableProperties + +表单配置项。 + +![](./images//collection-template-form.png) + +```ts +class SqlCollectionTemplate extends CollectionTemplate { + name = 'sql', + // ... + configurableProperties = { + title: { + type: 'string', + title: '{{ t("Collection display name") }}', + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'Input', + }, + name: { + type: 'string', + title: '{{t("Collection name")}}', + required: true, + 'x-disabled': '{{ !createOnly }}', + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-validator': 'uid', + description: + "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}", + }, + // ... + }, +} +``` + +### default + +表单默认值。 + + + +## 实例方法 + +### collectionTemplate.transform(collection) + +collection 创建后,会调用该方法,用于对 collection 进行转换。 + +## Utils + +### getConfigurableProperties() + +用于获取内置的配置项字段。 + +- 类型 + +```tsx | pure +export type DefaultConfigurableKeys = + | 'name' + | 'title' + | 'inherits' + | 'category' + | 'autoGenId' + | 'createdBy' + | 'updatedBy' + | 'createdAt' + | 'updatedAt' + | 'sortable' + | 'description' + | 'moreOptions'; + +const getConfigurableProperties: (...keys: DefaultConfigurableKeys[]) => Record +``` + +- 示例 + +```tsx | pure +import { getConfigurableProperties } from '@nocobase/client'; + +const sqlCollectionTemplate = new CollectionTemplate({ + name: 'sql', + // ... + configurableProperties: { + ...getConfigurableProperties('name', 'title', 'description'), + // ... + }, +}); +``` diff --git a/packages/core/client/docs/en-US/core/data-source/collection.md b/packages/core/client/docs/en-US/core/data-source/collection.md new file mode 100644 index 000000000..5cce1e551 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/collection.md @@ -0,0 +1,293 @@ +# Collection + +Collection 数据表类,其被 [CollectionManager](/core/data-source/collection-manager) 管理。 + +## 类型 + +```tsx | pure +interface CollectionOptions { + name: string; + title?: string; + fields?: FieldOptions[]; + // .... +} + +class Collection { + app: Application; + collectionManager: CollectionManager; + + constructor(options: CollectionOptions) {} + + name: string; + primaryKey: string; + titleField: string; + + getOptions(): CollectionOptions; + setOptions(options: CollectionOptions): void; + getOption(key: K): CollectionOptions[K]; + + getFields(predicate?: CollectionFieldOptions | ((collection: CollectionFieldOptions) => boolean) | keyof CollectionFieldOptions): any[] + getField(name: SchemaKey): CollectionFieldOptions + hasField(name: SchemaKey): boolean; +} +``` + +```tsx | pure +const usersCollection = new Collection({ + name: 'users', + title: 'Users', + fields: [ + { + type: 'string', + name: 'username', + }, + { + type: 'integer', + name: 'age', + }, + ], +}); +``` + +## CollectionOptions + +```tsx | pure +export interface CollectionOptions { + name: string; + title?: string; + dataSource?: string; + /** + * Used for @nocobase/plugin-duplicator + * @see packages/core/database/src/collection-group-manager.tss + * + * @prop {'required' | 'optional' | 'skip'} dumpable - Determine whether the collection is dumped + * @prop {string[] | string} [with] - Collections dumped with this collection + * @prop {any} [delayRestore] - A function to execute after all collections are restored + */ + duplicator?: + | dumpable + | { + dumpable: dumpable; + with?: string[] | string; + delayRestore?: any; + }; + + tableName?: string; + inherits?: string[] | string; + inherit?: string; + key?: string; + viewName?: string; + writableView?: boolean; + + filterTargetKey?: string; + fields?: CollectionFieldOptions[]; + model?: any; + repository?: any; + sortable?: CollectionSortable; + /** + * @default true + */ + autoGenId?: boolean; + /** + * @default 'options' + */ + magicAttribute?: string; + + tree?: string; + + template?: string; + + isThrough?: boolean; + autoCreate?: boolean; + resource?: string; + collectionName?: string; + sourceKey?: string; + uiSchema?: any; + [key: string]: any; +} +``` + +- name: Collection 的标识,必须唯一。 + +- title: Collection 的标题,用于显示。 + +- fields: 字段列表,详细说明请查看 [CollectionField](/core/data-source/collection-field) + +- template: 模板标识,用于标识该 Collection 是由哪个模板创建的,详细说明请查看 [CollectionTemplate](/core/data-source/collection-template) + +- dataSource: 数据源标识,用于标识该 Collection 是由哪个数据源创建的,详细说明请查看 [CollectionDataSource](/core/data-source/collection-manager#datasource) + +- duplicator + +- tableName + +- inherits + +- viewName + +- writableView + +- filterTargetKey + +- model + +- repository + + +## 实例属性 + +### collection.collectionManager + +[CollectionManager](/core/data-source/collection-manager) 的实例。 + +### collection.titleFieldName + +标题字段的 name 属性。 + +### 其他属性 + +其他属性同 [CollectionOptions](/core/data-source/collection#collectionoptions)。 + +## 实例方法 + +### collection.getOptions() + +获取 collection 的所有配置项。 + +- 类型 + +```tsx | pure +class Collection { + getOptions(): CollectionOptions; +} +``` + +- 示例 + +```tsx | pure +const usersCollection = new Collection({ + name: 'users', + title: 'Users', + fields: [ + // ... + ], +}); + +console.log(usersCollection.getOptions()); // { name: 'users', title: 'Users', fields: [ ] } +``` + +### collection.setOptions(options) + +设置 collection 的配置项,最终会和默认配置项进行合并。 + +- 类型 + +```tsx | pure +class Collection { + setOptions(options: CollectionOptions): void; +} +``` + +- 示例 + +```tsx | pure +collection.setOptions({ + name: 'users', + title: 'Users', + fields: [ + // ... + ], +}); +``` + +### collection.getOption(key) + +获取 collection 的单个配置项。 + +- 类型 + +```tsx | pure +class Collection { + getOption(key: K): CollectionOptions[K]; +} +``` + +- 示例 + +```tsx | pure +collection.getOption('name'); // 'users' +collection.getOption('title'); // 'Users' +``` + +### collection.getFields(predicate?) + +获取 collection 的字段列表。 + +- 类型 + +```tsx | pure +class Collection { + getFields(predicate?: CollectionFieldOptions | ((collection: CollectionFieldOptions) => boolean) | keyof CollectionFieldOptions): any[] +} +``` + +- 详解 + - predicate + - 类型 + - `CollectionFieldOptions` + - `(collection: CollectionFieldOptions) => boolean` + - `keyof CollectionFieldOptions` + - 说明 + - 如果传递了 `predicate`,则返回符合条件的字段列表 + - 如果没有传递 `predicate`,则返回所有字段列表 + +`predicate` 的使用可看参考 [lodash.filter](https://www.lodashjs.com/docs/lodash.filter)。 + +- 示例 + +```tsx | pure +collection.getFields(); // [{ name: 'username', type: 'string', primaryKey: true }, { name: 'age', type: 'integer' }] + +collection.getFields({ name: 'age' }); // [{ name: 'age', type: 'integer' }] + +collection.getFields('primaryKey'); // [{ name: 'username', type: 'string', primaryKey: true }] + +collection.getFields(field => field.type === 'string'); // [{ name: 'name', type: 'string' }] +``` + +### collection.getField(name) + +获取 collection 的单个字段。 + +- 类型 + +```tsx | pure +class Collection { + getField(name: SchemaKey): CollectionFieldOptions +} +``` + +- 示例 + +```tsx | pure +collection.getField('username'); // { name: 'username', type: 'string', primaryKey: true } +``` + +### collection.hasField(name) + +判断 collection 是否存在某个字段。 + +- 类型 + +```tsx | pure +class Collection { + hasField(name: SchemaKey): boolean; +} +``` + +- 示例 + +```tsx | pure +collection.hasField('username'); // true +collection.hasField('name'); // false +``` diff --git a/packages/core/client/docs/en-US/core/data-source/data-source-manager-provider.md b/packages/core/client/docs/en-US/core/data-source/data-source-manager-provider.md new file mode 100644 index 000000000..4b39e452c --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/data-source-manager-provider.md @@ -0,0 +1,25 @@ +# DataSourceManagerProvider + +用于提供 `DataSourceManager` 实例。 + + +## Hooks + +### useDataSourceManager() + +获取 `DataSourceManager` 实例。 + +- 类型 + +```tsx | pure +function useDataSourceManager(): DataSourceManager; +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSourceManager = useDataSourceManager(); + dataSourceManager.removeDataSources(['my-data-source']); +} +``` diff --git a/packages/core/client/docs/en-US/core/data-source/data-source-manager.md b/packages/core/client/docs/en-US/core/data-source/data-source-manager.md new file mode 100644 index 000000000..fd4b43c05 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/data-source-manager.md @@ -0,0 +1,331 @@ +# DataSourceManager + +Nocobase 数据源前端体系包含如下内容: + +- [DataSourceManager](./data-source-manager.md) + - [DataSource](./data-source) + - [CollectionManager](./collection-manager) + - [Collection](./collection) + - [Field](./collection-field) + - [CollectionTemplateManager](./collection-template-manager) + - [CollectionTemplate](./collection-template) + - [CollectionFieldInterfaceManager](./collection-field-interface-manager) + - [CollectionFieldInterface](./collection-field-interface) + - [CollectionMixins](./collection-mixins) + + +## 实例属性 + +- collectionTemplateManager + +用于管理 `CollectionTemplate` 实例。 + +```tsx | pure +import { Plugin, CollectionTemplate } from '@nocobase/client'; + +class MyCollectionTemplate extends CollectionTemplate { + name = 'my-collection-template'; +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.collectionTemplateManager.addCollectionTemplates([MyCollectionTemplate]) + } +} +``` + +详细请参考:[CollectionTemplateManager](./collection-template-manager) + +- collectionFieldInterfaceManager + +用于管理 `CollectionFieldInterface` 实例。 + +```tsx | pure +import { Plugin, CollectionFieldInterface } from '@nocobase/client'; + +class MyCollectionFieldInterface extends CollectionFieldInterface { + name = 'my-collection-field-interface'; +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.collectionFieldInterfaceManager.addFieldInterfaces([MyCollectionFieldInterface]) + } +} +``` + +详细请参考:[CollectionTemplateManager](./collection-template-manager) + +## 实例方法 + +### addCollectionTemplates() + +是 `CollectionTemplateManager` 的快捷方法,用于添加 `CollectionTemplate`。 + +- 类型 + +```tsx | pure +class DataSourceManager { + addCollectionTemplates(templates: CollectionTemplate[]): void; +} +``` + +- 示例 + +```tsx | pure +import { Plugin, CollectionTemplate } from '@nocobase/client'; + +class MyCollectionTemplate extends CollectionTemplate { + name = 'my-collection-template'; +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.collectionTemplateManager.addCollectionTemplates([MyCollectionTemplate]) + } +} +``` + +更多详细请参考:[CollectionTemplateManager](./collection-template-manager) + +### addFieldInterfaces() + +是 `CollectionFieldInterfaceManager` 的快捷方法,用于添加 `CollectionFieldInterface`。 + +- 类型 + +```tsx | pure +class DataSourceManager { + addFieldInterfaces(fieldInterfaces: CollectionFieldInterface[]): void; +} +``` + +- 示例 + +```tsx | pure +import { Plugin, CollectionFieldInterface } from '@nocobase/client'; + +class MyCollectionFieldInterface extends CollectionFieldInterface { + name = 'my-collection-field-interface'; +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.collectionFieldInterfaceManager.addFieldInterfaces([MyCollectionFieldInterface]) + } +} +``` + +更多详细请参考:[CollectionFieldInterfaceManager](./collection-field-interface-manager) + +### addCollectionMixins() + +用于添加 `Collection` 的 Mixins。 + +- 类型 + +```tsx | pure +class DataSourceManager { + addCollectionMixins(mixins: (typeof Collection)[]): void; +} +``` + +- 示例 + +```tsx | pure +import { Plugin, Collection } from '@nocobase/client'; + +class MyCollectionMixin extends Collection { + otherMethod() { + console.log('otherMethod'); + } +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.addCollectionMixins([MyCollectionMixin]) + } +} + +const MyComponent = () => { + const collection = useCollection(); + collection.otherMethod(); +} +``` + +更多详细请参考:[CollectionMixins](./collection-mixins) + +### addDataSource() + +用于添加 `DataSource`。 + +- 类型 + +```tsx | pure +class DataSourceManager { + addDataSource(DataSource: DataSource, options: DataSourceOptions): void; +} +``` + +- 示例 + +```tsx | pure +import { Plugin, DataSource, DataSourceOptions } from '@nocobase/client'; + +class MyDataSource extends DataSource { + async getDataSource() { + return { + status: 'loaded', + collections: [{ name: 'users' }] + } + } +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.addDataSource(MyDataSource, { + key: 'my-data-source', + displayName: 'My Data Source', + }) + } +} +``` + +更多详细请参考:[DataSource](./data-source) + +### removeDataSources() + +移除 `DataSource`。 + +- 类型 + +```tsx | pure +class DataSourceManager { + removeDataSources(keys: string[]): void; +} +``` + +- 示例 + +```tsx | pure + +const MyComponent = () => { + const dataSourceManager = useDataSourceManager(); + dataSourceManager.removeDataSources(['my-data-source']); +} +``` + +### getDataSources() + +获取全部 `DataSource` 实例列表。 + +- 类型 + +```tsx | pure +class DataSourceManager { + getDataSources(): DataSource[]; +} +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSourceManager = useDataSourceManager(); + const dataSources = dataSourceManager.getDataSources(); + + return ( +
+ {dataSources.map(dataSource => ( +
{dataSource.displayName}
+ ))} +
+ ) +} +``` + +### getDataSource() + +获取 `DataSource` 实例。 + +- 类型 + +```tsx | pure +class DataSourceManager { + getDataSource(key: string): DataSource; +} +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSourceManager = useDataSourceManager(); + const dataSource = dataSourceManager.getDataSource('my-data-source'); + + return ( +
+ {dataSource.displayName} +
+ ) +} +``` + +### getAllCollections() + +获取所有 DataSource 的所有 Collection 实例。 + +- 类型 + +```tsx | pure + +class DataSourceManager { + getAllCollections(predicate?: (collection: Collection) => boolean): (DataSourceOptions & { collections: Collection[] })[]; +} +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSourceManager = useDataSourceManager(); + const collections = dataSourceManager.getAllCollections(); + + return ( +
+ {collections.map(({ key, displayName, collections }) => ( +
+

{displayName}

+
    + {collections.map(collection => ( +
  • {collection.name}
  • + ))} +
+
+ ))} +
+ ) +} +``` + +### reload() + +重载所有 `DataSource`。 + +- 类型 + +```tsx | pure +class DataSourceManager { + reload(): Promise; +} +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSourceManager = useDataSourceManager(); + dataSourceManager.reload(); +} +``` + diff --git a/packages/core/client/docs/en-US/core/data-source/data-source-provider.md b/packages/core/client/docs/en-US/core/data-source/data-source-provider.md new file mode 100644 index 000000000..909a4d894 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/data-source-provider.md @@ -0,0 +1,66 @@ +# DataSourceProvider + +用于提供 `DataSource` 的实例。 + +## 组件 + +- 类型 + +```tsx | pure +interface DataSourceProviderProps { + dataSource?: string; + children?: ReactNode; +} +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + return ( + + + + ); +} +``` + +## Hooks + +### useDataSource() + +获取 `DataSource` 实例。 + +- 类型 + +```tsx | pure +function useDataSource(): DataSource; +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSource = useDataSource(); + return
{dataSource.displayName}
+} +``` + +### useDataSourceKey() + +获取 `DataSource` 的 key。 + +- 类型 + +```tsx | pure +function useDataSourceKey(): string; +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSourceKey = useDataSourceKey(); + return
{dataSourceKey}
+} +``` diff --git a/packages/core/client/docs/en-US/core/data-source/data-source.md b/packages/core/client/docs/en-US/core/data-source/data-source.md new file mode 100644 index 000000000..558a37089 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/data-source.md @@ -0,0 +1,176 @@ +# DataSource + +主要是用于获取数据源和数据源的数据表结构列表,并在获取后交给 [CollectionManager](./collection-manager.md) 进行管理,其被 [DataSourceManager](./data-source-manager.md) 管理。 + +## 数据源定义 + +数据源的定义需要继承 `DataSource` 类,并实现 `getDataSource` 方法,当调用 `reload` 方法时,会调用 `getDataSource` 方法获取数据表结构。 + +```tsx | pure +import { DataSource } from '@nocobase/client'; + +class MyDataSource extends DataSource { + async getDataSource() { + return this.app.request({ + url: 'xxx', + method: 'GET', + }); + } +} +``` + +### 数据源注册 + +数据源需要在插件中注册,通过 `DataSourceManager` 的 `addDataSource` 方法进行注册。 + +初始化添加的时候 `collections` 可以为空,当调用 `reload` 方法时,会调用 `getDataSource` 方法获取数据表结构。 + +```tsx | pure +import { Plugin, DataSource, DataSourceOptions } from '@nocobase/client'; + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.addDataSource(MyDataSource, { + key: 'my-data-source', + displayName: 'My Data Source', + status: 'loaded', + collections: [ + { + name: 'users', + fields: [ + { + name: 'name', + type: 'string', + }, + ], + }, + ], + }); + } +} +``` + +- `key`:数据源的唯一标识 +- `displayName`:数据源的显示名称 +- `status`:数据源的状态,`loaded` 表示已加载,`loading` 表示正在加载,`loading-failed` 表示加载失败 +- `collections`:数据表结构 +- `errorMessage`:错误信息 + +## 实例方法 + +### getDataSource() + +用于获取数据源信息,其会被 `reload` 方法内部调用,外部不需要调用。 + +### addReloadCallback() + +用于添加数据源加载完成后的回调函数。 + +- 类型 + +```tsx | pure +type LoadCallback = (collections: CollectionOptions[]) => void; + +class DataSource { + addReloadCallback(callback: LoadCallback): void; +} +``` + +### removeReloadCallback() + +用于移除数据源加载完成后的回调函数。 + +- 类型 + +```tsx | pure +type LoadCallback = (collections: CollectionOptions[]) => void; +class DataSource { + removeReloadCallback(callback: LoadCallback): void; +} +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSource = useDataSource(); + + useEffect(() => { + const callback = (collections) => { + console.log(collections); + }; + dataSource.addReloadCallback(callback); + return () => { + dataSource.removeReloadCallback(callback); + }; + }, []); +}; +``` + +### reload() + +用于重新加载数据源,会调用 `getDataSource` 方法获取数据表结构,并内部调用 `addReloadCallback` 添加的回调函数。 + +- 类型 + +```tsx | pure +class DataSource { + reload(): Promise; +} +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSource = useDataSource(); + + const handleClick = async () => { + await dataSource.reload(); + }; +}; +``` + +### getOptions() + +获取数据源的配置信息。 + +- 类型 + +```tsx | pure +interface DataSourceOptions { + key: string; + displayName: string; + collections?: CollectionOptions[]; + errorMessage?: string; + status?: 'loaded' | 'loading-failed' | 'loading'; +} + +class DataSource { + getOptions(): DataSourceOptions; +} +``` + +### getOption() + +获取数据源的配置信息。 + +- 类型 + +```tsx | pure +class DataSource { + getOption(key: string): any; +} +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSource = useDataSource(); + + const handleClick = async () => { + console.log(dataSource.getOption('key')); + }; +}; +``` diff --git a/packages/core/client/docs/en-US/core/data-source/demos/collection-field/demo1.tsx b/packages/core/client/docs/en-US/core/data-source/demos/collection-field/demo1.tsx new file mode 100644 index 000000000..385554a57 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/demos/collection-field/demo1.tsx @@ -0,0 +1,136 @@ +import { ISchema, observer, useForm } from '@formily/react'; +import { + Action, + Application, + CollectionProvider, + Form, + SchemaComponent, + FormItem, + InheritanceCollectionMixin, + Input, + SchemaComponentOptions, +} from '@nocobase/client'; +import React from 'react'; + +const collection = { + name: 'tests', + fields: [ + { + type: 'string', + name: 'title1', + interface: 'input', + uiSchema: { + title: 'Title1', + type: 'string', + 'x-component': 'Input', + required: true, + description: 'description1', + } as ISchema, + }, + { + type: 'string', + name: 'title2', + interface: 'input', + uiSchema: { + title: 'Title2', + type: 'string', + 'x-component': 'Input', + description: 'description', + default: 'ttt', + }, + }, + { + type: 'string', + name: 'title3', + }, + ], +}; + +const Root = observer(() => { + const schema: ISchema = { + type: 'object', + properties: { + form1: { + type: 'void', + 'x-component': 'Form', + properties: { + // 字段 title1 直接使用全局提供的 uiSchema + title1: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + default: '111', + }, + // 等同于 + // title1: { + // type: 'string', + // title: 'Title', + // required: true, + // 'x-component': 'Input', + // 'x-decorator': 'FormItem', + // }, + title2: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + title: 'Title4', // 覆盖全局已定义的 Title2 + required: true, // 扩展的配置参数 + description: 'description4', + }, + // 等同于 + // title2: { + // type: 'string', + // title: 'Title22', + // required: true, + // 'x-component': 'Input', + // 'x-decorator': 'FormItem', + // }, + // 字段 title3 没有提供 uiSchema,自行处理 + title3: { + 'x-component': 'Input', + 'x-decorator': 'FormItem', + title: 'Title3', + required: true, + }, + action1: { + // type: 'void', + 'x-component': 'Action', + title: 'Submit', + 'x-component-props': { + type: 'primary', + useAction: '{{ useSubmit }}', + }, + }, + }, + }, + }, + }; + + const useSubmit = () => { + const form = useForm(); + return { + async run() { + form.submit(() => { + console.log(form.values); + }); + }, + }; + }; + + return ( + + + + + + ); +}); + +const app = new Application({ + providers: [Root], + components: { Action, Form, Input, FormItem }, + dataSourceManager: { + collections: [collection], + collectionMixins: [InheritanceCollectionMixin], + }, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/data-source/demos/collection-field/demo2.tsx b/packages/core/client/docs/en-US/core/data-source/demos/collection-field/demo2.tsx new file mode 100644 index 000000000..31df89f79 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/demos/collection-field/demo2.tsx @@ -0,0 +1,89 @@ +import { ISchema, observer } from '@formily/react'; +import { + Application, + CollectionProvider, + Form, + FormItem, + InheritanceCollectionMixin, + Input, + SchemaComponent, + SchemaSettings, +} from '@nocobase/client'; +import React from 'react'; + +const collection = { + name: 'tests', + fields: [ + { + type: 'string', + name: 'title1', + interface: 'input', + uiSchema: { + title: 'Title1', + type: 'string', + 'x-component': 'Input', + required: true, + description: 'description1', + } as ISchema, + }, + ], +}; + +const Root = observer(() => { + const schema: ISchema = { + type: 'object', + properties: { + form1: { + type: 'void', + 'x-component': 'Form', + properties: { + title1: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-settings': 'FormItemSettings', + }, + }, + }, + }, + }; + + return ( + + + + ); +}); + +const formSettings = new SchemaSettings({ + name: 'FormItemSettings', + items: [ + { + name: 'required', + type: 'switch', + useComponentProps() { + // const collectionField = useCollectionField(); // 报错 + return { + checked: true, + // checked: !!collectionField?.uiSchema?.required, + title: 'Required', + onChange(v) { + // ? + }, + }; + }, + }, + ], +}); + +const app = new Application({ + providers: [Root], + components: { Form, Input, FormItem }, + dataSourceManager: { + collections: [collection], + collectionMixins: [InheritanceCollectionMixin], + }, + schemaSettings: [formSettings], + designable: true, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/data-source/demos/collection/demo1.tsx b/packages/core/client/docs/en-US/core/data-source/demos/collection/demo1.tsx new file mode 100644 index 000000000..a01748165 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/demos/collection/demo1.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { CollectionProvider, useCollection } from '@nocobase/client'; +import { createApp } from '../createApp'; + +const Demo = () => { + const collection = useCollection(); + return
{JSON.stringify(collection.getField('username'), null, 2)}
; +}; + +const Root = () => { + return ( + + + + ); +}; + +export default createApp(Root); diff --git a/packages/core/client/docs/en-US/core/data-source/demos/collections.json b/packages/core/client/docs/en-US/core/data-source/demos/collections.json new file mode 100644 index 000000000..4d3aa45f8 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/demos/collections.json @@ -0,0 +1,198 @@ +[ + { + "key": "h7b9i8khc3q", + "name": "users", + "inherit": false, + "hidden": false, + "description": null, + "category": [], + "namespace": "users.users", + "duplicator": { + "dumpable": "optional", + "with": "rolesUsers" + }, + "sortable": "sort", + "model": "UserModel", + "createdBy": true, + "updatedBy": true, + "logging": true, + "from": "db2cm", + "title": "{{t(\"Users\")}}", + "rawTitle": "{{t(\"Users\")}}", + "fields": [ + { + "uiSchema": { + "type": "number", + "title": "{{t(\"ID\")}}", + "x-component": "InputNumber", + "x-read-pretty": true, + "rawTitle": "{{t(\"ID\")}}" + }, + "key": "ffp1f2sula0", + "name": "id", + "type": "bigInt", + "interface": "id", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "autoIncrement": true, + "primaryKey": true, + "allowNull": false + }, + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Nickname\")}}", + "x-component": "Input", + "rawTitle": "{{t(\"Nickname\")}}" + }, + "key": "vrv7yjue90g", + "name": "nickname", + "type": "string", + "interface": "input", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null + }, + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Username\")}}", + "x-component": "Input", + "x-validator": { + "username": true + }, + "required": true, + "rawTitle": "{{t(\"Username\")}}" + }, + "key": "2ccs6evyrub", + "name": "username", + "type": "string", + "interface": "input", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "unique": true + }, + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Email\")}}", + "x-component": "Input", + "x-validator": "email", + "required": true, + "rawTitle": "{{t(\"Email\")}}" + }, + "key": "rrskwjl5wt1", + "name": "email", + "type": "string", + "interface": "email", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "unique": true + }, + { + "key": "t09bauwm0wb", + "name": "roles", + "type": "belongsToMany", + "interface": "m2m", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "target": "roles", + "foreignKey": "userId", + "otherKey": "roleName", + "onDelete": "CASCADE", + "sourceKey": "id", + "targetKey": "name", + "through": "rolesUsers", + "uiSchema": { + "type": "array", + "title": "{{t(\"Roles\")}}", + "x-component": "AssociationField", + "x-component-props": { + "multiple": true, + "fieldNames": { + "label": "title", + "value": "name" + } + } + } + } + ] + }, + { + "key": "pqnenvqrzxr", + "name": "roles", + "inherit": false, + "hidden": false, + "description": null, + "category": [], + "namespace": "acl.acl", + "duplicator": { + "dumpable": "required", + "with": "uiSchemas" + }, + "autoGenId": false, + "model": "RoleModel", + "filterTargetKey": "name", + "sortable": true, + "from": "db2cm", + "title": "{{t(\"Roles\")}}", + "rawTitle": "{{t(\"Roles\")}}", + "fields": [ + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Role UID\")}}", + "x-component": "Input", + "rawTitle": "{{t(\"Role UID\")}}" + }, + "key": "jbz9m80bxmp", + "name": "name", + "type": "uid", + "interface": "input", + "description": null, + "collectionName": "roles", + "parentKey": null, + "reverseKey": null, + "prefix": "r_", + "primaryKey": true + }, + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Role name\")}}", + "x-component": "Input", + "rawTitle": "{{t(\"Role name\")}}" + }, + "key": "faywtz4sf3u", + "name": "title", + "type": "string", + "interface": "input", + "description": null, + "collectionName": "roles", + "parentKey": null, + "reverseKey": null, + "unique": true, + "translation": true + }, + { + "key": "1enkovm9sye", + "name": "description", + "type": "string", + "interface": null, + "description": null, + "collectionName": "roles", + "parentKey": null, + "reverseKey": null + } + ] + } +] diff --git a/packages/core/client/docs/en-US/core/data-source/demos/createApp.tsx b/packages/core/client/docs/en-US/core/data-source/demos/createApp.tsx new file mode 100644 index 000000000..127f2e7c8 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/demos/createApp.tsx @@ -0,0 +1,98 @@ +import { + Application, + ApplicationOptions, + CardItem, + Plugin, + CollectionPlugin, + DataBlockProvider, + DEFAULT_DATA_SOURCE_KEY, + DEFAULT_DATA_SOURCE_TITLE, + LocalDataSource, +} from '@nocobase/client'; +import MockAdapter from 'axios-mock-adapter'; +import { ComponentType } from 'react'; +import collections from './collections.json'; + +const defaultMocks = { + 'users:list': { + data: [ + { + id: '1', + username: 'jack', + nickname: 'Jack Ma', + email: 'test@gmail.com', + }, + { + id: '2', + username: 'jim', + nickname: 'Jim Green', + }, + { + id: '3', + username: 'tom', + nickname: 'Tom Cat', + email: 'tom@gmail.com', + }, + ], + }, + 'roles:list': { + data: [ + { + name: 'root', + title: 'Root', + description: 'Root', + }, + { + name: 'admin', + title: 'Admin', + description: 'Admin description', + }, + ], + }, +}; + +export function createApp( + Demo: ComponentType, + options: ApplicationOptions = {}, + mocks: Record = defaultMocks, +) { + class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.addDataSource(LocalDataSource, { + key: DEFAULT_DATA_SOURCE_KEY, + displayName: DEFAULT_DATA_SOURCE_TITLE, + collections: collections as any, + }); + } + } + const app = new Application({ + apiClient: { + baseURL: 'http://localhost:8000', + }, + providers: [Demo], + ...options, + components: { + ...options.components, + DataBlockProvider, + CardItem, + }, + plugins: [CollectionPlugin, MyPlugin, ...(options.plugins || [])], + designable: true, + }); + + const mock = new MockAdapter(app.apiClient.axios); + + Object.entries(mocks).forEach(([url, data]) => { + mock.onGet(url).reply(async (config) => { + const res = typeof data === 'function' ? data(config) : data; + return [200, res]; + }); + mock.onPost(url).reply(async (config) => { + const res = typeof data === 'function' ? data(config) : data; + return [200, res]; + }); + }); + + const Root = app.getRootComponent(); + return Root; +} diff --git a/packages/core/client/docs/en-US/core/data-source/demos/data-source-manager/mixins.tsx b/packages/core/client/docs/en-US/core/data-source/demos/data-source-manager/mixins.tsx new file mode 100644 index 000000000..f2f26151f --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/demos/data-source-manager/mixins.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { CollectionProvider, Collection, useCollection, Plugin } from '@nocobase/client'; +import { createApp } from '../createApp'; + +class TestMixin extends Collection { + test() { + const { name } = this.options; + return 'test ' + name; + } +} + +class Test2Mixin extends Collection { + test2() { + const { name } = this.options; + return 'test2 ' + name; + } +} + +const Demo = () => { + const collection = useCollection(); + return ( +
+
test: {collection.test()}
+
test2: {collection.test2()}
+
fields.length: {collection.getFields().length}
+
+ ); +}; + +const Root = () => { + return ( + + + + ); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.addCollectionMixins([TestMixin, Test2Mixin]); + } +} + +export default createApp(Root, { + plugins: [MyPlugin], +}); diff --git a/packages/core/client/docs/en-US/core/data-source/extend-collections-provider.md b/packages/core/client/docs/en-US/core/data-source/extend-collections-provider.md new file mode 100644 index 000000000..8b033ff0f --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/extend-collections-provider.md @@ -0,0 +1,79 @@ +# ExtendCollectionsProvider + +用于扩展 [Collection](./collection.md) 。 + +## 组件 + +- 类型 + +```tsx | pure +interface ExtendCollectionsProviderProps { + collections: CollectionOptions[]; + children?: ReactNode; +} +``` + +- 示例 + +```tsx | pure +import { ExtendCollectionsProvider, CollectionOptions } from '@nocobase/client'; + +const userCollection: CollectionOptions = { + name: 'users', + title: '{{t("Users")}}', + fields: [ + { + type: 'string', + name: 'name', + title: '{{t("Name")}}', + }, + { + type: 'string', + name: 'email', + title: '{{t("Email")}}', + }, + ], +} + +const MyPlugin = () => { + return ( + + + + + + ); +} + +const ChildComponent = () => { + const collection = useCollection(); + + return ( +
+

{collection.name}

+
+ ); +} +``` + +## Hooks + +### useExtendCollections() + +获取扩展的数据表。 + +```tsx | pure +import { useExtendCollections } from '@nocobase/client'; + +const MyComponent = () => { + const collections = useExtendCollections(); + + return ( +
+ {collections.map((collection) => ( +
{collection.title}
+ ))} +
+ ); +} +``` diff --git a/packages/core/client/docs/en-US/core/data-source/images/collection-field-interface-form.png b/packages/core/client/docs/en-US/core/data-source/images/collection-field-interface-form.png new file mode 100644 index 000000000..9a6029e8a Binary files /dev/null and b/packages/core/client/docs/en-US/core/data-source/images/collection-field-interface-form.png differ diff --git a/packages/core/client/docs/en-US/core/data-source/images/collection-field-interface.png b/packages/core/client/docs/en-US/core/data-source/images/collection-field-interface.png new file mode 100644 index 000000000..6d04d0e2b Binary files /dev/null and b/packages/core/client/docs/en-US/core/data-source/images/collection-field-interface.png differ diff --git a/packages/core/client/docs/en-US/core/data-source/images/collection-template-form.png b/packages/core/client/docs/en-US/core/data-source/images/collection-template-form.png new file mode 100644 index 000000000..6223be103 Binary files /dev/null and b/packages/core/client/docs/en-US/core/data-source/images/collection-template-form.png differ diff --git a/packages/core/client/docs/en-US/core/data-source/images/collection-template.png b/packages/core/client/docs/en-US/core/data-source/images/collection-template.png new file mode 100644 index 000000000..45a9de527 Binary files /dev/null and b/packages/core/client/docs/en-US/core/data-source/images/collection-template.png differ diff --git a/packages/core/client/docs/en-US/core/data-source/images/field-groups.png b/packages/core/client/docs/en-US/core/data-source/images/field-groups.png new file mode 100644 index 000000000..3f9381a70 Binary files /dev/null and b/packages/core/client/docs/en-US/core/data-source/images/field-groups.png differ diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-basic.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-basic.tsx index 40a0c5a29..e285d6ccd 100644 --- a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-basic.tsx +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-basic.tsx @@ -54,11 +54,14 @@ const myInitializer = new SchemaInitializer({ ], }); -const AddBlockButton = observer(() => { - const fieldSchema = useFieldSchema(); - const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); - return render(); -}); +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); const Page = observer( (props) => { diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-component.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-component.tsx index 642705068..4fe9c9f7b 100644 --- a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-component.tsx +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-component.tsx @@ -62,11 +62,14 @@ const myInitializer = new SchemaInitializer({ ], }); -const AddBlockButton = observer(() => { - const fieldSchema = useFieldSchema(); - const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); - return render(); -}); +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); const Page = observer( (props) => { diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-select.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-select.tsx index b635052e3..69590dab4 100644 --- a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-select.tsx +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-select.tsx @@ -1,10 +1,10 @@ /** * defaultShowCode: true */ -import { useFieldSchema } from '@formily/react'; -import { Application, Grid, SchemaInitializer, SchemaInitializerSelect, useDesignable } from '@nocobase/client'; +import { Grid, SchemaInitializer, Application, SchemaInitializerSelect, useDesignable } from '@nocobase/client'; import React from 'react'; import { appOptions } from './schema-initializer-common'; +import { useFieldSchema } from '@formily/react'; const OpenModeSelect = () => { const fieldSchema = useFieldSchema(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-items.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-items.tsx index 9c7ae48c3..33ca587a9 100644 --- a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-items.tsx +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-items.tsx @@ -75,11 +75,14 @@ const myInitializer = new SchemaInitializer({ ], }); -const AddBlockButton = observer(() => { - const fieldSchema = useFieldSchema(); - const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); - return render(); -}); +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); const Page = observer( (props) => { diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-popover.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-popover.tsx index a5890ba9f..3db26240b 100644 --- a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-popover.tsx +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-popover.tsx @@ -43,11 +43,14 @@ const myInitializer = new SchemaInitializer({ Component: MyInitializerComponent, }); -const AddBlockButton = observer(() => { - const fieldSchema = useFieldSchema(); - const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); - return render(); -}); +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); const Page = observer( (props) => { diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-options-item-children.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-options-item-children.tsx index 45fba8c3b..102c312b2 100644 --- a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-options-item-children.tsx +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-options-item-children.tsx @@ -34,7 +34,7 @@ const mySettings = new SchemaSettings({ ], }, { - name: 'a', + name: 'b', type: 'itemGroup', componentProps: { title: '动态 children', @@ -44,7 +44,9 @@ const mySettings = new SchemaSettings({ { name: 'a1', type: 'item', - title: 'A 1', + componentProps: { + title: 'A 1', + }, onClick: () => { alert('a-1'); }, @@ -52,7 +54,9 @@ const mySettings = new SchemaSettings({ { name: 'a2', type: 'item', - title: 'A 2', + componentProps: { + title: 'A 2', + }, }, ]; }, diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-render.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-render.tsx index cb179a78a..089b17cd2 100644 --- a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-render.tsx +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-render.tsx @@ -28,7 +28,6 @@ class PluginHello extends Plugin { export const SchemaSettingsBlockTitleItem = function BlockTitleItem() { const { dn } = useSchemaSettings(); - console.log(dn.current); return ( { - const record = new Record({ current: item }); - return + const record = new CollectionRecord({ current: item }); + return })} ``` a:get(view、edit) ```jsx | pure -const record = new Record({ current: item }); - +const record = new CollectionRecord({ current: item }); + ``` a:create ```jsx | pure -const record = new Record({ current, isNew: true }); - +const record = new CollectionRecord({ current, isNew: true }); + ``` a.b:list ```jsx | pure - + {list.map((item) => { - const recordB = new Record({ + const recordB = new CollectionRecord({ current: item, }); - return + return })} - + ``` a.b:get ```jsx | pure -const recordA = new Record({ +const recordA = new CollectionRecord({ current: itemA, }); -const recordB = new Record({ +const recordB = new CollectionRecord({ current: itemB, parent: recordA, }); // 或者 recordB.setParent(recordA); - - - + + + ``` a.b:create ```jsx | pure -const recordA = new Record({ +const recordA = new CollectionRecord({ current: itemA, }); -const recordB = new Record({ +const recordB = new CollectionRecord({ isNew: true, parent: recordA, }); - - - + + + ``` ## 区块 @@ -178,9 +178,9 @@ DataBlockProvider 内容 - + {props.children} - + @@ -189,7 +189,7 @@ DataBlockProvider 内容 有当前父级记录的区块,如对多关系的表格区块,区块本身不套 RecordPicker ```tsx | pure - + @@ -208,7 +208,7 @@ DataBlockProvider 内容
- + ``` DataBlockProvider 内容 @@ -228,14 +228,14 @@ DataBlockProvider 内容 有父级记录也有当前记录的区块,如关系的表单 ```tsx | pure - +
-
+ ``` DataBlockProvider 内容 @@ -245,9 +245,9 @@ DataBlockProvider 内容 - + {props.children} - + diff --git a/packages/core/client/docs/zh-CN/core/application/application.md b/packages/core/client/docs/zh-CN/core/application/application.md index 77d44d288..b33fe9bbf 100644 --- a/packages/core/client/docs/zh-CN/core/application/application.md +++ b/packages/core/client/docs/zh-CN/core/application/application.md @@ -8,7 +8,7 @@ ```tsx | pure export interface ApplicationOptions { - apiClient?: APIClientOptions; + apiClient?: APIClientOptions | APIClient; ws?: WebSocketClientOptions | boolean; i18n?: i18next; providers?: (ComponentType | ComponentAndProps)[]; @@ -19,6 +19,7 @@ export interface ApplicationOptions { schemaSettings?: SchemaSetting[]; schemaInitializers?: SchemaInitializer[]; loadRemotePlugins?: boolean; + dataSourceManager?: DataSourceManagerOptions; } ``` @@ -33,7 +34,7 @@ export interface ApplicationOptions { - schemaSettings:Schema 设置工具,具体参考:[SchemaSettingsManager](/core/ui-schema/schema-initializer-manager) - schemaInitializers:Schema 添加工具,具体参考:[SchemaInitializerManager](/core/ui-schema/schema-initializer-manager) - loadRemotePlugins:用于控制是否加载远程插件,默认为 `false`,即不加载远程插件(方便单测和 DEMO 环境)。 - + - dataSourceManager:数据源管理器,具体参考:[DataSourceManager](/core/data-source/data-source-manager) - 示例 ```tsx @@ -108,6 +109,10 @@ class Application { 详细介绍,请参考:[SchemaInitializerManager](/core/ui-schema/schema-initializer-manager) +### app.dataSourceManager + +详细介绍,请参考:[dataSourceManager](/core/data-source/data-source-manager) + ## 实例方法 ### app.getRootComponent() diff --git a/packages/core/client/docs/zh-CN/core/application/plugin.md b/packages/core/client/docs/zh-CN/core/application/plugin.md index 839bfb964..5bfcfe42c 100644 --- a/packages/core/client/docs/zh-CN/core/application/plugin.md +++ b/packages/core/client/docs/zh-CN/core/application/plugin.md @@ -34,6 +34,10 @@ class Plugin { return this.app.schemaSettingsManager; } + get dataSourceManager() { + return this.app.dataSourceManager; + } + async afterAdd() {} async beforeLoad() {} diff --git a/packages/core/client/docs/zh-CN/core/collection-manager/index.md b/packages/core/client/docs/zh-CN/core/collection-manager/index.md deleted file mode 100644 index 90eef3dee..000000000 --- a/packages/core/client/docs/zh-CN/core/collection-manager/index.md +++ /dev/null @@ -1,25 +0,0 @@ -# CollectionManager - -```ts -class CollectionManager { - addFieldInterfaces(interfaces) {} - addCollectionTemplates(templates) {} - addCollections(ns, collections) {} - getCollection() {} -} - -cm.setDefaultNamespace('default'); - -cm.addFieldInterfaces({ - select: {}, -}); - -cm.addCollectionTemplates({ - calendar: {}, -}); - -cm.getCollection(); - - -const cm = new CollectionManager(); -``` diff --git a/packages/core/client/docs/zh-CN/core/data-block/collection-record-provider.md b/packages/core/client/docs/zh-CN/core/data-block/collection-record-provider.md new file mode 100644 index 000000000..0cfad105f --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/collection-record-provider.md @@ -0,0 +1,259 @@ +# CollectionRecordProvider + +用于提供 [CollectionRecord](./collection-record) 实例。 + +## 组件 + +- 类型 + +```tsx | pure +interface CollectionRecordProviderProps { + isNew?: boolean; + record?: CollectionRecord | DataType; + parentRecord?: CollectionRecord | DataType; + /** + * 当前记录所属的 collection name + */ + collectionName?: string; +} +``` + +- 详解 + +参数的具体说明参见 [CollectionRecord](./collection-record)。 + +需要说明的是 `record` 和 `parentRecord` 即可以是普通的对象,也可以是 [CollectionRecord](./collection-record) 实例,但最终会转为 `CollectionRecord` 实例,并通过 context 传递给子组件。 + +## 示例 + +- record 参数为 CollectionRecord 实例 + +```tsx | pure +import { CollectionRecord, CollectionRecordProvider } from '@nocobase/client'; + +const record = new CollectionRecord({ data: { id: 1, name: 'foo' } }); + + +// 最终向子组件传递的数据为: props.record +``` + +```tsx +import { CollectionRecord, useCollectionRecord, CollectionRecordProvider } from '@nocobase/client'; + +const record = new CollectionRecord({ data: { id: 1, name: 'foo' } }); + +const Demo = () => { + const record = useCollectionRecord(); + return
{JSON.stringify(record, null, 2)}
; +} + +export default () => +``` + +- record 参数为普通对象 + +```tsx | pure + +// 最终向子组件传递的数据为: const record = new CollectionRecord({ data: { id: 1, name: 'foo' } }); +``` + +```tsx +import { useCollectionRecord, CollectionRecordProvider } from '@nocobase/client'; + +const Demo = () => { + const record = useCollectionRecord(); + return
{JSON.stringify(record, null, 2)}
; +} + +export default () => +``` + +- record 参数为 CollectionRecord 实例且带有父记录 + +```tsx | pure +const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } }); +const record = new CollectionRecord({ data: { id: 1, name: 'foo' }, parentRecord }); + + +// 最终向子组件传递的数据为: props.record +``` + +```tsx +import { CollectionRecord, useCollectionRecord, CollectionRecordProvider } from '@nocobase/client'; + +const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } }); +const record = new CollectionRecord({ data: { id: 1, name: 'foo' }, parentRecord }); + +const Demo = () => { + const record = useCollectionRecord(); + return
{JSON.stringify(record, null, 2)}
; +} + +export default () => +``` + +- record 参数为 CollectionRecord 实例,父记录通过 `parentRecord` 参数传递 + +```tsx | pure +const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } }); +const record = new CollectionRecord({ data: { id: 1, name: 'foo' } }); + + + +// 首先设置父记录:record.setParentRecord(parentRecord); +// 最终向子组件传递的数据为:record(带有父记录) +``` + +```tsx +import { CollectionRecord, useCollectionRecord, CollectionRecordProvider } from '@nocobase/client'; + +const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } }); +const record = new CollectionRecord({ data: { id: 1, name: 'foo' } }); + +const Demo = () => { + const record = useCollectionRecord(); + return
{JSON.stringify(record, null, 2)}
; +} + +export default () => +``` + +- record 参数为普通对象,父记录也是普通对象 + +```tsx | pure + + +// 首先实例化父记录:const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } }); +// 然后实例化记录:const record = new CollectionRecord({ data: { id: 1, name: 'foo' } }); +// 最后设置父记录:record.setParentRecord(parentRecord); +// 最终向子组件传递的数据为:record(带有父记录) +``` + + +```tsx +import { useCollectionRecord, CollectionRecordProvider } from '@nocobase/client'; + +const Demo = () => { + const record = useCollectionRecord(); + return
{JSON.stringify(record, null, 2)}
; +} + +export default () => +``` + +## Hooks + +### useCollectionRecord() + +用于获取 `CollectionRecordProvider` 组件传递的数据记录。 + +- 示例 + +```tsx | pure +const record = useCollectionRecord(); + +console.log(record, record.data, record.parentRecord, record.parentRecord.data); +``` + +```tsx +import { useCollectionRecord, CollectionRecordProvider } from '@nocobase/client'; + +const Demo = () => { + const record = useCollectionRecord(); + return
+
record:
{JSON.stringify(record, null, 2)}
+
record.data:
{JSON.stringify(record.data, null, 2)}
+
record.parentRecord:
{JSON.stringify(record.parentRecord, null, 2)}
+
record.parentRecord.data:
{JSON.stringify(record.parentRecord.data, null, 2)}
+
; +} + +export default () => +``` + +### useCollectionRecordData() + +直接获取 CollectionRecord 的 `data` 属性,等同于 `useCollectionRecord().data`。 + +- 示例 + +```tsx | pure +const data = useCollectionRecordData(); +const record = useCollectionRecord(); +console.log(data === record.data); +``` + +```tsx +import { useCollectionRecord, useCollectionRecordData, CollectionRecordProvider } from '@nocobase/client'; + +const Demo = () => { + const data = useCollectionRecordData(); + const record = useCollectionRecord(); + return
+
data === record.data: { JSON.stringify(data === record.data) }
+
data:
{JSON.stringify(data, null, 2)}
+
record.data:
{JSON.stringify(record.data, null, 2)}
+
; +} + +export default () => +``` + +### useCollectionParentRecord() + +直接获取 CollectionRecord 的数据 `parentRecord`,等同于 `useCollectionRecord().parentRecord`。 + +- 示例 + +```tsx | pure +const parentRecord = useCollectionParentRecord(); +const record = useCollectionRecord(); +console.log(parentRecord === record.parentRecord); +``` + +```tsx +import { useCollectionRecord, useCollectionParentRecord, CollectionRecordProvider } from '@nocobase/client'; + +const Demo = () => { + const record = useCollectionRecord(); + const parentRecord = useCollectionParentRecord(); + return
+
parentRecord === record.parentRecord: { JSON.stringify(parentRecord === record.parentRecord) }
+
parentRecord:
{JSON.stringify(parentRecord, null, 2)}
+
record.parentRecord:
{JSON.stringify(record.parentRecord, null, 2)}
+
; +} + +export default () => +``` + + +### useCollectionParentRecordData() + +直接获取 CollectionRecord 的数据 `parentRecord.data`,等同于 `useCollectionRecord().parentRecord.data`。 + +- 示例 + +```tsx | pure +const record = useCollectionRecord(); +const parentData = useCollectionParentRecordData(); +const parentRecord = useCollectionParentRecord(); +console.log(parentData === parentRecord.data === record.parentRecord.data); +``` + +```tsx +import { useCollectionRecord, CollectionRecordProvider, useCollectionParentRecordData, useCollectionParentRecord } from '@nocobase/client'; + +const Demo = () => { + const record = useCollectionRecord(); + const parentData = useCollectionParentRecordData(); + const parentRecord = useCollectionParentRecord(); + return
+
parentData:
{JSON.stringify(parentData, null, 2)}
+
parentRecord.data:
{JSON.stringify(parentRecord.data, null, 2)}
+
record.parentRecord.data:
{JSON.stringify(record.parentRecord.data, null, 2)}
+
; +} + +export default () => +``` diff --git a/packages/core/client/docs/zh-CN/core/data-block/collection-record.md b/packages/core/client/docs/zh-CN/core/data-block/collection-record.md new file mode 100644 index 000000000..d2bc6ffa4 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/collection-record.md @@ -0,0 +1,194 @@ +# CollectionRecord + +数据表中的一条记录。 + +## 类型 + +```tsx | pure +interface CollectionRecordOptions { + isNew?: boolean; + data?: DataType; + parentRecord?: CollectionRecord; + /** + * 当前记录所属的 collection name + */ + collectionName?: string; +} + +class CollectionRecord { + public isNew?: boolean; + public data?: DataType; + public parentRecord?: CollectionRecord; + public collectionName?: string; + constructor(options: CollectionRecordOptions) {} + + setData(data: DataType) { + this.data = data; + } + + setParentRecord(parentRecord: CollectionRecord) { + this.parentRecord = parentRecord; + } +} +``` + +## 详解 + +### CollectionRecord 基本概念 + +CollectionRecord 类用于提供数据记录,通常情况下对应着后端数据表中的一条记录。以用户表为例,其一条数据对应的 CollectionRecord 类如下: + +```tsx | pure +const useCollectionRecord = new CollectionRecord({ + data: { + "id": 1, + "roleId": 10, + "appLang": null, + "createdById": null, + "email": "test@nocobase.com", + "nickname": "Admin", + "phone": null, + "systemSettings": {}, + "updatedById": null, + "username": "nocobase", + "createdAt": "2023-12-04T09:42:52.953Z", + "updatedAt": "2023-12-04T09:42:52.953Z", + } +}); +``` + +### CollectionRecord 和 Collection 的关系 + +CollectionRecord 是指的数据,而 Collection 则是表结构。对于上面的用户表,其对应的 Collection 如下: + +```tsx | pure +const usersCollection = new Collection({ + name: 'users', + fields: [ + { + type: 'bigInt', + name: 'id', + }, + { + type: 'string', + name: 'username', + }, + { + type: 'integer', + name: 'age', + }, + { + "name": "email", + "type": "string", + }, + // .... + ], +}); +``` + +### 父子关系和关系字段 + +对于[关系字段](https://docs.nocobase.com/development/server/collections/association-fields),例如用户和角色的关系,在用户表中会有一个 `roleId` 字段,其值为角色表中的 `id`,当我们通过 `users.roleId` 字段查询用户的角色时: + +```bash | pure +GET /api/users/1/roles:get/10 +``` + +其中 `1` 为用户的 `id`,`10` 为角色的 `id`,我们可以得到用户的角色数据: + +```tsx | pure +const roleRecord = new CollectionRecord({ + data: { + "id": 10, + "name": "member", + "title": "test role", + "strategy": { + "actions": [ + "view", + "update:own", + "destroy:own", + "create" + ] + }, + "createdAt": "2023-03-30T07:53:10.924Z", + "updatedAt": "2023-12-15T02:51:43.577Z", + } +}) +``` + +其中 `users` id 为 1 的记录我们称之为父记录: + +```tsx | pure +roleRecord.setParentRecord(userRecord); +``` + +### 新记录 + +对于新表单,我们可以通过 `isNew` 属性来标识: + +```tsx | pure +const record = new CollectionRecord({ + isNew: true, +}); +``` + +## 示例 + +### 基本使用 + +```tsx | pure +import { CollectionRecord } from '@nocobase/client'; + +const record = new CollectionRecord({ + data: { + name: 'foo', + } +}); +``` + +### 创建空记录 + +```tsx | pure +import { CollectionRecord } from '@nocobase/client'; + +const record = new CollectionRecord({ + isNew: true, +}); +``` + +### 设置 parentRecord + +方式1: 通过构造函数设置 + +```tsx | pure +const parentRecord = new CollectionRecord({ + data: { + foo: 'foo', + } +}); + +const record = new CollectionRecord({ + data: { + name: 'bar', + }, + parentRecord, +}); +``` + +方式2: 通过 `setParentRecord` 方法设置 + +```tsx | pure +const parentRecord = new CollectionRecord({ + data: { + foo: 'foo', + } +}); + +const record = new CollectionRecord({ + data: { + name: 'bar', + } +}); + +record.setParentRecord(parentRecord); +``` diff --git a/packages/core/client/docs/zh-CN/core/data-block/data-block-provider.md b/packages/core/client/docs/zh-CN/core/data-block/data-block-provider.md new file mode 100644 index 000000000..6aeaec049 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/data-block-provider.md @@ -0,0 +1,246 @@ +# DataBlockProvider + +## 区块类型 + +区块分为简单区块和包含各种数据的区块。 + +### 简单区块 + +简单区块例如 Markdown 区块。 + +![](./images/markdown-block.png) + +它只有文本内容,没有其他更多复杂数据,且文本内容是存储在 `schema` 中的,没有存储在数据库中。 + +```json {5}| pure +{ + "type": "void", + "x-component": "Markdown.Void", + "x-component-props": { + "content": "markdown content" + }, +} +``` + +### 数据区块 + +数据区块是指区块的数据存储在服务端的数据表中,例如 Table 组件。 + +![](./images/data-block.png) + +Table 中的字段信息及列表数据,都是存储在数据库中的。 + + +## DataBlockProvider 介绍 + +为了方便对数据区块的数据进行管理,我们提供了 `DataBlockProvider` 组件,其内部封装了: + +- `DataBlockProvider`:封装了下面的所有组件,并提供了区块属性 + - [CollectionProvider](/core/data-source/collection-provider) / [AssociationProvider](/core/data-source/association-provider): 根据 `DataBlockProvider` 提供的上下文信息,查询对应数据表数据及关系字段信息并传递 + - [BlockResourceProvider](/core/data-block/data-block-resource-provider): 根据 `DataBlockProvider` 提供的上下文信息,构建区块 [Resource](https://docs.nocobase.com/api/sdk#resource-action) API,用于区块数据的增删改查 + - [BlockRequestProvider](/core/data-block/data-block-request-provider): 根据 `DataBlockProvider` 提供的上下文信息,自动调用 `BlockResourceProvider` 提供的 `resource.get()` 或 `resource.list()` 发起请求,得到区块数据,并传递 + - [CollectionRecordProvider](/core/data-source/record-provider): 对于 `resource.get()` 场景,会自动嵌套 `CollectionRecordProvider` 并将 `resource.get()` 请求结果传递下去,`resource.list()` 场景则需要自行使用 `CollectionRecordProvider` 提供数据记录 + +```tsx | pure +const DataBlockProvider = (props) => { + return + + / + + + {action !== 'list' && + {props.children} + } + + / + + + +} +``` + +上述组件封装到 `DataBlockProvider` 的内部,只需要使用 `DataBlockProvider` 即可自行得到上述数据。 + +### 使用方式 + +其主要使用在区块的 schema [x-decorator](https://docs.nocobase.com/development/client/ui-schema/what-is-ui-schema#x-decorator) 中,例如: + +```js {5}| pure +{ + type: 'void', + name: 'hello-block', + 'x-component': 'CardItem', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + action: 'list', + tableProps: { + bordered: true, + } + }, + 'x-use-decorator-props': 'useDynamicDataBlockProps', +} +``` + +### 完整示例 + + + +## 属性 + +### 静态属性和动态属性 + +- schema 中的 `x-decorator-props` 称为静态属性,它是一个普通对象,记录区块的配置信息 +- schema 中的 `x-use-decorator-props` 中的属性称为动态属性,它是一个 React hook,可用于获取例如 URL 上的 ID,或者父级的 context 数据 + +当两者都存在时,会进行深度合并,作为 `DataBlockProvider` 的属性。 + +### 属性详解 + +```ts | pure +interface AllDataBlockProps { + collection?: string; + association?: string; + sourceId?: string | number; + record?: Record; + action?: 'list' | 'get'; + filterByTk?: string; + params?: Record; + parentRecord?: Record; + [index: string]: any; +} +``` + +- collection(`x-decorator-props`):区块的 collection 表名,用于获取区块的字段信息和区块数据 +- association(`x-decorator-props`):区块的关系字段名,用于获取区块的关系字段信息和关系字段数据 +- dataSource(`x-decorator-props`): 数据源,具体可参考 [Data Modeling](https://docs.nocobase.com/manual/data-modeling) +- action(`x-decorator-props`):区块的请求类型,`list` 或 `get` +- params(`x-decorator-props` 和 `x-use-decorator-props`):区块的请求参数,同时存在于 +- filterByTk(`x-use-decorator-props`):相当于 `params.filterByTk`,可理解为 `id`,用于获取单条数据 +- sourceId(`x-use-decorator-props`):区块的 sourceId,配合 `association` 使用,用于获取区块的关系字段数据 +- record(`x-use-decorator-props`):当提供 `record` 时,会使用 `record` 作为区块的数据,不发起请求 +- parentRecord(`x-use-decorator-props`):当提供 `parentRecord` 时,会使用 `parentRecord` 作为关系字段的表数据,不发起请求 + +```tsx | pure +const DataBlockProvider = (props) => { + return + + / + + + {action !== 'list' && + {props.children} + } + + + + + +} +``` + +### 属性组合和场景 + +这些属性根据不同的场景,共有 8 中情况: + +- collection + - 创建:`collection` + - 获取单条数据:`collection` + `action: get` + `params` + - 获取列表数据:`collection` + `action: list` + `params` + - 使用 `record` 作为数据:`collection` + `record` + +对于 *获取单条数据* 和 *获取列表数据* `params` 非必须。 + +- association + - 创建:`association` + `sourceId` + - 获取单条数据:`association` + `sourceId` + `action: get` + `params` + `parentRecord` + - 获取列表数据:`association` + `sourceId` + `action: list` + `params` + `parentRecord` + - 使用 `record` 作为数据:`association` + `sourceId` + `record` + `parentRecord` + + +对于 *获取单条数据* 和 *获取列表数据* `params` 和 `parentRecord` 非必须,当没有 `parentRecord` 会根据 `association` 查询到对应的 `collection`,然后再根据 `collection` 查询到对应的 `parentRecord`。 + +### 属性获取和修改 + +#### useDataBlock() + +可用于获取和修改 `DataBlockProvider` 的属性。 + +- 类型 + +```tsx | pure +interface Result { + props: AllDataBlockProps & T; + dn: Designable; +} +const useDataBlock: () => Result +``` + +- 详解 + +`props` 就对应着上面的 `AllDataBlockProps`。 +`dn` 是 `Designable` 对象,可用于修改 `DataBlockProvider` 的 UI schema,详细见 [Designable](/core/ui-schema/designable)。 + +- 示例 + +```tsx | pure +const { props, dn } = useDataBlock<{ tableProps: { bordered?: boolean } }>(); + +// 获取 +const checked = props.tableProps.bordered; + +// 修改 +dn.deepMerge({ + 'x-decorator-props': { + tableProps: { + bordered: !checked, + }, + }, +}); +``` + +#### useDataBlockProps() + +相当于 `useDataBlock().props`。 + +```tsx | pure +const props = useDataBlockProps<{ tableProps: { bordered?: boolean } }>(); + +const checked = props.tableProps.bordered; +``` + +## 示例 + +### collection + +#### Table list + + + +#### Form get & update + + + +#### Form create + + + +#### Form record & update + + + +### association + +association 与 collection 类似,只是需要提供 `sourceId`,我们以 `Table list` 为例。 + +#### Table list & sourceId + + + +#### Table list & parentRecord + +如果不提供 `sourceId`,则需要提供 `parentRecord`,我们以 `Table list` 为例。 + + + diff --git a/packages/core/client/docs/zh-CN/core/data-block/data-block-request-provider.md b/packages/core/client/docs/zh-CN/core/data-block/data-block-request-provider.md new file mode 100644 index 000000000..48b5801b3 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/data-block-request-provider.md @@ -0,0 +1,139 @@ +# DataBlockRequestProvider + +其内部获取到 [BlockResourceProvider](/core/data-block/data-block-resource-provider) 提供的 `resource`,根据 [BlockProvider](/core/data-block/data-block-provider) 提供的参数,自动调用 `resource.get()` 或者 `resource.list()` 获取的区块的数据,并通过 context 传递下去。 + +## 请求参数 + +请求参数是获取 `DataBlockProvider` 提供中的 `params` 和 `filterByTk`。 + +```ts | pure +const schema = { + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + 'collection': 'users', + 'action': 'list', + // 静态参数 + params: { + pageSize: 10, + } + }, + // 动态参数 + 'x-use-decorator-props': 'useDynamicDataBlockProps', +} + +const useDynamicDataBlockProps: UseDataBlockProps<'CollectionList'> = () => { + return { + params: { + size: 15, + } + } +} +``` + +会自动调用 `resource.list()` 获取数据,发起 `GET /api/users:list?pageSize=10&size=15` 的请求。 + +## Hooks + +### useDataBlockRequest() + +用于获取请求对象,一般用区块组件中。 + +```tsx | pure +const MyTable = () => { + const { data, loading } = useDataBlockRequest(); + + return ( +
+ ) +} +``` + +## Record + +### Get 请求 + +对于 `get` 请求,当获取到 `data` 数据后,会通过 `CollectionRecordProvider` 提供 `record` 对象,用于获取当前区块的数据。 + +```ts | pure +const schema = { + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + 'collection': 'users', + 'action': 'get', // get 请求 + }, + // 动态参数 + 'x-use-decorator-props': 'useDynamicFormProps', +} + +const useDynamicDataBlockProps: UseDataBlockProps<'CollectionGet'> = () => { + return { + params: { + filterByTk: 1, + } + } +} +``` + +会自动调用 `resource.get()` 获取数据,发起 `GET /api/users:get/1` 的请求,并通过 `CollectionRecordProvider` 提供上下文。 + +```tsx | pure +const { data } = useDataBlockRequest(); +const record = useCollectionRecord(); // record 上下文数据 + +// 相等 +record.data === data; +``` + +### List 请求 + +对于 `list` 请求则不会提供 `record` 对象,需要自己通过 `` 设置上下文。 + +```tsx | pure +const MyTable = () => { + const { data } = useDataBlockRequest(); + + return ( +
{ + return ( + + + + ) + }, + }, + ]} + pagination={{ + total: data?.meta.total, + pageSize: data?.meta.pageSize, + page: data?.meta.page, + }} + /> + ) +} + +const MyAction = () => { + const record = useCollectionRecord(); + return ( + + ) +} +``` diff --git a/packages/core/client/docs/zh-CN/core/data-block/data-block-resource-provider.md b/packages/core/client/docs/zh-CN/core/data-block/data-block-resource-provider.md new file mode 100644 index 000000000..e94246b6c --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/data-block-resource-provider.md @@ -0,0 +1,36 @@ +# DataBlockResourceProvider + +根据 `DataBlockProvider` 中的 `collection`、`association`、`sourceId` 等属性,构建好 [resource](https://docs.nocobase.com/api/sdk#resource-action) 对象,方便子组件对区块数据的增删改查操作,其内置在 [DataBlockProvider](/core/data-block/data-block-provider) 中 + + +## useDataBlockResource + +用于获取当前数据块的 resource 对象。 + +- 类型 + +```ts | pure +function useDataBlockResource(): IResource +``` + +- 示例 + +```ts | pure +const resource = useDataBlockResource(); + +const onSubmit = async (values) => { + // 创建 + await resource.create({ values }); +} +``` + +```ts | pure +const resource = useDataBlockResource(); + +const onDelete = async () => { + // 删除 + await resource.destroy(); +} +``` + +可运行示例 diff --git a/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/association-table-list-and-parent-record.tsx b/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/association-table-list-and-parent-record.tsx new file mode 100644 index 000000000..27af6235f --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/association-table-list-and-parent-record.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { Table, TableProps } from 'antd'; +import { SchemaComponent, UseDataBlockProps, useDataBlockRequest, withDynamicSchemaProps } from '@nocobase/client'; +import { ISchema } from '@formily/json-schema'; + +import { createApp } from '../../../data-source/demos/createApp'; + +const collection = 'users'; +const associationField = 'roles'; + +const association = `${collection}.${associationField}`; +const action = 'list'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-use-decorator-props': 'useBlockDecoratorProps', + 'x-decorator-props': { + association, + action, + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + }, + }, +}; + +const MyTable = withDynamicSchemaProps(Table); + +function useTableProps(): TableProps { + const { data, loading } = useDataBlockRequest(); + return { + loading, + dataSource: data?.data || [], + columns: [ + { + title: 'Name', + dataIndex: 'name', + }, + { + title: 'Title', + dataIndex: 'title', + }, + { + title: 'Description', + dataIndex: 'description', + }, + ], + }; +} + +const useBlockDecoratorProps: UseDataBlockProps<'CollectionList'> = () => { + const parentRecord = { + id: 1, + username: 'Tom', + }; + return { + parentRecord, + }; +}; + +const Demo = () => { + return ; +}; + +const mocks = { + [`${collection}/1/${associationField}:${action}`]: { + data: [ + { + name: 'admin', + title: 'Admin', + description: 'Admin description', + }, + { + name: 'developer', + title: 'Developer', + description: 'Developer description', + }, + ], + }, +}; + +const Root = createApp( + Demo, + { + components: { MyTable }, + scopes: { useTableProps, useBlockDecoratorProps }, + }, + mocks, +); + +export default Root; diff --git a/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/association-table-list-and-source-id.tsx b/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/association-table-list-and-source-id.tsx new file mode 100644 index 000000000..a7594c7b5 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/association-table-list-and-source-id.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { Select, Table, TableProps } from 'antd'; +import { SchemaComponent, UseDataBlockProps, useDataBlockRequest, withDynamicSchemaProps } from '@nocobase/client'; +import { ISchema } from '@formily/json-schema'; + +import { createApp } from '../../../data-source/demos/createApp'; +import useUrlState from '@ahooksjs/use-url-state'; + +const collection = 'users'; +const associationField = 'roles'; + +const association = `${collection}.${associationField}`; +const action = 'list'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-use-decorator-props': 'useBlockDecoratorProps', + 'x-decorator-props': { + association, + action, + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + }, + }, +}; + +const MyTable = withDynamicSchemaProps(Table); + +function useTableProps(): TableProps { + const { data, loading } = useDataBlockRequest(); + return { + loading, + dataSource: data?.data || [], + columns: [ + { + title: 'Name', + dataIndex: 'name', + }, + { + title: 'Title', + dataIndex: 'title', + }, + { + title: 'Description', + dataIndex: 'description', + }, + ], + }; +} + +const useBlockDecoratorProps: UseDataBlockProps<'CollectionList'> = () => { + const [state] = useUrlState({ userId: '1' }); + return { + sourceId: state.userId, + }; +}; + +const Demo = () => { + const [state, setState] = useUrlState({ userId: '1' }); + return ( + <> + + + + ); +}; + +const mocks = { + [`${collection}/1/${associationField}:${action}`]: { + data: [ + { + name: 'admin', + title: 'Admin', + description: 'Admin description', + }, + { + name: 'developer', + title: 'Developer', + description: 'Developer description', + }, + ], + }, + [`${collection}/2/${associationField}:${action}`]: { + data: [ + { + name: 'developer', + title: 'Developer', + description: 'Developer description', + }, + { + name: 'tester', + title: 'Tester', + description: 'Tester description', + }, + ], + }, + [`${collection}:get/1`]: { + id: 1, + username: 'Tom', + }, + [`${collection}:get/2`]: { + id: 1, + username: 'Jack', + }, +}; + +const Root = createApp( + Demo, + { + components: { MyTable }, + scopes: { useTableProps, useBlockDecoratorProps }, + }, + mocks, +); + +export default Root; diff --git a/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/collection-form-create.tsx b/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/collection-form-create.tsx new file mode 100644 index 000000000..078c7dd7a --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/collection-form-create.tsx @@ -0,0 +1,95 @@ +import React, { FC } from 'react'; +import { Button, Form, FormProps, Input, InputNumber, notification } from 'antd'; +import { SchemaComponent, useDataBlockResource, withDynamicSchemaProps } from '@nocobase/client'; +import { ISchema } from '@formily/json-schema'; + +import { createApp } from '../../../data-source/demos/createApp'; + +interface DemoFormFieldType { + id: number; + username: string; + age: number; +} +type DemoFormProps = FormProps; +const DemoForm: FC = withDynamicSchemaProps((props) => { + return ( + + + label="Username" + name="username" + rules={[{ required: true, message: 'Please input your username!' }]} + > + + + + label="Age" + name="age" + rules={[{ required: true, message: 'Please input your age!' }]} + > + + + + + + + ); +}); + +function useDemoFormProps(): DemoFormProps { + const resource = useDataBlockResource(); + const onFinish = async (values: DemoFormFieldType) => { + console.log('values', values); + await resource.create({ + values, + }); + notification.success({ + message: 'Save successfully!', + }); + }; + + return { + onFinish, + }; +} + +const collection = 'users'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: collection, + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'object', + 'x-component': 'DemoForm', + 'x-use-component-props': 'useDemoFormProps', + }, + }, +}; + +const Demo = () => { + return ; +}; + +const mocks = { + [`${collection}:create`]: (config) => { + console.log('config.data', config.data); + return [200, { msg: 'ok' }]; + }, +}; +const Root = createApp( + Demo, + { + components: { DemoForm }, + scopes: { useDemoFormProps }, + }, + mocks, +); + +export default Root; diff --git a/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/collection-form-get-and-update.tsx b/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/collection-form-get-and-update.tsx new file mode 100644 index 000000000..1383a5180 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/collection-form-get-and-update.tsx @@ -0,0 +1,158 @@ +import React, { FC, useEffect } from 'react'; +import { Button, Form, FormProps, Input, InputNumber, Select, notification } from 'antd'; +import { + SchemaComponent, + UseDataBlockProps, + useDataBlockResource, + useCollectionRecordData, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { ISchema } from '@formily/json-schema'; +import useUrlState from '@ahooksjs/use-url-state'; + +import { createApp } from '../../../data-source/demos/createApp'; + +interface DemoFormFieldType { + id: number; + username: string; + age: number; +} +type DemoFormProps = FormProps; +const DemoForm: FC = withDynamicSchemaProps((props) => { + return ( +
+ + label="Username" + name="username" + rules={[{ required: true, message: 'Please input your username!' }]} + > + + + + label="Age" + name="age" + rules={[{ required: true, message: 'Please input your age!' }]} + > + + + + + + + ); +}); + +function useDemoFormProps(): DemoFormProps { + const data = useCollectionRecordData(); + const resource = useDataBlockResource(); + + const [form] = Form.useForm(); + + useEffect(() => { + form.setFieldsValue(data); + }, [data, form]); + + const onFinish = async (values: DemoFormFieldType) => { + console.log('values', values); + await resource.update({ + filterByTk: data.id, + values, + }); + notification.success({ + message: 'Save successfully!', + }); + }; + + return { + initialValues: data, + preserve: true, + onFinish, + form, + }; +} +const useBlockDecoratorProps: UseDataBlockProps<'CollectionGet'> = () => { + const [state] = useUrlState({ id: '1' }); + return { + filterByTk: state.id, + }; +}; + +const collection = 'users'; +const action = 'get'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-use-decorator-props': 'useBlockDecoratorProps', + 'x-decorator-props': { + collection: collection, + action: action, + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'object', + 'x-component': 'DemoForm', + 'x-use-component-props': 'useDemoFormProps', + }, + }, +}; + +const Demo = () => { + const [state, setState] = useUrlState({ id: '1' }); + + return ( + <> + + + + ); +}; + +const mocks = { + [`${collection}:${action}`]: function (config) { + const { filterByTk } = config.params; + return { + data: + Number(filterByTk) === 1 + ? { + id: 1, + username: 'Bamboo', + age: 18, + } + : { + id: 2, + username: 'Mary', + age: 25, + }, + }; + }, + [`${collection}:update`]: function (config) { + console.log('config.data', config.data); + return { + data: 'ok', + }; + }, +}; +const Root = createApp( + Demo, + { + components: { DemoForm }, + scopes: { useDemoFormProps, useBlockDecoratorProps }, + }, + mocks, +); + +export default Root; diff --git a/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/collection-form-record-and-update.tsx b/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/collection-form-record-and-update.tsx new file mode 100644 index 000000000..2cd8518e0 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/collection-form-record-and-update.tsx @@ -0,0 +1,137 @@ +import React, { FC, useEffect } from 'react'; +import { Button, Form, FormProps, Input, InputNumber, notification } from 'antd'; +import { + CollectionRecordProvider, + SchemaComponent, + UseDataBlockProps, + useDataBlockResource, + useCollectionRecordData, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { ISchema } from '@formily/json-schema'; + +import { createApp } from '../../../data-source/demos/createApp'; + +interface DemoFormFieldType { + id: number; + username: string; + age: number; +} +type DemoFormProps = FormProps; +const DemoForm: FC = withDynamicSchemaProps((props) => { + return ( +
+ + label="Username" + name="username" + rules={[{ required: true, message: 'Please input your username!' }]} + > + + + + label="Age" + name="age" + rules={[{ required: true, message: 'Please input your age!' }]} + > + + + + + + + ); +}); + +function useDemoFormProps(): DemoFormProps { + const data = useCollectionRecordData(); + const resource = useDataBlockResource(); + + const [form] = Form.useForm(); + + useEffect(() => { + form.setFieldsValue(data); + }, [data, form]); + + const onFinish = async (values: DemoFormFieldType) => { + console.log('values', values); + await resource.update({ + filterByTk: data.id, + values, + }); + notification.success({ + message: 'Save successfully!', + }); + }; + + return { + initialValues: data, + preserve: true, + onFinish, + form, + }; +} +const useFormBlockDecoratorProps: UseDataBlockProps<'CollectionRecord'> = () => { + const record = useCollectionRecordData(); + return { + record, + }; +}; + +const collection = 'users'; +const action = 'get'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-use-decorator-props': 'useFormBlockDecoratorProps', + 'x-decorator-props': { + collection: collection, + action: action, + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'object', + 'x-component': 'DemoForm', + 'x-use-component-props': 'useDemoFormProps', + }, + }, +}; + +const recordData = { + id: 1, + username: 'Bamboo', + age: 18, +}; + +const Demo = () => { + return ( + <> + + + + + ); +}; + +const mocks = { + [`${collection}:update`]: function (config) { + console.log('config.data', config.data); + return { + data: 'ok', + }; + }, +}; +const Root = createApp( + Demo, + { + components: { DemoForm }, + scopes: { useDemoFormProps, useFormBlockDecoratorProps }, + }, + mocks, +); + +export default Root; diff --git a/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/collection-table-list.tsx b/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/collection-table-list.tsx new file mode 100644 index 000000000..278152be4 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/collection-table-list.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { Table, TableProps } from 'antd'; +import { SchemaComponent, useDataBlockRequest, withDynamicSchemaProps } from '@nocobase/client'; +import { ISchema } from '@formily/json-schema'; + +import { createApp } from '../../../data-source/demos/createApp'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: 'users', + action: 'list', + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', // 动态 table 属性 + }, + }, +}; + +const MyTable = withDynamicSchemaProps(Table); + +function useTableProps(): TableProps { + const { data, loading } = useDataBlockRequest(); + return { + loading, + dataSource: data?.data || [], + columns: [ + { + title: 'UserName', + dataIndex: 'username', + }, + { + title: 'NickName', + dataIndex: 'nickname', + }, + { + title: 'Email', + dataIndex: 'email', + }, + ], + }; +} + +const Demo = () => { + return ; +}; + +const Root = createApp(Demo, { + components: { MyTable }, + scopes: { useTableProps }, +}); + +export default Root; diff --git a/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/complete-demo.tsx b/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/complete-demo.tsx new file mode 100644 index 000000000..93bfcfa3a --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/demos/data-block-provider/complete-demo.tsx @@ -0,0 +1,162 @@ +import React, { useMemo } from 'react'; +import { + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + useDataBlockRequest, + useCollectionManager, + useCollection, + useCompile, + useDataBlockProps, + useDataBlock, + useSchemaInitializer, + useSchemaInitializerRender, + withDynamicSchemaProps, +} from '@nocobase/client'; + +import { createApp } from '../../../data-source/demos/createApp'; +import { Table, TableProps } from 'antd'; +import { ISchema } from '@formily/json-schema'; +import { observer, useFieldSchema } from '@formily/react'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'MyInitializer', +}; + +const MyTable = withDynamicSchemaProps(Table); + +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const compile = useCompile(); + const collection = useCollection(); + const columns = useMemo(() => { + return collection.getFields().map((field) => { + return { + title: compile(field.uiSchema?.title || field.name), + dataIndex: field.name, + }; + }); + }, [collection, compile]); + return { + ...tableProps, + loading, + dataSource: data?.data || [], + columns, + }; +} + +const MyTableSettings = new SchemaSettings({ + name: 'MyTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + ], +}); + +const TableDataBlockInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = ({ item }) => { + insert({ + type: 'void', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: item.value, + action: 'list', + tableProps: { + bordered: true, // 用于设置是否显示边框 + }, + }, + 'x-settings': 'MyTableSettings', + 'x-component': 'CardItem', + 'x-toolbar-props': { + draggable: false, + }, + properties: { + table: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', // 动态 table 属性 + }, + }, + }); + }; + const compile = useCompile(); + const collectionManager = useCollectionManager(); + const collectionMenuItems = useMemo( + () => + collectionManager.getCollections().map((collection) => { + return { + label: compile(collection.getOption('title')), + value: collection.getOption('name'), + }; + }), + [collectionManager, compile], + ); + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Add Block', + // 插入位置 + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +const Root = () => { + return ; +}; + +const App = createApp(Root, { + components: { MyTable, Page }, + scopes: { useTableProps }, + schemaSettings: [MyTableSettings], + schemaInitializers: [myInitializer], +}); + +export default App; diff --git a/packages/core/client/docs/zh-CN/core/data-block/demos/settings/demo1.tsx b/packages/core/client/docs/zh-CN/core/data-block/demos/settings/demo1.tsx new file mode 100644 index 000000000..fe3852422 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/demos/settings/demo1.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { + SchemaComponent, + SchemaSettings, + useDataBlockRequest, + useDataBlock, + withDynamicSchemaProps, +} from '@nocobase/client'; + +import { createApp } from '../../../data-source/demos/createApp'; +import { Table, TableProps } from 'antd'; +import { ISchema } from '@formily/json-schema'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: 'users', + action: 'list', + bordered: true, // 用于设置是否显示边框 + }, + 'x-settings': 'MyTableSettings', + 'x-component': 'CardItem', + 'x-toolbar-props': { + draggable: false, + }, + properties: { + demo: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', // 动态 table 属性 + }, + }, +}; + +const MyTable = withDynamicSchemaProps(Table); + +function useTableProps(): TableProps { + // 获取 + const { props } = useDataBlock(); + const { bordered } = props; + const { data, loading } = useDataBlockRequest(); + return { + bordered, + loading, + dataSource: data?.data || [], + columns: [ + { + title: 'Name', + dataIndex: 'name', + }, + { + title: 'Age', + dataIndex: 'age', + }, + { + title: 'Address', + dataIndex: 'address', + }, + ], + }; +} + +const MyTableSettings = new SchemaSettings({ + name: 'MyTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + // 获取 + const { props: blockSettingsProps, dn } = useDataBlock(); + const { bordered } = blockSettingsProps; + + return { + title: 'Bordered', + checked: bordered, + onChange: (checked) => { + // 修改 + dn.deepMerge({ 'x-decorator-props': { bordered: checked } }); + }, + }; + }, + }, + ], +}); + +const Demo = () => { + return ; +}; + +const Root = createApp(Demo, { + components: { MyTable }, + scopes: { useTableProps }, + schemaSettings: [MyTableSettings], +}); + +export default Root; diff --git a/packages/core/client/docs/zh-CN/core/data-block/images/data-block.png b/packages/core/client/docs/zh-CN/core/data-block/images/data-block.png new file mode 100644 index 000000000..0ac7ee8e8 Binary files /dev/null and b/packages/core/client/docs/zh-CN/core/data-block/images/data-block.png differ diff --git a/packages/core/client/docs/zh-CN/core/data-block/images/markdown-block.png b/packages/core/client/docs/zh-CN/core/data-block/images/markdown-block.png new file mode 100644 index 000000000..9c9c351be Binary files /dev/null and b/packages/core/client/docs/zh-CN/core/data-block/images/markdown-block.png differ diff --git a/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/collections.ts b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/collections.ts new file mode 100644 index 000000000..82ba795b8 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/collections.ts @@ -0,0 +1,128 @@ +export const mainCollections = [ + { + name: 'users', + title: 'Users', + fields: [ + { + key: 'id', + name: 'id', + interface: 'id', + primaryKey: true, + uiSchema: { + type: 'number', + title: 'ID', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + rawTitle: 'ID', + }, + }, + { + uiSchema: { + type: 'string', + title: 'Nickname', + 'x-component': 'Input', + }, + name: 'nickname', + type: 'string', + interface: 'input', + }, + { + uiSchema: { + type: 'string', + title: 'Email', + 'x-component': 'Input', + required: true, + }, + key: 'rrskwjl5wt1', + name: 'email', + type: 'string', + interface: 'email', + unique: true, + }, + { + name: 'favoriteColor', + type: 'string', + interface: 'color', + uiSchema: { + type: 'string', + 'x-component': 'ColorPicker', + default: '#1677FF', + title: 'Favorite Color', + }, + }, + ], + }, + { + name: 'roles', + title: 'Roles', + fields: [ + { + uiSchema: { + type: 'string', + title: 'Role UID', + 'x-component': 'Input', + }, + name: 'name', + type: 'uid', + interface: 'input', + primaryKey: true, + }, + { + uiSchema: { + type: 'string', + title: 'Role name', + 'x-component': 'Input', + }, + name: 'title', + type: 'string', + interface: 'input', + }, + { + name: 'description', + type: 'string', + }, + ], + }, +]; + +export const TestDBCollections = [ + { + name: 'test1', + title: 'Test1', + fields: [ + { + name: 'id', + interface: 'id', + primaryKey: true, + uiSchema: { + type: 'number', + title: 'ID', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + }, + }, + { + name: 'field1', + interface: 'input', + uiSchema: { + type: 'string', + 'x-component': 'Input', + title: 'Field1', + }, + }, + { + name: 'field2', + interface: 'number', + uiSchema: { + 'x-component-props': { + step: '1', + stringMode: true, + }, + 'x-component': 'InputNumber', + title: 'Field2', + }, + defaultValue: 1, + }, + ], + }, +]; diff --git a/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/createApp.tsx b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/createApp.tsx new file mode 100644 index 000000000..ebbc8ec12 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/createApp.tsx @@ -0,0 +1 @@ +import { Application } from '@nocobase/client'; diff --git a/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo1.tsx b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo1.tsx new file mode 100644 index 000000000..8c3f6a54f --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo1.tsx @@ -0,0 +1,41 @@ +import { Table } from 'antd'; +import React from 'react'; + +const MyTable = () => { + const dataSource = [ + { + key: '1', + name: 'Mike', + age: 32, + address: '10 Downing Street', + }, + { + key: '2', + name: 'John', + age: 42, + address: '10 Downing Street', + }, + ]; + + const columns = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, + { + title: 'Age', + dataIndex: 'age', + key: 'age', + }, + { + title: 'Address', + dataIndex: 'address', + key: 'address', + }, + ]; + + return
; +}; + +export default MyTable; diff --git a/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo10.tsx b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo10.tsx new file mode 100644 index 000000000..844315f33 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo10.tsx @@ -0,0 +1,412 @@ +import { Button, Drawer, Space, Table, TableProps } from 'antd'; +import React, { useEffect, useMemo, useState } from 'react'; +import { + CardItem, + CollectionField, + ColorFieldInterface, + ColorPicker, + DataBlockProvider, + EmailFieldInterface, + FormItem, + IdFieldInterface, + Input, + InputFieldInterface, + InputNumber, + NumberFieldInterface, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + SchemaToolbar, + useCollection, + useCompile, + useDataBlock, + useDataBlockProps, + useDataBlockRequest, + useDataSourceManager, + useSchemaInitializer, + useSchemaInitializerRender, + useSchemaToolbar, + useSchemaToolbarRender, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, RecursionField, Schema, observer, useField, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const MyTable = withDynamicSchemaProps(Table, { displayName: 'MyTable' }); + +const TableColumn = observer(() => { + const field = useField(); + return
{field.title}
; +}); + +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const collection = useCollection(); + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + + return { + title: , + dataIndex: collectionField.name, + render(value, record, index) { + return ( + + ); + }, + }; + }); + }, [collection]); + + return { + ...tableProps, + loading, + dataSource, + columns, + }; +} + +const myTableSettings = new SchemaSettings({ + name: 'myTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const CreateAction = () => { + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + + const compile = useCompile(); + const collection = useCollection(); + const title = compile(collection.title); + const fieldSchema = useFieldSchema(); + + const { render } = useSchemaToolbarRender(fieldSchema); + return ( + <> +
+ {render()} + +
+ +

Some contents...

+
+ + ); +}; + +const createActionSettings = new SchemaSettings({ + name: 'createActionSettings', + items: [ + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const RefreshAction = () => { + const { refresh } = useDataBlockRequest(); + return ; +}; + +const ActionBar = ({ children }) => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + + return ( +
+ + {children} + {render()} + +
+ ); +}; + +const CreateActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'CreateAction', + 'x-settings': 'createActionSettings', + }); + }; + return ; +}; + +const RefreshActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'RefreshAction', + }); + }; + return ; +}; + +const tableActionInitializers = new SchemaInitializer({ + name: 'tableActionInitializers', + title: 'Configure actions', + icon: 'SettingOutlined', + style: { + marginLeft: 8, + }, + items: [ + { + type: 'item', + name: 'addNew', + Component: CreateActionInitializer, + }, + { + type: 'item', + name: 'refresh', + Component: RefreshActionInitializer, + }, + ], +}); + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-settings': 'myTableSettings', + 'x-decorator': 'DataBlockProvider', + 'x-toolbar': 'MyToolbar', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + tableProps: { + bordered: true, + }, + }, + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-initializer': 'tableActionInitializers', + }, + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + + return ; +}; + +const MyToolbar = (props) => { + const collection = useCollection(); + const compile = useCompile(); + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable, TableColumn, MyToolbar, ActionBar, CreateAction, RefreshAction }); + this.app.schemaInitializerManager.add(myInitializer); + this.app.schemaSettingsManager.add(myTableSettings); + this.app.schemaSettingsManager.add(createActionSettings); + this.app.addScopes({ useTableProps }); + this.app.schemaInitializerManager.add(tableActionInitializers); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + InputNumber, + Input, + CollectionField, + ColorPicker, + FormItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + fieldInterfaces: [ + IdFieldInterface, + InputFieldInterface, + EmailFieldInterface, + ColorFieldInterface, + NumberFieldInterface, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo11.tsx b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo11.tsx new file mode 100644 index 000000000..e362ddf03 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo11.tsx @@ -0,0 +1,492 @@ +import { Button, Drawer, Space, Table, TableProps } from 'antd'; +import React, { useEffect, useMemo, useState } from 'react'; +import { + CardItem, + CollectionField, + ColorFieldInterface, + ColorPicker, + DataBlockProvider, + EmailFieldInterface, + FormItem, + IdFieldInterface, + Input, + InputFieldInterface, + InputNumber, + NumberFieldInterface, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaInitializerSwitch, + SchemaSettings, + SchemaToolbar, + useCollection, + useCompile, + useCurrentSchema, + useDataBlock, + useDataBlockProps, + useDataBlockRequest, + useDataSourceManager, + useDesignable, + useSchemaInitializer, + useSchemaInitializerItem, + useSchemaInitializerRender, + useSchemaToolbarRender, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, RecursionField, observer, useField, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const MyTable = withDynamicSchemaProps(Table, { displayName: 'MyTable' }); + +const TableColumn = observer(() => { + const field = useField(); + return
{field.title}
; +}); + +function useTableColumns() { + const schema = useFieldSchema(); + const filed = useField(); + const { designable } = useDesignable(); + const { render } = useSchemaInitializerRender(schema['x-initializer'], schema['x-initializer-props']); + const columns = schema.mapProperties((tableField: any, name) => { + return { + title: , + dataIndex: name, + key: name, + width: 200, + render(value, record, index) { + return ; + }, + }; + }); + + const tableColumns = useMemo(() => { + return [ + ...columns, + { + title: render(), + dataIndex: 'TABLE_COLUMN_INITIALIZER', + key: 'TABLE_COLUMN_INITIALIZER', + width: 200, + render: designable ? () =>
: null, + }, + ]; + }, [columns, render, designable]); + + return tableColumns; +} + +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useTableColumns(); + + return { + ...tableProps, + loading, + dataSource, + columns, + }; +} + +const myTableSettings = new SchemaSettings({ + name: 'myTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const CreateAction = () => { + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + + const compile = useCompile(); + const collection = useCollection(); + const title = compile(collection.title); + const fieldSchema = useFieldSchema(); + + const { render } = useSchemaToolbarRender(fieldSchema); + return ( + <> +
+ {render()} + +
+ +

Some contents...

+
+ + ); +}; + +const createActionSettings = new SchemaSettings({ + name: 'createActionSettings', + items: [ + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const RefreshAction = () => { + const { refresh } = useDataBlockRequest(); + return ; +}; + +const ActionBar = ({ children }) => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + + return ( +
+ + {children} + {render()} + +
+ ); +}; + +const CreateActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'CreateAction', + 'x-settings': 'createActionSettings', + }); + }; + return ; +}; + +const RefreshActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'RefreshAction', + }); + }; + return ; +}; + +const tableActionInitializers = new SchemaInitializer({ + name: 'tableActionInitializers', + title: 'Configure actions', + icon: 'SettingOutlined', + style: { + marginLeft: 8, + }, + items: [ + { + type: 'item', + name: 'addNew', + Component: CreateActionInitializer, + }, + { + type: 'item', + name: 'refresh', + Component: RefreshActionInitializer, + }, + ], +}); + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-settings': 'myTableSettings', + 'x-decorator': 'DataBlockProvider', + 'x-toolbar': 'MyToolbar', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + tableProps: { + bordered: true, + }, + }, + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-initializer': 'tableActionInitializers', + }, + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + 'x-initializer': 'tableColumnInitializers', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + + return ; +}; + +const useTableColumnInitializerFields = () => { + const collection = useCollection(); + return collection.fields.map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + return { + type: 'item', + name: collectionField.name, + title: collectionField?.uiSchema?.title || collectionField.name, + Component: 'TableCollectionFieldInitializer', + schema: tableFieldSchema, + }; + }); +}; + +export const TableCollectionFieldInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { insert } = useSchemaInitializer(); + const { remove } = useDesignable(); + const schema = useFieldSchema(); + const exists = !!schema.properties?.[itemConfig.name]; + return ( + { + if (exists) { + return remove(schema.properties?.[itemConfig.name]); + } + insert(itemConfig.schema); + }} + /> + ); +}; + +const tableColumnInitializers = new SchemaInitializer({ + name: 'tableColumnInitializers', + insertPosition: 'beforeEnd', + icon: 'SettingOutlined', + title: 'Configure columns', + items: [ + { + name: 'displayFields', + type: 'itemGroup', + title: 'Display fields', + useChildren: useTableColumnInitializerFields, + }, + ], +}); + +const MyToolbar = (props) => { + const collection = useCollection(); + const compile = useCompile(); + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ + MyTable, + TableColumn, + MyToolbar, + ActionBar, + CreateAction, + RefreshAction, + TableCollectionFieldInitializer, + }); + this.app.schemaInitializerManager.add(myInitializer); + this.app.schemaSettingsManager.add(myTableSettings); + this.app.addScopes({ useTableProps }); + this.app.schemaInitializerManager.add(tableActionInitializers); + this.app.schemaInitializerManager.add(tableColumnInitializers); + this.app.schemaSettingsManager.add(createActionSettings); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + InputNumber, + Input, + CollectionField, + ColorPicker, + FormItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + fieldInterfaces: [ + IdFieldInterface, + InputFieldInterface, + EmailFieldInterface, + ColorFieldInterface, + NumberFieldInterface, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo12.tsx b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo12.tsx new file mode 100644 index 000000000..1fd5d9a67 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo12.tsx @@ -0,0 +1,532 @@ +import { Button, Drawer, Space, Table, TableProps } from 'antd'; +import React, { useEffect, useMemo, useState } from 'react'; +import { + CardItem, + CollectionField, + ColorFieldInterface, + ColorPicker, + DataBlockProvider, + EmailFieldInterface, + FormItem, + IdFieldInterface, + Input, + InputFieldInterface, + InputNumber, + NumberFieldInterface, + Plugin, + CollectionRecordProvider, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaInitializerSwitch, + SchemaSettings, + SchemaToolbar, + useCollection, + useCompile, + useCurrentSchema, + useDataBlock, + useDataBlockProps, + useDataBlockRequest, + useDataSource, + useDataSourceManager, + useDesignable, + useCollectionRecord, + useSchemaInitializer, + useSchemaInitializerItem, + useSchemaInitializerRender, + useSchemaToolbarRender, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, RecursionField, observer, useField, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const MyTable = withDynamicSchemaProps(Table, { displayName: 'MyTable' }); + +const TableColumn = observer(() => { + const field = useField(); + return
{field.title}
; +}); + +const ColumnView = observer( + () => { + const record = useCollectionRecord(); + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + return ( + <> + + +
{JSON.stringify(record, null, 2)}
+
+ + ); + }, + { displayName: 'ColumnView' }, +); + +function useTableColumns() { + const schema = useFieldSchema(); + const filed = useField(); + const { designable } = useDesignable(); + const { render } = useSchemaInitializerRender(schema['x-initializer'], schema['x-initializer-props']); + const columns = schema.mapProperties((tableField: any, name) => { + return { + title: , + dataIndex: name, + key: name, + render(value, record, index) { + return ; + }, + }; + }); + const tableColumns = useMemo(() => { + return [ + ...columns, + { + title: 'Actions', + dataIndex: 'actions', + key: 'actions', + render: (value, record) => { + return ( + + + + + + ); + }, + }, + { + title: render(), + dataIndex: 'TABLE_COLUMN_INITIALIZER', + key: 'TABLE_COLUMN_INITIALIZER', + render: designable ? () =>
: null, + }, + ]; + }, [columns, render, designable]); + + return tableColumns; +} + +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useTableColumns(); + + return { + ...tableProps, + loading, + dataSource, + columns, + }; +} + +const myTableSettings = new SchemaSettings({ + name: 'myTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const CreateAction = () => { + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + + const compile = useCompile(); + const collection = useCollection(); + const title = compile(collection.title); + const fieldSchema = useFieldSchema(); + + const { render } = useSchemaToolbarRender(fieldSchema); + return ( + <> +
+ {render()} + +
+ +

Some contents...

+
+ + ); +}; + +const createActionSettings = new SchemaSettings({ + name: 'createActionSettings', + items: [ + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const RefreshAction = () => { + const { refresh } = useDataBlockRequest(); + return ; +}; + +const ActionBar = ({ children }) => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + + return ( +
+ + {children} + {render()} + +
+ ); +}; + +const CreateActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'CreateAction', + 'x-settings': 'createActionSettings', + }); + }; + return ; +}; + +const RefreshActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'RefreshAction', + }); + }; + return ; +}; + +const tableActionInitializers = new SchemaInitializer({ + name: 'tableActionInitializers', + title: 'Configure actions', + icon: 'SettingOutlined', + style: { + marginLeft: 8, + }, + items: [ + { + type: 'item', + name: 'addNew', + Component: CreateActionInitializer, + }, + { + type: 'item', + name: 'refresh', + Component: RefreshActionInitializer, + }, + ], +}); + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-settings': 'myTableSettings', + 'x-decorator': 'DataBlockProvider', + 'x-toolbar': 'MyToolbar', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + tableProps: { + bordered: true, + }, + }, + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-initializer': 'tableActionInitializers', + }, + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + 'x-initializer': 'tableColumnInitializers', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + + return ; +}; + +const useTableColumnInitializerFields = () => { + const collection = useCollection(); + return collection.fields.map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + return { + type: 'item', + name: collectionField.name, + title: collectionField?.uiSchema?.title || collectionField.name, + Component: 'TableCollectionFieldInitializer', + schema: tableFieldSchema, + }; + }); +}; + +export const TableCollectionFieldInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { insert } = useSchemaInitializer(); + const { remove } = useDesignable(); + const schema = useFieldSchema(); + const exists = !!schema.properties?.[itemConfig.name]; + return ( + { + if (exists) { + return remove(schema.properties?.[itemConfig.name]); + } + insert(itemConfig.schema); + }} + /> + ); +}; + +const tableColumnInitializers = new SchemaInitializer({ + name: 'tableColumnInitializers', + insertPosition: 'beforeEnd', + icon: 'SettingOutlined', + title: 'Configure columns', + items: [ + { + name: 'displayFields', + type: 'itemGroup', + title: 'Display fields', + useChildren: useTableColumnInitializerFields, + }, + ], +}); + +const MyToolbar = (props) => { + const collection = useCollection(); + const compile = useCompile(); + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ + MyTable, + TableColumn, + MyToolbar, + ActionBar, + CreateAction, + RefreshAction, + TableCollectionFieldInitializer, + }); + this.app.schemaInitializerManager.add(myInitializer); + this.app.schemaSettingsManager.add(myTableSettings); + this.app.addScopes({ useTableProps }); + this.app.schemaInitializerManager.add(tableActionInitializers); + this.app.schemaInitializerManager.add(tableColumnInitializers); + this.app.schemaSettingsManager.add(createActionSettings); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + InputNumber, + Input, + CollectionField, + ColorPicker, + FormItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + fieldInterfaces: [ + IdFieldInterface, + InputFieldInterface, + EmailFieldInterface, + ColorFieldInterface, + NumberFieldInterface, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo2.tsx b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo2.tsx new file mode 100644 index 000000000..f487115da --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo2.tsx @@ -0,0 +1,82 @@ +import { Table } from 'antd'; +import React from 'react'; +import { CardItem, Plugin, SchemaComponent } from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; + +const MyTable = () => { + const dataSource = [ + { + key: '1', + name: 'Mike', + age: 32, + address: '10 Downing Street', + }, + { + key: '2', + name: 'John', + age: 42, + address: '10 Downing Street', + }, + ]; + + const columns = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, + { + title: 'Age', + dataIndex: 'age', + key: 'age', + }, + { + title: 'Address', + dataIndex: 'address', + key: 'address', + }, + ]; + + return
; +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable }); + } +} + +const tableSchema = { + name: 'demo', + type: 'void', + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + }, + }, +}; + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + CardItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, +}); + +app.router.add('root', { + path: '/', + Component: Root, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo3.tsx b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo3.tsx new file mode 100644 index 000000000..1d899d6fb --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo3.tsx @@ -0,0 +1,147 @@ +import { Table } from 'antd'; +import React, { useCallback } from 'react'; +import { + CardItem, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + useSchemaInitializer, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, observer, useFieldSchema } from '@formily/react'; + +const MyTable = () => { + const dataSource = [ + { + key: '1', + name: 'Mike', + age: 32, + address: '10 Downing Street', + }, + { + key: '2', + name: 'John', + age: 42, + address: '10 Downing Street', + }, + ]; + + const columns = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, + { + title: 'Age', + dataIndex: 'age', + key: 'age', + }, + { + title: 'Address', + dataIndex: 'address', + key: 'address', + }, + ]; + + return
; +}; + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = useCallback( + ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }, + [insert, setVisible], + ); + + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable }); + this.app.schemaInitializerManager.add(myInitializer); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo4.tsx b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo4.tsx new file mode 100644 index 000000000..7c327227b --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo4.tsx @@ -0,0 +1,180 @@ +import { Table } from 'antd'; +import React, { useMemo } from 'react'; +import { + CardItem, + DataBlockProvider, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + useCollection, + useDataBlockRequest, + useDataSourceManager, + useSchemaInitializer, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, observer, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const MyTable = () => { + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const collection = useCollection(); + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { + return { + title: collectionField.uiSchema?.title || collectionField.name, + dataIndex: collectionField.name, + }; + }); + }, [collection]); + + return
; +}; + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + }, + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable }); + this.app.schemaInitializerManager.add(myInitializer); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo5.tsx b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo5.tsx new file mode 100644 index 000000000..51b09f7bc --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo5.tsx @@ -0,0 +1,236 @@ +import { Table } from 'antd'; +import React, { useEffect, useMemo } from 'react'; +import { + CardItem, + CollectionField, + ColorFieldInterface, + ColorPicker, + DataBlockProvider, + EmailFieldInterface, + FormItem, + IdFieldInterface, + Input, + InputFieldInterface, + InputNumber, + NumberFieldInterface, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + useCollection, + useDataBlockRequest, + useDataSourceManager, + useSchemaInitializer, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, RecursionField, observer, useField, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const TableColumn = observer(() => { + const field = useField(); + return
{field.title}
; +}); + +const MyTable = () => { + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const collection = useCollection(); + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + + return { + title: , + dataIndex: collectionField.name, + render(value, record, index) { + return ( + + ); + }, + }; + }); + }, [collection]); + + return
; +}; + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + }, + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable, TableColumn }); + this.app.schemaInitializerManager.add(myInitializer); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + InputNumber, + Input, + CollectionField, + ColorPicker, + FormItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + fieldInterfaces: [ + IdFieldInterface, + InputFieldInterface, + EmailFieldInterface, + ColorFieldInterface, + NumberFieldInterface, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo6.tsx b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo6.tsx new file mode 100644 index 000000000..a07197d3f --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo6.tsx @@ -0,0 +1,280 @@ +import { Table, TableProps } from 'antd'; +import React, { useEffect, useMemo } from 'react'; +import { + CardItem, + CollectionField, + ColorFieldInterface, + ColorPicker, + DataBlockProvider, + EmailFieldInterface, + FormItem, + IdFieldInterface, + Input, + InputFieldInterface, + InputNumber, + NumberFieldInterface, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + useCollection, + useDataBlock, + useDataBlockProps, + useDataBlockRequest, + useDataSourceManager, + useSchemaInitializer, + useSchemaInitializerRender, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, RecursionField, observer, useField, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const MyTable = withDynamicSchemaProps(Table, { displayName: 'MyTable' }); + +const TableColumn = observer(() => { + const field = useField(); + return
{field.title}
; +}); + +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const collection = useCollection(); + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + + return { + title: , + dataIndex: collectionField.name, + render(value, record, index) { + return ( + + ); + }, + }; + }); + }, [collection]); + + return { + ...tableProps, + loading, + dataSource, + columns, + rowKey: 'id', + }; +} + +const myTableSettings = new SchemaSettings({ + name: 'myTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-settings': 'myTableSettings', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + tableProps: { + bordered: true, + }, + }, + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable, TableColumn }); + this.app.schemaInitializerManager.add(myInitializer); + this.app.schemaSettingsManager.add(myTableSettings); + this.app.addScopes({ useTableProps }); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + InputNumber, + Input, + CollectionField, + ColorPicker, + FormItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + fieldInterfaces: [ + IdFieldInterface, + InputFieldInterface, + EmailFieldInterface, + ColorFieldInterface, + NumberFieldInterface, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo7.tsx b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo7.tsx new file mode 100644 index 000000000..6acb8e01c --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo7.tsx @@ -0,0 +1,291 @@ +import { Table, TableProps } from 'antd'; +import React, { useEffect, useMemo } from 'react'; +import { + CardItem, + CollectionField, + ColorFieldInterface, + ColorPicker, + DataBlockProvider, + EmailFieldInterface, + FormItem, + IdFieldInterface, + Input, + InputFieldInterface, + InputNumber, + NumberFieldInterface, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + SchemaToolbar, + useCollection, + useCompile, + useDataBlock, + useDataBlockProps, + useDataBlockRequest, + useDataSource, + useDataSourceManager, + useSchemaInitializer, + useSchemaInitializerRender, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, RecursionField, observer, useField, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const MyTable = withDynamicSchemaProps(Table, { displayName: 'MyTable' }); + +const TableColumn = observer(() => { + const field = useField(); + return
{field.title}
; +}); + +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const collection = useCollection(); + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + + return { + title: , + dataIndex: collectionField.name, + render(value, record, index) { + return ( + + ); + }, + }; + }); + }, [collection]); + + return { + ...tableProps, + loading, + dataSource, + columns, + }; +} + +const myTableSettings = new SchemaSettings({ + name: 'myTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-settings': 'myTableSettings', + 'x-decorator': 'DataBlockProvider', + 'x-toolbar': 'MyToolbar', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + tableProps: { + bordered: true, + }, + }, + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + + return ; +}; + +const MyToolbar = (props) => { + const collection = useCollection(); + const compile = useCompile(); + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable, TableColumn, MyToolbar }); + this.app.schemaInitializerManager.add(myInitializer); + this.app.schemaSettingsManager.add(myTableSettings); + this.app.addScopes({ useTableProps }); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + InputNumber, + Input, + CollectionField, + ColorPicker, + FormItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + fieldInterfaces: [ + IdFieldInterface, + InputFieldInterface, + EmailFieldInterface, + ColorFieldInterface, + NumberFieldInterface, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo8.tsx b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo8.tsx new file mode 100644 index 000000000..edb6b3310 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo8.tsx @@ -0,0 +1,351 @@ +import { Button, Drawer, Space, Table, TableProps } from 'antd'; +import React, { useEffect, useMemo, useState } from 'react'; +import { + CardItem, + CollectionField, + ColorFieldInterface, + ColorPicker, + DataBlockProvider, + EmailFieldInterface, + FormItem, + IdFieldInterface, + Input, + InputFieldInterface, + InputNumber, + NumberFieldInterface, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + SchemaToolbar, + useCollection, + useCompile, + useDataBlock, + useDataBlockProps, + useDataBlockRequest, + useDataSource, + useDataSourceManager, + useSchemaInitializer, + useSchemaInitializerRender, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, RecursionField, observer, useField, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const MyTable = withDynamicSchemaProps(Table, { displayName: 'MyTable' }); + +const TableColumn = observer(() => { + const field = useField(); + return
{field.title}
; +}); + +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const collection = useCollection(); + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + + return { + title: , + dataIndex: collectionField.name, + render(value, record, index) { + return ( + + ); + }, + }; + }); + }, [collection]); + + return { + ...tableProps, + loading, + dataSource, + columns, + }; +} + +const myTableSettings = new SchemaSettings({ + name: 'myTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const CreateAction = () => { + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + + const compile = useCompile(); + const collection = useCollection(); + const title = compile(collection.title); + + return ( + <> + + +

Some contents...

+
+ + ); +}; + +const RefreshAction = () => { + const { refresh } = useDataBlockRequest(); + return ; +}; + +const ActionBar = () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + + return ( +
+ + + + {render()} + +
+ ); +}; + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-settings': 'myTableSettings', + 'x-decorator': 'DataBlockProvider', + 'x-toolbar': 'MyToolbar', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + tableProps: { + bordered: true, + }, + }, + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-initializer': 'tableActionInitializers', + }, + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + + return ; +}; + +const MyToolbar = (props) => { + const collection = useCollection(); + const compile = useCompile(); + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable, TableColumn, MyToolbar, ActionBar }); + this.app.schemaInitializerManager.add(myInitializer); + this.app.schemaSettingsManager.add(myTableSettings); + this.app.addScopes({ useTableProps }); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + InputNumber, + Input, + CollectionField, + ColorPicker, + FormItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + fieldInterfaces: [ + IdFieldInterface, + InputFieldInterface, + EmailFieldInterface, + ColorFieldInterface, + NumberFieldInterface, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo9.tsx b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo9.tsx new file mode 100644 index 000000000..f5cf4261b --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/demo9.tsx @@ -0,0 +1,394 @@ +import { Button, Drawer, Space, Table, TableProps } from 'antd'; +import React, { useEffect, useMemo, useState } from 'react'; +import { + CardItem, + CollectionField, + ColorFieldInterface, + ColorPicker, + DataBlockProvider, + EmailFieldInterface, + FormItem, + IdFieldInterface, + Input, + InputFieldInterface, + InputNumber, + NumberFieldInterface, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + SchemaToolbar, + useCollection, + useCompile, + useDataBlock, + useDataBlockProps, + useDataBlockRequest, + useDataSource, + useDataSourceManager, + useSchemaInitializer, + useSchemaInitializerRender, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { Application } from '@nocobase/client'; +import { uid } from '@formily/shared'; +import { ISchema, RecursionField, observer, useField, useFieldSchema } from '@formily/react'; +import { mainCollections, TestDBCollections } from './collections'; +import { mock } from './mockData'; + +const MyTable = withDynamicSchemaProps(Table, { displayName: 'MyTable' }); + +const TableColumn = observer(() => { + const field = useField(); + return
{field.title}
; +}); + +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const collection = useCollection(); + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + + return { + title: , + dataIndex: collectionField.name, + render(value, record, index) { + return ( + + ); + }, + }; + }); + }, [collection]); + + return { + ...tableProps, + loading, + dataSource, + columns, + }; +} + +const myTableSettings = new SchemaSettings({ + name: 'myTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + { + type: 'remove', + name: 'remove', + }, + ], +}); + +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +function useCollectionMenuItems() { + const dataSourceManager = useDataSourceManager(); + const allCollections = dataSourceManager.getAllCollections(); + const menus = useMemo( + () => + allCollections.map((item) => { + const { key, displayName, collections } = item; + return { + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((collection) => { + return { + name: collection.name, + label: collection.title, + collection: collection.name, + dataSource: key, + }; + }), + }; + }), + [allCollections], + ); + + return menus; +} + +const CreateAction = () => { + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + + const compile = useCompile(); + const collection = useCollection(); + const title = compile(collection.title); + + return ( + <> + + +

Some contents...

+
+ + ); +}; + +const RefreshAction = () => { + const { refresh } = useDataBlockRequest(); + return ; +}; + +const ActionBar = ({ children }) => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + + return ( +
+ + {children} + {render()} + +
+ ); +}; + +const CreateActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'CreateAction', + }); + }; + return ; +}; + +const RefreshActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'RefreshAction', + }); + }; + return ; +}; + +const tableActionInitializers = new SchemaInitializer({ + name: 'tableActionInitializers', + title: 'Configure actions', + icon: 'SettingOutlined', + style: { + marginLeft: 8, + }, + items: [ + { + type: 'item', + name: 'addNew', + Component: CreateActionInitializer, + }, + { + type: 'item', + name: 'refresh', + Component: RefreshActionInitializer, + }, + ], +}); + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + 'x-settings': 'myTableSettings', + 'x-decorator': 'DataBlockProvider', + 'x-toolbar': 'MyToolbar', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', + tableProps: { + bordered: true, + }, + }, + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-initializer': 'tableActionInitializers', + }, + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }; + + const menuItems = useCollectionMenuItems(); + + return ; +}; + +const MyToolbar = (props) => { + const collection = useCollection(); + const compile = useCompile(); + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); + +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable, TableColumn, MyToolbar, ActionBar, CreateAction, RefreshAction }); + this.app.schemaInitializerManager.add(myInitializer); + this.app.schemaSettingsManager.add(myTableSettings); + this.app.addScopes({ useTableProps }); + this.app.schemaInitializerManager.add(tableActionInitializers); + } +} + +const Root = () => { + return ; +}; + +const app = new Application({ + plugins: [MyPlugin], + components: { + Page, + AddBlockButton, + CardItem, + DataBlockProvider, + InputNumber, + Input, + CollectionField, + ColorPicker, + FormItem, + }, + router: { + type: 'memory', + initialEntries: ['/'], + }, + designable: true, + dataSourceManager: { + collections: mainCollections, + dataSources: [ + { + key: 'test-db', + displayName: 'TestDB', + collections: TestDBCollections, + }, + ], + fieldInterfaces: [ + IdFieldInterface, + InputFieldInterface, + EmailFieldInterface, + ColorFieldInterface, + NumberFieldInterface, + ], + }, + apiClient: { + baseURL: 'http://localhost:8000', + }, +}); + +app.router.add('home', { + path: '/', + Component: Root, +}); + +mock(app); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/mockData.ts b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/mockData.ts new file mode 100644 index 000000000..59bff781e --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/tmp-demos/mockData.ts @@ -0,0 +1,79 @@ +import type { Application } from '@nocobase/client'; +import MockAdapter from 'axios-mock-adapter'; + +const mockData = { + 'users:list': { + data: [ + { + id: '1', + nickname: 'Jack Ma', + email: 'test@gmail.com', + favoriteColor: '#1677FF', + }, + { + id: '2', + email: 'jim@gmail.com', + nickname: 'Jim Green', + favoriteColor: '#36C', + }, + { + id: '3', + nickname: 'Tom Cat', + email: 'tom@gmail.com', + favoriteColor: '#F0F', + }, + ], + }, + 'users:destroy': { + result: 'ok', + }, + 'roles:list': { + data: [ + { + name: 'root', + title: 'Root', + description: 'Root', + }, + { + name: 'admin', + title: 'Admin', + description: 'Admin description', + }, + ], + }, + 'test1:list': { + data: [ + { + id: '1', + field1: 'aaa', + field2: 1, + }, + { + id: '2', + field1: 'bbb', + field2: 2, + }, + { + id: '3', + field1: 'ccc', + field2: 3, + }, + ], + }, +}; + +export function mock(app: Application) { + const mock = new MockAdapter(app.apiClient.axios); + + Object.entries(mockData).forEach(([url, data]) => { + mock.onGet(url).reply(async (config) => { + return [200, data]; + }); + mock.onPost(url).reply(async (config) => { + return [200, data]; + }); + mock.onDelete(url).reply(async (config) => { + return [200, data]; + }); + }); +} diff --git a/packages/core/client/docs/zh-CN/core/data-block/tmp.md b/packages/core/client/docs/zh-CN/core/data-block/tmp.md new file mode 100644 index 000000000..49807329c --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-block/tmp.md @@ -0,0 +1,1149 @@ +# 临时 + + +## 区块介绍 + + +## 区块扩展 + +### 1. 先简单定义一个 Table 组件 + +```tsx | pure +const MyTable = () => { + const dataSource = [ + { + key: '1', + name: 'Mike', + age: 32, + address: '10 Downing Street', + }, + { + key: '2', + name: 'John', + age: 42, + address: '10 Downing Street', + }, + ]; + + const columns = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, + { + title: 'Age', + dataIndex: 'age', + key: 'age', + }, + { + title: 'Address', + dataIndex: 'address', + key: 'address', + }, + ]; + + return
; +} +``` + + + +### 2. 使用 UI Schema 的方式将组件渲染到页面上 + +先将组件注册到 NocoBase 应用中 + +```tsx | pure +import { Plugin } from '@nocobase/client'; + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable }) + } +} +``` + +然后定义 UI Schema,并通过 `SchemaComponent` 渲染到页面上。 + +```tsx | pure +const tableSchema = { + name: 'demo', + type: 'void', + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + }, + }, +} + +const Root = () => { + return +} + +app.router.add('root', { + path: '/', + Component: Root, +}) +``` + + + +关于 UI Schema 的更多信息请参考 [UI Schema](/core/ui-schema/schema-component) 和 [UI Schema Quick Start](https://docs-cn.nocobase.com/development/client/ui-schema/quick-start) + +其中 `CardItem` 是 NocoBase 的内置组件,相当于外层的容器。 + +### 3. 使用 `SchemaInitializer` 将组件动态添加到页面上 + +首先我们将 `Root` 组件更换 Schema 为 `rootSchema`。 + +```tsx | pure +const rootSchema: ISchema = { + type: 'void', + name: 'root', + 'x-component': 'Page', + 'x-initializer': 'myInitializer', +}; + +const Root = () => { + return ; +}; +``` + +其中 `Page` 的定义如下: + +```tsx | pure +import { observer } from "@formily/react"; + +const AddBlockButton = observer(() => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); +}, { displayName: 'AddBlockButton' }); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); +``` + +其中: + +- `useSchemaInitializerRender()`:用于获取 `SchemaInitializer` 的渲染函数,具体用法请参考 [useSchemaInitializerRender](/core/ui-schema/schema-initializer#useschemainitializer)。 +- `useFieldSchema()`:用于获取当前字段的 schema,具体用法请参考 [useFieldSchema](/core/ui-schema/designable#usefieldschema)。 +- `observer`:用于将函数组件转换为响应式组件,具体用法请参考 [observer](https://react.formilyjs.org/api/shared/observer)。 + +`myInitializer` 的定义如下: + +```tsx | pure +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = useCallback(({ item }) => { + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + }, + }, + }; + insert(tableSchema); + setVisible(false); + }, [insert, setVisible]); + + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'myInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + items: [ + { + name: 'table', + Component: TableDataBlockInitializer, + }, + ], +}); +``` + +我们将 `tableSchema` 放到了 `TableDataBlockInitializer` 中,当点击 `Add Block` 时,会将 `tableSchema` 插入到页面上。 + +关于 SchemaInitializer 的更多信息请参考 [SchemaInitializer](/core/ui-schema/schema-initializer) 和 [SchemaInitializer Quick Start](https://docs-cn.nocobase.com/development/client/ui-schema/quick-start#schemainitializer)。 + +最后将 `myInitializer` 注册到 NocoBase 应用中。 + +```diff +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable }) ++ this.app.schemaInitializerManager.add(myInitializer); + } +} +``` + + + +在真正的插件开发过程中只需要定义和添加 `myInitializer` 和 `MyTable` 即可,`Page` 和 `Root` 是 NocoBase 内置的组件,无需重复定义。 + + +### 4. 从数据表中读取数据 + +上述 Table 中的数据是固定数据,我们需要动态的获取数据并渲染。 + +```diff ++ function useCollectionMenuItems() { ++ const dataSourceManager = useDataSourceManager(); ++ const allCollections = dataSourceManager.getAllCollections(); ++ const menus = useMemo( ++ () => allCollections.map((item) => { ++ const { key, displayName, collections } = item; ++ return { ++ name: key, ++ label: displayName, ++ type: 'subMenu', ++ children: collections.map((collection) => { ++ return { ++ name: collection.name, ++ label: collection.title, ++ collection: collection.name, ++ dataSource: key, ++ } ++ }) ++ }; ++ }), ++ [allCollections], ++ ); ++ ++ return menus; ++ } + +const TableDataBlockInitializer = () => { + const { insert, setVisible } = useSchemaInitializer(); + + const handleClick = ({ item }) => { + const tableSchema = { + type: 'void', ++ 'x-decorator': 'DataBlockProvider', ++ 'x-decorator-props': { ++ collection: item.collection, ++ dataSource: item.dataSource, ++ action: 'list', ++ }, + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', + }, + }, + } + insert(tableSchema); + setVisible(false); + }; + ++ const collectionMenuItems = useCollectionMenuItems(); + return ; +}; +``` + +其中: + +- [DataBlockProvider](/core/data-block/data-block-provider) 是 NocoBase 的内置组件,主要作用有4个: + - 提供 Collection 表结构信息 + - 提供 Collection 表的数据 + - 提供对数据增删改查的 [Resource](https://docs.nocobase.com/api/sdk#action-api) 对象 + - 提供区块的配置信息 + +- item:是当前点击的菜单对象,包括 `collection` 和 `dataSource`。 +- `useCollectionMenuItems`:是一个自定义的 hook,用于获取所有的 Collection 表信息,返回一个菜单数组。 + - `dataSourceManager`:用于获取数据源管理器,具体用法请参考 [useDataSourceManager](/core/data-source/data-source-manager-provider#usedatasourcemanager)。 + + +然后我们修改 `MyTable` 组件 + +```diff +const MyTable = () => { +- const dataSource = [ +- { +- key: '1', +- name: 'Mike', +- age: 32, +- address: '10 Downing Street', +- }, +- { +- key: '2', +- name: 'John', +- age: 42, +- address: '10 Downing Street', +- }, +- ]; +- +- const columns = [ +- { +- title: 'Name', +- dataIndex: 'name', +- key: 'name', +- }, +- { +- title: 'Age', +- dataIndex: 'age', +- key: 'age', +- }, +- { +- title: 'Address', +- dataIndex: 'address', +- key: 'address', +- }, +- ]; + + ++ const { data, loading } = useDataBlockRequest(); ++ const dataSource = useMemo(() => data?.data || [], [data]); + ++ const collection = useCollection(); ++ const columns = useMemo(() => { ++ return collection.getFields().map((collectionField) => { ++ return { ++ title: compile(collectionField.uiSchema?.title || collectionField.name), ++ dataIndex: collectionField.name, ++ }; ++ }); ++ }, [collection, compile]); + + return
; +} +``` + +其中: + +- `useDataBlockRequest()`:由 [DataBlockProvider](/core/data-block/data-block-provider) 组件里面的 [DataBlockRequestProvider](/core/data-block/data-block-request-provider) 提供,用于获取对应的数据表结构。 +- `useCollection()`:由 [DataBlockProvider](/core/data-block/data-block-provider) 组件里面的 [DataBlockRequestProvider](/core/data-source/collection-provider) 提供,用于获取当前 Collection 的信息。 + + + +### 5. 将字段渲染为自定义组件 + +上面一步仅是把数据渲染成普通的字符串,还需要进一步把字段渲染成不同的组件。 + +我们以 `favoriteColor` 字段为例,其数据结构如下: + +```json +{ + "name": "favoriteColor", + "type": "string", + "interface": "color", + "uiSchema": { + "type": "string", + "x-component": "ColorPicker", + "default": "#1677FF", + "title": "Favorite Color" + } +} +``` + +其和渲染相关的是 `uiSchema` 字段和 `interface` 字段,`interface` 相当于一些公共的配置,`uiSchema` 相当于一些私有的配置,更多信息请参考 [CollectionField](/core/data-source/collection-field)。 + +```diff ++ useEffect(() => { ++ field.value = dataSource; ++ }, [dataSource]); + + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { ++ const tableFieldSchema = { + name: collectionField.name, ++ type: 'void', ++ title: collectionField.uiSchema?.title || collectionField.name, ++ 'x-component': 'TableColumn', ++ properties: { ++ [collectionField.name]: { ++ 'x-component': 'CollectionField', ++ 'x-read-pretty': true, ++ 'x-decorator-props': { ++ labelStyle: { ++ display: 'none', ++ }, ++ }, ++ }, ++ }, ++ }; ++ ++ return { +- title: collectionField.uiSchema?.title || collectionField.name, ++ title: , + dataIndex: collectionField.name, ++ render(value, record, index) { ++ return ; ++ }, ++ }; + }); + }, [collection]); +``` + +```diff ++ const TableColumn = observer(() => { ++ const field = useField(); ++ return
{field.title}
; ++ }); + +class MyPlugin extends Plugin { + async load() { + // ... ++ this.app.addComponents({ TableColumn }); + } +} +``` + +其中: + +`field.value = dataSource` 用于将数据赋值给 `MyTable` 这一层的 `field`,相当于: + +```diff +const tableSchema = { + // ... + properties: { + [uid()]: { + type: 'array', ++ value: dataSource, + 'x-component': 'MyTable', + }, + }, +}; +``` + +然后使用 `RecursionField` 组件将字段渲染成自定义组件,因为是数组,所以需要加上 `index`,这样子组件才能正确的读取到数据。 + +关于 `RecursionField` 的更多信息请参考 [RecursionField](https://react.formilyjs.org/api/components/recursion-fieldeld)。 + + + +### 6. 区块属性配置 + +首先我们先修改 `tableSchema`。 + +```diff + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', ++ 'x-settings': 'myTableSettings', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: item.collection, + dataSource: item.dataSource, + action: 'list', ++ tableProps: { ++ bordered: true, ++ }, + }, + properties: { + [uid()]: { + type: 'array', + 'x-component': 'MyTable', ++ 'x-use-component-props': 'useTableProps', + }, + }, +} +``` + +其中: + +- `x-settings`:用于指定区块的配置信息,具体用法请参考 [SchemaSettings](/core/ui-schema/schema-settings)。 +- `tableProps`:Table 的属性 +- `x-use-component-props`:用于动态读取的 UI Schema 属性 + +关于 `useTableProps` 的定义,我们先将 `MyTable` 的属性单独抽离为一个 Hook。 + +```tsx | pure +function useTableProps(): TableProps { + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); + const collection = useCollection(); + + const field = useField(); + + useEffect(() => { + field.value = dataSource; + }, [dataSource]); + + const columns = useMemo(() => { + return collection.getFields().map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + + return { + title: , + dataIndex: collectionField.name, + render(value, record, index) { + return ; + }, + }; + }); + }, [collection]); + + return { + loading, + dataSource, + columns, + }; +} +``` + +然后增加如下内容: + +```diff +function useTableProps(): TableProps { ++ const { tableProps } = useDataBlockProps(); + // ... + + return { ++ ...tableProps, + loading, + dataSource, + columns, + }; +} +``` + +其中: + +- `useDataBlockProps()`:用于获取区块的配置信息,具体用法请参考 [useDataBlockProps](/core/data-block/data-block-provider#usedatablockprops)。 + + +再将 `MyTable` 组件使用 `withDynamicSchemaProps` 包裹。 + +```tsx | pure +const MyTable = withDynamicSchemaProps(Table, { displayName: 'MyTable' }); +``` + +`withDynamicSchemaProps` 的作用是能读取 Schema 中的 `x-use-component-props` 属性,将其解析并执行,然后将结果传递给组件。 + +然后我们实现 `myTableSettings`: + +```tsx | pure +const myTableSettings = new SchemaSettings({ + name: 'myTableSettings', + items: [ + { + name: 'bordered', + type: 'switch', + useComponentProps() { + const { props: blockSettingsProps, dn } = useDataBlock(); + + return { + title: 'Bordered', + checked: !!blockSettingsProps.tableProps?.bordered, + onChange: (checked) => { + // 修改 schema + dn.deepMerge({ 'x-decorator-props': { tableProps: { bordered: checked } } }); + }, + }; + }, + }, + { + type: 'remove', + name: 'remove', + } + ], +}); +``` + +然后我们将 `myTableSettings` 和 `useTableProps` 注册到 NocoBase 应用中。 + +```diff +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable }) + this.app.schemaInitializerManager.add(myInitializer); ++ this.app.schemaSettingsManager.add(myTableSettings); ++ this.app.addScopes({ useTableProps }); + } +} +``` + +以上各个部分的关系如下: + +- `tableSchema`:`tableProps` 存储属性配置信息 +- `DataBlockProvider`:读取 `tableSchema` 的配置,并通过 `useDataBlockProps` 传递子组件 +- `useTableProps`:通过 `useDataBlockProps` 获取 `DataBlockProvider` 提供的配置 +- `MyTable`:通过 `withDynamicSchemaProps` 自动读取到 `'x-use-component-props': 'useTableProps'` 进行编译,执行 `useTableProps()` 获取到配置传递给 `Table` +- `myTableSettings`:通过 `useDataBlock()` 并修改配置 `DataBlockProvider` 提供的配置 + +然后在右上角的 `Settings` 中可以看到 `Bordered` 选项,可以动态修改 `Table` 的 `bordered` 属性。 + + + + +### 7. 区块 Toolbar 配置 + +我们还可以给区块增加 Toolbar 配置,让其在左上角显示所属的 Collection 表信息。 + +我们先修改 `tableSchema`。 + +```diff + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', ++ 'x-toolbar': 'MyToolbar', + // ... +} +``` + +然后我们实现 `MyToolbar`。 + +```tsx | pure +const MyToolbar = (props) => { + const collection = useCollection(); + const compile = useCompile(); + return +} +``` + +然后我们将 `MyToolbar` 注册到 NocoBase 应用中。 + +```diff +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ MyTable }) + this.app.schemaInitializerManager.add(myInitializer); + this.app.schemaSettingsManager.add(myTableSettings); + this.app.addScopes({ useTableProps }); ++ this.app.addComponents({ MyToolbar }); + } +} +``` + +这样左上角 Toolbar 就会显示所属的 Collection 表信息。 + +关于 `SchemaToolbar` 的更多信息请参考 [SchemaToolbar](/core/ui-schema/schema-toolbar)。 + + + +### 8. 增加顶部操作按钮 + +我们可以给区块增加顶部操作按钮,比如增加 `Add New` 和 `Refresh` 新增列表数据按钮。 + +我们先修改 `tableSchema`。 + +```diff + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + // ... + properties: { ++ actions: { ++ type: 'void', ++ 'x-component': 'ActionBar', ++ }, + // ... + } +} +``` + +然后我们实现 `ActionBar`。 + +```tsx | pure +const CreateAction = () => { + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + + const compile = useCompile(); + const collection = useCollection(); + const title = compile(collection.title); + + return <> + + +

Some contents...

+
+ +} + +const RefreshAction = () => { + const { refresh } = useDataBlockRequest(); + return +} + +const ActionBar = () => { + return ( +
+ + + + +
+ ) +}; +``` + +其中: + +- `useDataBlockRequest()`: 用于获取区块的请求对象,具体用法请参考 [useDataBlockRequest](/core/data-block/data-block-request-provider#usedatablockrequest)。 + +然后我们将 `ActionBar` 注册到 NocoBase 应用中。 + +```diff +class MyPlugin extends Plugin { + async load() { + // ... ++ this.app.addComponents({ ActionBar }); + } +} +``` + + + +### 9. 动态配置顶部操作按钮 + +上面的 `ActionBar` 里面的 `CreateAction` 和 `RefreshAction` 是固定的,我们可以将其配置动态化。 + +首先修改 `tableSchema`。 + +```diff + const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + // ... + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', ++ 'x-initializer': 'tableActionInitializers', + }, + // ... + } +} +``` + +然后我们实现 `tableActionInitializers`。 + +```tsx | pure +const CreateActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'CreateAction', + }); + }; + return ; +} + + +const RefreshActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'RefreshAction', + }); + }; + return ; +} + +const tableActionInitializers = new SchemaInitializer({ + name: 'tableActionInitializers', + title: "Configure actions", + icon: 'SettingOutlined', + style: { + marginLeft: 8, + }, + items: [ + { + type: 'item', + name: 'addNew', + title: "Add New", + Component: CreateActionInitializer, + }, + { + type: 'item', + name: 'refresh', + title: "Refresh", + Component: RefreshActionInitializer, + }, + ] +}) +``` + +然后修改 `ActionBar`: + +```diff +const ActionBar = ({ children }) => { ++ const fieldSchema = useFieldSchema(); ++ const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + + return ( +
+ ++ {children} ++ {render()} + +
+ ) +}; +``` + +然后我们将 `tableActionInitializers` 和 `CreateAction`、`RefreshAction` 注册到 NocoBase 应用中。 + +```diff +class MyPlugin extends Plugin { + async load() { + // ... ++ this.app.addComponents({ CreateAction, RefreshAction }); ++ this.app.schemaInitializerManager.add(tableActionInitializers); + } +} +``` + + + +### 10. 操作按钮的配置 + +目前的操作按钮只能添加,不能删除或者配置 ,我们可以通过 `SchemaSettings` 来配置。 + +这里以 `Add New` 按钮为例。首先我们修改 `CreateActionInitializer`。 + +```diff +const CreateActionInitializer = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'CreateAction', ++ 'x-settings': 'createActionSettings', + }); + }; + return ; +}; +``` + +然后我们实现 `createActionSettings`。 + +```tsx | pure +const createActionSettings = new SchemaSettings({ + name: 'createActionSettings', + items: [ + { + type: 'remove', + name: 'remove', + }, + ], +}) +``` + +然后我们将 `createActionSettings` 注册到 NocoBase 应用中。 + +```diff +class MyPlugin extends Plugin { + async load() { + // ... ++ this.app.schemaSettingsManager.add(createActionSettings); + } +} +``` + +最后将 `createActionSettings` 渲染出来。 + +```diff +const CreateAction = () => { + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + + const compile = useCompile(); + const collection = useCollection(); + const title = compile(collection.title); + ++ const fieldSchema = useFieldSchema(); ++ const { render } = useSchemaToolbarRender(fieldSchema); + + return ( + <> +- ++
++ {render()} ++ ++
+ +

Some contents...

+
+ + ); +}; +``` + +需要注意的是,要将 `render()` 和 `Add New` 包裹在一个 `div` 中,这样 `render()` 才能正确的渲染。 + + + +### 11. 动态配置 table 列 + +首先修改 `tableSchema`。 + +```diff +const tableSchema = { + type: 'void', + 'x-component': 'CardItem', + // ... + properties: { + action: { + // ... + }, + [uid()]: { + type: 'array', + 'x-component': 'MyTable', ++ 'x-initializer': 'tableColumnInitializers', + 'x-use-component-props': 'useTableProps', + }, + } +} +``` + +然后我们实现 `tableColumnInitializers`。 + +```tsx | pure +const tableColumnInitializers = new SchemaInitializer({ + name: 'tableColumnInitializers', + insertPosition: 'beforeEnd', + icon: 'SettingOutlined', + title: 'Configure columns', + items: [ + { + name: 'displayFields', + type: 'itemGroup', + title: 'Display fields', + useChildren: useTableColumnInitializerFields, + } + ], +}) + +const useTableColumnInitializerFields = () => { + const collection = useCollection(); + return collection.fields.map((collectionField) => { + const tableFieldSchema = { + name: collectionField.name, + type: 'void', + title: collectionField.uiSchema?.title || collectionField.name, + 'x-component': 'TableColumn', + properties: { + [collectionField.name]: { + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }, + }, + }; + return { + type: 'item', + name: collectionField.name, + title: collectionField?.uiSchema?.title || collectionField.name, + Component: 'TableCollectionFieldInitializer', + schema: tableFieldSchema, + }; + }); +}; + +export const TableCollectionFieldInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { insert } = useSchemaInitializer(); + const { remove } = useDesignable(); + const schema = useFieldSchema(); + const exists = !!schema.properties?.[itemConfig.name]; + return ( + { + if (exists) { + return remove(schema.properties?.[itemConfig.name]); + } + insert(itemConfig.schema); + }} + /> + ); +}; +``` + +然后我们修改 `useTableProps`,增加 `useTableColumns`: + +```diff +function useTableProps(): TableProps { + const { tableProps } = useDataBlockProps(); + const { data, loading } = useDataBlockRequest(); + const dataSource = useMemo(() => data?.data || [], [data]); ++ const columns = useTableColumns(); + + return { + ...tableProps, + loading, + dataSource, + columns, + }; +} +``` + +```tsx | pure +function useTableColumns() { + const schema = useFieldSchema(); + const filed = useField(); + const { designable } = useDesignable(); + const { render } = useSchemaInitializerRender(schema['x-initializer'], schema['x-initializer-props']); + const columns = schema.mapProperties((tableField: any, name) => { + return { + title: , + dataIndex: name, + key: name, + width: 200, + render(value, record, index) { + return ; + }, + }; + }); + + const tableColumns = useMemo(() => { + return [ + ...columns, + { + title: render(), + dataIndex: 'TABLE_COLUMN_INITIALIZER', + key: 'TABLE_COLUMN_INITIALIZER', + width: 200, + render: designable ? () =>
: null, + }, + ]; + }, [columns, render, designable]); + + return tableColumns; +} +``` + +然后我们将 `tableColumnInitializers` 和 `TableCollectionFieldInitializer` 注册到 NocoBase 应用中。 + +```diff +class MyPlugin extends Plugin { + async load() { + // ... ++ this.app.schemaInitializerManager.add(tableColumnInitializers); ++ this.app.addComponents({ TableCollectionFieldInitializer }); + } +} +``` + + + +### 12. 增加操作列 + +修改 `useTableColumns`: + +```diff +function useTableColumns() { + // ... + const tableColumns = useMemo(() => { + return [ + ...columns, ++ { ++ title: 'Actions', ++ dataIndex: 'actions', ++ key: 'actions', ++ render: (value, record) => { ++ return ( ++ ++ ++ ++ ++ ++ ); ++ }, ++ }, + { + title: render(), + dataIndex: 'TABLE_COLUMN_INITIALIZER', + key: 'TABLE_COLUMN_INITIALIZER', + render: designable ? () =>
: null, + }, + ]; + }, [columns, render, designable]); + + return tableColumns; +} +``` + +然后我们实现 `ColumnView`。 + +```tsx | pure + +const ColumnView = observer( + () => { + const record = useCollectionRecord(); + const [open, setOpen] = useState(false); + + const showDrawer = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + return ( + <> + + +
{JSON.stringify(record, null, 2)}
+
+ + ); + }, + { displayName: 'ColumnView' }, +); +``` + + diff --git a/packages/core/client/docs/zh-CN/core/data-source/association-provider.md b/packages/core/client/docs/zh-CN/core/data-source/association-provider.md new file mode 100644 index 000000000..97da3d3ab --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/association-provider.md @@ -0,0 +1,13 @@ +# AssociationProvider + +用于传递关联数据字段以及对应的数据表信息,等同于 `CollectionFieldProvider` + `CollectionProvider`。 + +```tsx | pure +const AssociationProvider = + + {children} + + +``` + +关于关系字段的更多信息,请参考 [关系字段](https://docs.nocobase.com/development/server/collections/association-fields) 以及 [CollectionField](/core/data-source/collection-field)。 diff --git a/packages/core/client/docs/zh-CN/core/data-source/collection-field-interface-manager.md b/packages/core/client/docs/zh-CN/core/data-source/collection-field-interface-manager.md new file mode 100644 index 000000000..b589784e5 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/collection-field-interface-manager.md @@ -0,0 +1,171 @@ +# CollectionFieldInterfaceManager + +主要是用来管理 [CollectionFieldInterface](./collection-field-interface.md) 和 [CollectionFieldInterfaceGroups](#collectionfieldinterfacegroups),其被 [DataSourceManager](./data-source-manager) 管理。 + + +## CollectionFieldInterfaceGroups + +CollectionFieldInterfaceGroups 是用来对数据表字段进行分组的。 + +![Field Groups](./images/field-groups.png) + +## 实例方法 + +### field interface + +#### addFieldInterfaces() + +添加 field interface。 + +- 类型 + +```tsx | pure +class CollectionFieldInterfaceManager { + addFieldInterfaces(fieldInterfaces: CollectionFieldInterface[]): void +} +``` + +- 示例 + +```tsx | pure +class CheckboxFieldInterface extends CollectionFieldInterface { + name = 'checkbox'; + type = 'object'; + group = 'choices'; + title = '{{t("Checkbox")}}'; + // ... +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.collectionFieldInterfaceManager.addFieldInterfaces([CheckboxFieldInterface]); + // or + this.app.dataSourceManager.addFieldInterfaces([CheckboxFieldInterface]); + } +} +``` + +#### getFieldInterface() + +获取 field interface。 + +- 类型 + +```tsx | pure +class CollectionFieldInterfaceManager { + getFieldInterface(name: string): T; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const fieldInterface = this.app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterface('checkbox'); // checkboxFieldInterface + } +} +``` + +#### getFieldInterfaces() + +获取所有的 field interface。 + +- 类型 + +```tsx | pure +class CollectionFieldInterfaceManager { + getFieldInterfaces(): CollectionFieldInterface[]; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const fieldInterfaces = this.app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterfaces(); + } +} +``` + +### field interface group + +#### addFieldInterfaceGroups() + +添加 field interface group。 + +- 类型 + +```tsx | pure +class CollectionFieldInterfaceManager { + addFieldInterfaceGroups(fieldGroups: Record): void; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.collectionFieldInterfaceManager.addFieldInterfaceGroups({ + 'test': { + label: 'Test', + order: 1, + } + }); + + // or + this.app.dataSourceManager.addFieldInterfaceGroups({ + 'test': { + label: 'Test', + order: 1, + } + }); + } +} +``` + +#### getFieldInterfaceGroups() + +获取所有的 field interface group。 + +- 类型 + +```tsx | pure +class CollectionFieldInterfaceManager { + getFieldInterfaceGroups(): Record; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const fieldInterfaceGroups = this.app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterfaceGroups(); // { 'test': { label: 'Test', order: 1 } } + } +} +``` + +#### getFieldInterfaceGroup() + +获取 field interface group。 + +- 类型 + +```tsx | pure +class CollectionFieldInterfaceManager { + getFieldInterfaceGroup(name: string): { label: string; order?: number }; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const fieldInterfaceGroup = this.app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterfaceGroup('test'); // { label: 'Test', order: 1 } + } +} +``` diff --git a/packages/core/client/docs/zh-CN/core/data-source/collection-field-interface.md b/packages/core/client/docs/zh-CN/core/data-source/collection-field-interface.md new file mode 100644 index 000000000..bb2c1aebf --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/collection-field-interface.md @@ -0,0 +1,94 @@ +# CollectionFieldInterface + +用于创建数据字段。 + +![](./images/collection-field-interface.png) + +```ts +class CollectionFieldInterface { + app: Application; + collectionManager: CollectionManager; + + name: string; + group: string; + title?: string; + description?: string; + order?: number; + default?: { + type: string; + uiSchema?: ISchema; + [key: string]: any; + }; + sortable?: boolean; + availableTypes?: string[]; + hasDefaultValue?: boolean; + isAssociation?: boolean; + operators?: any[]; + filterable?: { + operators?: any[]; + children?: any[]; + [key: string]: any; + }; + titleUsable?: boolean; + + validateSchema(fieldSchema: ISchema): Record + usePathOptions(field: CollectionFieldOptions): any + schemaInitialize(schema: ISchema, data: any): void + + getOption(key: K): CollectionFieldInterfaceOptions[K] + getOptions(): CollectionFieldInterfaceOptions; + setOptions(options: CollectionFieldInterfaceOptions): void; +} +``` + +其需要结合 [CollectionManager](./collection-field-interface-manager.md) 使用。 + +```ts +class EmailFieldInterface extends CollectionFieldInterface { + name = 'email'; + type = 'object'; + group = 'basic'; + order = 4; + title = '{{t("Email")}}'; + sortable = true; + // ... +} + +class MyPlugin extends Plugin { + load() { + this.app.dataSourceManager.addFieldInterfaces([ EmailFieldInterface ]); + } +} +``` + +## 实例属性 + +### name + +唯一标识符。 + +### group + +分组。 + +### title + +标题。 + +### default + +配置表单默认值字段 schema。 + +![](./images/collection-field-interface-form.png) + + +## 实例方法 + +### collectionFieldInterface.validateSchema(fieldSchema) + + +### collectionFieldInterface.usePathOptions(field: CollectionFieldOptions) + + +### collectionFieldInterface.schemaInitialize(schema: ISchema, data: any) + diff --git a/packages/core/client/docs/zh-CN/core/data-source/collection-field.md b/packages/core/client/docs/zh-CN/core/data-source/collection-field.md new file mode 100644 index 000000000..3f64b94a6 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/collection-field.md @@ -0,0 +1,164 @@ +# CollectionField + +NocoBase 将字段的 schema 分为 2 部分,一部分在 schema 中,一部分在 collection 中。例如: + +```tsx | pure +const schema = { + properties: { + username: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + }, + } +} + +const collection = { + fields: [ + { + type: 'string', + name: 'username', + interface: 'input', + uiSchema: { + title: 'UserName', + type: 'string', + 'x-component': 'Input', + required: true, + description: 'description1', + } as ISchema, + } + ], +} +``` + +两者通过 `name: username` 联系起来,`CollectionField` 会自动读取 schema `name` 属性,并根据 `name` 属性查找 collection 中对应的 `uiSchema` 属性,然后拼接到 schema 中,进行渲染。 + +这样做的好处是,对于同一个字段创建的内容,可以在不同的地方共享同一个 schema,当 schema 变化时,只需要修改一处即可。比如通过上面的 `title: "UserName"` 假设变化 `title: "Name"` 则所有使用到此字段的地方都会变化。 + + + +## CollectionFieldOptions + +字段的配置项。 + +```ts +interface CollectionFieldOptions { + name?: any; + collectionName?: string; + sourceKey?: string; + uiSchema?: ISchema; + target?: string; + + [key: string]: any; +} +``` + +### 普通字段和关系字段 + +字段有 2 种情况,一种是普通字段,一种是 [关系字段](https://docs-cn.nocobase.com/development/server/collections/association-fields)。 + +关系字段是指,字段的值是另一个 collection 的数据,例如 `users` 和 `roles` 两个 collection,`users` 中有一个字段 `roles`,其值是 `roles` collection 的数据,那么 `roles` 就是一个关系字段。 + +普通字段的示例如下: + +```json +{ + "key": "ootprgkoawo", + "name": "email", + "type": "string", + "interface": "email", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "unique": true, + "uiSchema": { + "type": "string", + "title": "{{t(\"Email\")}}", + "x-component": "Input", + "x-validator": "email", + "required": true + } +} +``` + +关系字段的示例如下: + +```json +{ + "key": "t09bauwm0wb", + "name": "roles", + "type": "belongsToMany", + "interface": "m2m", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "target": "roles", + "foreignKey": "userId", + "otherKey": "roleName", + "onDelete": "CASCADE", + "sourceKey": "id", + "targetKey": "name", + "through": "rolesUsers", + "uiSchema": { + "type": "array", + "title": "{{t(\"Roles\")}}", + "x-component": "AssociationField", + "x-component-props": { + "multiple": true, + "fieldNames": { + "label": "title", + "value": "name" + } + } + } +} +``` + +相对于普通字段,关系字段多了以下属性: + +- xx +- xx + +### 全部字段说明 + +- `name`:字段名称 +- `collectionName`:数据表名称 +- `sourceKey`:当字段为关系字段时,对应的关系字段名称。 + +TODO:补全 + +## Hooks + +### useCollectionField() + +用于获取字段信息。 + +```tsx | pure +const collection = { + fields: [ + { + type: 'string', + name: 'username', + interface: 'input', + uiSchema: { + title: 'UserName', + type: 'string', + 'x-component': 'Input', + required: true, + description: 'description1', + } as ISchema, + } + ], +} + +const { uiSchema } = useCollectionField() +const required = uiSchema?.required +``` + +其通常在 [SchemaSettings](/core/ui-schema/schema-settings) 中使用,用来获取和修改字段的属性。 + + + + + diff --git a/packages/core/client/docs/zh-CN/core/data-source/collection-manager-provider.md b/packages/core/client/docs/zh-CN/core/data-source/collection-manager-provider.md new file mode 100644 index 000000000..b37a6a27f --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/collection-manager-provider.md @@ -0,0 +1,52 @@ +# CollectionManagerProvider + +用于提供 [CollectionManager](/core/data-source/collection-manager) 实例。 + +## 组件 + +- 类型 + +```tsx | pure +interface CollectionManagerProviderProps { + instance?: CollectionManager; + dataSource?: string; + children?: ReactNode; +} +``` + +- 参数详解 + - `dataSource` - 数据源名称,如果为空,则会取默认数据源。 + - `instance` - CollectionManager 实例,如果没有,则会取 `dataSource` 对应的 collectionManager。 + +- 示例 + +```tsx | pure +const collectionManager = new CollectionManager(); + +const Demo = () => { + return ( + +
...
+
+ ); +}; +``` + +## Hooks + +### useCollectionManager() + +用于获取 `CollectionManagerProvider` 传递的实例。 + +- 示例 + +```tsx | pure +const Demo = () => { + const collectionManager = useCollectionManager(); + const collections = collectionManager.getCollections() + + return
+
{JSON.stringify(collections, null, 2)}
+
; +}; +``` diff --git a/packages/core/client/docs/zh-CN/core/data-source/collection-manager.md b/packages/core/client/docs/zh-CN/core/data-source/collection-manager.md new file mode 100644 index 000000000..5df92575d --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/collection-manager.md @@ -0,0 +1,175 @@ +# CollectionManager + +用于管理 [Collection](./collection.md),其被 [DataSource](./data-source.md) 管理。 + +## 实例方法 + +### addCollections(collections) + +添加数据表。 + +- 类型 + +```tsx | pure +class CollectionManager { + addCollections(collections: CollectionOptions[]): void +} +``` + +- 示例 + +```tsx | pure +const userCollectionOptions = { + "name": "users", + "title": "Users", + fields: [ + // ... + ], +}; + +collectionManager.addCollections([userCollectionOptions]); +``` + +### setCollections(collections) + +重置数据表,会先移除所有数据表,然后再调用 `addCollections()` 添加数据表。 + +- 类型 + +```tsx | pure +class CollectionManager { + setCollections(collections: CollectionOptions[]): void +} +``` + +### reAddCollections(collections) + +由于 [CollectionTemplate](./collection-template.md) 或者 [CollectionMixins](./collection-mixins.md) 的添加会影响 Collection 的实例化,所以提供了重新添加数据表的方法。 + +- 类型 + +```tsx | pure +class CollectionManager { + reAddCollections(collectionInstances: Collection[]): void +} +``` + +- 示例 + +```tsx | pure +const userCollectionInstance = collectionManager.getCollection('users'); +collectionManager.reAddCollections([userCollectionInstance]); +``` + +### getCollections(predicate?) + +获取数据表。 + +- 类型 + +```tsx | pure +class CollectionManager { + getCollections(predicate?: (collection: Collection) => boolean) +} +``` + +- 示例 + +```tsx | pure +collectionManager.getCollections(); // [ userCollection ] + +collectionManager.getCollections(collection => collection.name === 'posts'); // [ postCollection ] +``` + + +### getCollection(path) + +获取数据表。 + +- 类型 + +```tsx | pure +class CollectionManager { + getCollection(path: string): (Mixins & Collection) | undefined +} +``` + +- 详细解释 + - `path` 参数可以是数据表名称,也可以是[关系字段](https://docs.nocobase.com/development/server/collections/association-fields)路径。 + - `path: 'users'`: 获取 `users` 数据表 + - `path: 'users.posts'`: 获取 `users` 数据表的 `posts` 关联字段对应的数据表,即 `postCollection` + +- 示例 + +```tsx | pure +collectionManager.getCollection('users'); // userCollection + +collectionManager.getCollection('users.posts'); // postCollection +collectionManager.getCollection('users.profileId'); // profileCollection +``` + +结合 Mixin 使用: + +```tsx | pure +const collection = collectionManager.getCollection('users'); +const collection = collectionManager.getCollection('users'); +``` + +### getCollectionFields(collectionName) + +获取数据表字段列表。 + +- 类型 + +```tsx | pure +class CollectionManager { + getCollectionFields(collectionName: string): CollectionFieldOptions[]; +} +``` + +- 示例 + +```tsx | pure +collectionManager.getCollectionFields('users'); // [ { name: 'username', type: 'string', title: 'Username', .. }, { name: 'password', type: 'password', title: 'Password', .. } ] +``` + +### getCollectionName(path) + +获取数据表名称。 + +- 类型 + +```tsx | pure +class CollectionManager { + getCollectionName(path: string: GetCollectionOptions): string | undefined; +} +``` + +- 示例 + +```tsx | pure +collectionManager.getCollectionName('users'); // 'users' + +collectionManager.getCollectionName('users.profiles'); // 'profiles' +``` + + +### getCollectionField(path) + +获取数据表字段。 + +- 类型 + +```tsx | pure +class CollectionManager { + getCollectionField(path: string: GetCollectionOptions): CollectionFieldOptions | undefined; +} +``` + +- 示例 + +```tsx | pure +collectionManager.getCollectionField('users.username'); // { name: 'username', type: 'string', title: 'Username', .. } + +collectionManager.getCollectionField('users.roles.name'); // 获取 roles 关联字段对应的 roles 表中的 name 字段 +``` diff --git a/packages/core/client/docs/zh-CN/core/data-source/collection-mixins.md b/packages/core/client/docs/zh-CN/core/data-source/collection-mixins.md new file mode 100644 index 000000000..6512fccc3 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/collection-mixins.md @@ -0,0 +1,57 @@ + +## Collection Mixins + +Collection Mixins 是为扩展 Collection 类提供的一种机制,可以通过 `dataSourceManager.addCollectionMixins()` 添加 Collection Mixins。 + +### 定义和注册 + +```tsx | pure +import { Collection, Plugin } from '@nocobase/client'; + +class TestMixin extends Collection { + test() { + const { name } = this.options; + return 'test '+ name; + } +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.addCollectionMixins([TestMixin]); + } +} +``` + +### 使用 + +- 使用方式1:在 `CollectionManager` 实例上调用 `getCollection()` 获取指定 `Collection` 实例。 + +```tsx | pure +const Demo = () => { + const cm = useCollectionManager(); + const userCollection = cm.getCollection('users'); + + userCollection.test(); // 'test users' +} +``` + +- 使用方式2:调用 `useCollection()` 获取当前上下文的数据表信息。 + +```tsx | pure +const Demo = () => { + const collection = useCollection(); + collection.test(); // 'test users' +} +``` + +### 多个 Mixins 的使用 + +如果添加了 Mixins,可通过如下方式获得类型提示: + +```tsx | pure +const Demo = () => { + const collection = useCollection(); +} +``` + + diff --git a/packages/core/client/docs/zh-CN/core/data-source/collection-provider.md b/packages/core/client/docs/zh-CN/core/data-source/collection-provider.md new file mode 100644 index 000000000..141187df8 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/collection-provider.md @@ -0,0 +1,63 @@ +# CollectionProvider + +用于提供 [Collection](/core/data-source/collction) 实例。 + +## 组件 + +### CollectionProvider + +- 类型 + +```tsx | pure +interface CollectionProviderProps { + name: string; + dataSource?: string; + children?: ReactNode; +} +``` + +- 详解 + +组件会根据 `name` 去 [CollectionManager](/core/data-source/collection-manager) 中查询数据表信息,如果查询不到,则会不进行渲染。 + +`dataSource` 用于指定数据表所在的[命名空间](/core/data-source/collection-manager#datasource),如果不指定,则默认命名空间。 + +- 示例 + +```tsx | pure +import { CollectionProvider } from '@nocobase/client'; + +const MyComponent = () => { + return ( + +
...
+
+ ) +} +``` + + +## Hooks + +### useCollection() + +用于获取 `CollectionProvider` 传递的 `Collection` 实例。 + +```tsx | pure +const collection = useCollection() + +console.log(collection instanceof Collection) // true +console.log(collection); +``` + +结合 Mixin 使用: + +```tsx | pure +const collection = useCollection() +const collection = useCollection() +``` + +## 示例 + + + diff --git a/packages/core/client/docs/zh-CN/core/data-source/collection-template-manager.md b/packages/core/client/docs/zh-CN/core/data-source/collection-template-manager.md new file mode 100644 index 000000000..1ff0bffea --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/collection-template-manager.md @@ -0,0 +1,86 @@ +# CollectionTemplateManager + +用于管理 [CollectionTemplate](./collection-template),其被 [DataSourceManager](./data-source-manager) 管理。 + +## 实例方法 + +### addCollectionTemplates() + +用于添加 collection template。 + +- 类型 + +```tsx | pure +class CollectionTemplateManager { + addCollectionTemplates(templates: CollectionTemplate[]): void; +} +``` + +- 示例 + +```tsx | pure +class SqlCollectionTemplate extends CollectionTemplate { + name = 'sql'; + type = 'object'; + title = '{{t("SQL collection")}}'; + configurableProperties = { + // ... + } +} + +class TreeCollectionTemplate extends CollectionTemplate { + name = 'tree'; + type = 'object'; + title = '{{t("Tree collection")}}'; + configurableProperties = { + // ... + } +} + +class MyPlugin extends Plugin { + async load() { + this.dataSourceManager.collectionTemplateManager.addCollectionTemplates([ SqlCollectionTemplate, TreeCollectionTemplate ]); + + // or + this.dataSourceManager.addCollectionTemplates([ SqlCollectionTemplate, TreeCollectionTemplate ]); + } +} +``` + +### getCollectionTemplate() + +用于获取 collection template。 + +- 类型 + +```tsx | pure +class CollectionTemplateManager { + getCollectionTemplate(name: string): CollectionTemplate; +} +``` + +- 示例 + +```tsx | pure +collectionManager.getCollectionTemplate(); // generalCollectionTemplate + +collectionManager.getCollectionTemplate('tree'); // treeCollectionTemplate +``` + +### getCollectionTemplates() + +用于获取所有 collection templates。 + +- 类型 + +```tsx | pure +class CollectionTemplateManager { + getCollectionTemplates(): CollectionTemplate[]; +} +``` + +- 示例 + +```tsx | pure +collectionManager.getCollectionTemplates(); // [ generalCollectionTemplate, treeCollectionTemplate, sqlCollectionTemplate ] +``` diff --git a/packages/core/client/docs/zh-CN/core/data-source/collection-template.md b/packages/core/client/docs/zh-CN/core/data-source/collection-template.md new file mode 100644 index 000000000..eb248e519 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/collection-template.md @@ -0,0 +1,249 @@ +# CollectionTemplate + +用于创建数据表的模板。 + +![](./images/collection-template.png) + +```ts +interface AvailableFieldInterfacesInclude { + include?: any[]; +} + +interface AvailableFieldInterfacesExclude { + exclude?: any[]; +} + +interface CollectionTemplateDefaultOptions { + /** + * 自动生成 id + * @default true + * */ + autoGenId?: boolean; + /** 创建人 */ + createdBy?: boolean; + /** 最后更新人 */ + updatedBy?: boolean; + /** 创建日期 */ + createdAt?: boolean; + /** 更新日期 */ + updatedAt?: boolean; + /** 可排序 */ + sortable?: boolean; + /* 树结构 */ + tree?: string; + /* 日志 */ + logging?: boolean; + /** 继承 */ + inherits?: string | string[]; + /* 字段列表 */ + fields?: CollectionOptions['fields']; +} + +class CollectionTemplate { + app: Application; + collectionManager: CollectionManager; + + name: string; + Collection?: typeof Collection; + transform?: (collection: CollectionOptions, app: Application) => CollectionOptions; + title?: string; + color?: string; + /** 排序 */ + order?: number; + /** 默认配置 */ + default?: CollectionTemplateDefaultOptions; + events?: any; + /** UI 可配置的 CollectionOptions 参数(添加或编辑的 Collection 表单的字段) */ + configurableProperties?: Record; + /** 当前模板可用的字段类型 */ + availableFieldInterfaces?: AvailableFieldInterfacesInclude | AvailableFieldInterfacesExclude; + /** 是否分割线 */ + divider?: boolean; + /** 模板描述 */ + description?: string; + /**配置字段中的操作按钮 */ + configureActions?: Record; + //是否禁止删除字段 + forbidDeletion?: boolean; +} +``` + +其需要结合 [CollectionManager](./collection-template-manager.md) 使用。 + +```ts +import { Plugin, Collection, CollectionTemplate } from '@nocobase/client'; + +class SqlCollection extends Collection { + otherMethods() { + // ... + } +} + +class SqlCollectionTemplate extends CollectionTemplate { + name = 'sql'; + Collection = SqlCollection; // 自定义的数据表类 + title = '{{t("SQL collection")}}'; + order = 4; + color = 'yellow'; + default = { + fields: [], + }; + configurableProperties = { + // ... + } +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.collectionTemplateManager.addCollectionTemplates([ SqlCollectionTemplate ]); + + // or + this.app.dataSourceManager.addCollectionTemplates([ SqlCollectionTemplate ]); + } +} +``` + +## 实例属性 + +### name + +模板的唯一标识符。 + + +### Collection + +模板对应的数据表类。 + +在创建数据表后,Collection 会有 [template 字段](/core/data-source/collection#collectionoptions),用于标识该数据表是由哪个模板创建的。 + +当通过 `collectionManager.addCollections()` 添加数据表对象时,会先读取 `collection.template` 字段,然后通过 `collectionManager.getCollectionTemplate(collection.template)` 获取到 `collectionTemplate`。 + +读取 `collectionTemplate.Collection` 字段,并通过 `new collectionTemplate.Collection(collection)` 创建对应的实例。 + +如果不传递 `Collection`,则会通过 `new Collection(collection)` 创建对应的实例。 + +```ts +class SqlCollection extends Collection { + otherMethods() { + // ... + } +} + +class SqlCollectionTemplate extends CollectionTemplate { + name = 'sql'; + Collection = SqlCollection; // 自定义的数据表类 + // ... +} + +const userCollection = { + name: 'users', + template: 'sql', + // ... +} + +// 内部会调用 new SqlCollection(userCollection) +``` + +### title + +模板的标题。 + +### color + +模板的颜色。 + +### order + +模板的排序。 + +### events + +- `beforeSubmit`:提交前触发 + + +### configurableProperties + +表单配置项。 + +![](./images//collection-template-form.png) + +```ts +class SqlCollectionTemplate extends CollectionTemplate { + name = 'sql', + // ... + configurableProperties = { + title: { + type: 'string', + title: '{{ t("Collection display name") }}', + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'Input', + }, + name: { + type: 'string', + title: '{{t("Collection name")}}', + required: true, + 'x-disabled': '{{ !createOnly }}', + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-validator': 'uid', + description: + "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}", + }, + // ... + }, +} +``` + +### default + +表单默认值。 + + + +## 实例方法 + +### collectionTemplate.transform(collection) + +collection 创建后,会调用该方法,用于对 collection 进行转换。 + +## Utils + +### getConfigurableProperties() + +用于获取内置的配置项字段。 + +- 类型 + +```tsx | pure +export type DefaultConfigurableKeys = + | 'name' + | 'title' + | 'inherits' + | 'category' + | 'autoGenId' + | 'createdBy' + | 'updatedBy' + | 'createdAt' + | 'updatedAt' + | 'sortable' + | 'description' + | 'moreOptions'; + +const getConfigurableProperties: (...keys: DefaultConfigurableKeys[]) => Record +``` + +- 示例 + +```tsx | pure +import { getConfigurableProperties } from '@nocobase/client'; + +const sqlCollectionTemplate = new CollectionTemplate({ + name: 'sql', + // ... + configurableProperties: { + ...getConfigurableProperties('name', 'title', 'description'), + // ... + }, +}); +``` diff --git a/packages/core/client/docs/zh-CN/core/data-source/collection.md b/packages/core/client/docs/zh-CN/core/data-source/collection.md new file mode 100644 index 000000000..5cce1e551 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/collection.md @@ -0,0 +1,293 @@ +# Collection + +Collection 数据表类,其被 [CollectionManager](/core/data-source/collection-manager) 管理。 + +## 类型 + +```tsx | pure +interface CollectionOptions { + name: string; + title?: string; + fields?: FieldOptions[]; + // .... +} + +class Collection { + app: Application; + collectionManager: CollectionManager; + + constructor(options: CollectionOptions) {} + + name: string; + primaryKey: string; + titleField: string; + + getOptions(): CollectionOptions; + setOptions(options: CollectionOptions): void; + getOption(key: K): CollectionOptions[K]; + + getFields(predicate?: CollectionFieldOptions | ((collection: CollectionFieldOptions) => boolean) | keyof CollectionFieldOptions): any[] + getField(name: SchemaKey): CollectionFieldOptions + hasField(name: SchemaKey): boolean; +} +``` + +```tsx | pure +const usersCollection = new Collection({ + name: 'users', + title: 'Users', + fields: [ + { + type: 'string', + name: 'username', + }, + { + type: 'integer', + name: 'age', + }, + ], +}); +``` + +## CollectionOptions + +```tsx | pure +export interface CollectionOptions { + name: string; + title?: string; + dataSource?: string; + /** + * Used for @nocobase/plugin-duplicator + * @see packages/core/database/src/collection-group-manager.tss + * + * @prop {'required' | 'optional' | 'skip'} dumpable - Determine whether the collection is dumped + * @prop {string[] | string} [with] - Collections dumped with this collection + * @prop {any} [delayRestore] - A function to execute after all collections are restored + */ + duplicator?: + | dumpable + | { + dumpable: dumpable; + with?: string[] | string; + delayRestore?: any; + }; + + tableName?: string; + inherits?: string[] | string; + inherit?: string; + key?: string; + viewName?: string; + writableView?: boolean; + + filterTargetKey?: string; + fields?: CollectionFieldOptions[]; + model?: any; + repository?: any; + sortable?: CollectionSortable; + /** + * @default true + */ + autoGenId?: boolean; + /** + * @default 'options' + */ + magicAttribute?: string; + + tree?: string; + + template?: string; + + isThrough?: boolean; + autoCreate?: boolean; + resource?: string; + collectionName?: string; + sourceKey?: string; + uiSchema?: any; + [key: string]: any; +} +``` + +- name: Collection 的标识,必须唯一。 + +- title: Collection 的标题,用于显示。 + +- fields: 字段列表,详细说明请查看 [CollectionField](/core/data-source/collection-field) + +- template: 模板标识,用于标识该 Collection 是由哪个模板创建的,详细说明请查看 [CollectionTemplate](/core/data-source/collection-template) + +- dataSource: 数据源标识,用于标识该 Collection 是由哪个数据源创建的,详细说明请查看 [CollectionDataSource](/core/data-source/collection-manager#datasource) + +- duplicator + +- tableName + +- inherits + +- viewName + +- writableView + +- filterTargetKey + +- model + +- repository + + +## 实例属性 + +### collection.collectionManager + +[CollectionManager](/core/data-source/collection-manager) 的实例。 + +### collection.titleFieldName + +标题字段的 name 属性。 + +### 其他属性 + +其他属性同 [CollectionOptions](/core/data-source/collection#collectionoptions)。 + +## 实例方法 + +### collection.getOptions() + +获取 collection 的所有配置项。 + +- 类型 + +```tsx | pure +class Collection { + getOptions(): CollectionOptions; +} +``` + +- 示例 + +```tsx | pure +const usersCollection = new Collection({ + name: 'users', + title: 'Users', + fields: [ + // ... + ], +}); + +console.log(usersCollection.getOptions()); // { name: 'users', title: 'Users', fields: [ ] } +``` + +### collection.setOptions(options) + +设置 collection 的配置项,最终会和默认配置项进行合并。 + +- 类型 + +```tsx | pure +class Collection { + setOptions(options: CollectionOptions): void; +} +``` + +- 示例 + +```tsx | pure +collection.setOptions({ + name: 'users', + title: 'Users', + fields: [ + // ... + ], +}); +``` + +### collection.getOption(key) + +获取 collection 的单个配置项。 + +- 类型 + +```tsx | pure +class Collection { + getOption(key: K): CollectionOptions[K]; +} +``` + +- 示例 + +```tsx | pure +collection.getOption('name'); // 'users' +collection.getOption('title'); // 'Users' +``` + +### collection.getFields(predicate?) + +获取 collection 的字段列表。 + +- 类型 + +```tsx | pure +class Collection { + getFields(predicate?: CollectionFieldOptions | ((collection: CollectionFieldOptions) => boolean) | keyof CollectionFieldOptions): any[] +} +``` + +- 详解 + - predicate + - 类型 + - `CollectionFieldOptions` + - `(collection: CollectionFieldOptions) => boolean` + - `keyof CollectionFieldOptions` + - 说明 + - 如果传递了 `predicate`,则返回符合条件的字段列表 + - 如果没有传递 `predicate`,则返回所有字段列表 + +`predicate` 的使用可看参考 [lodash.filter](https://www.lodashjs.com/docs/lodash.filter)。 + +- 示例 + +```tsx | pure +collection.getFields(); // [{ name: 'username', type: 'string', primaryKey: true }, { name: 'age', type: 'integer' }] + +collection.getFields({ name: 'age' }); // [{ name: 'age', type: 'integer' }] + +collection.getFields('primaryKey'); // [{ name: 'username', type: 'string', primaryKey: true }] + +collection.getFields(field => field.type === 'string'); // [{ name: 'name', type: 'string' }] +``` + +### collection.getField(name) + +获取 collection 的单个字段。 + +- 类型 + +```tsx | pure +class Collection { + getField(name: SchemaKey): CollectionFieldOptions +} +``` + +- 示例 + +```tsx | pure +collection.getField('username'); // { name: 'username', type: 'string', primaryKey: true } +``` + +### collection.hasField(name) + +判断 collection 是否存在某个字段。 + +- 类型 + +```tsx | pure +class Collection { + hasField(name: SchemaKey): boolean; +} +``` + +- 示例 + +```tsx | pure +collection.hasField('username'); // true +collection.hasField('name'); // false +``` diff --git a/packages/core/client/docs/zh-CN/core/data-source/data-source-manager-provider.md b/packages/core/client/docs/zh-CN/core/data-source/data-source-manager-provider.md new file mode 100644 index 000000000..4b39e452c --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/data-source-manager-provider.md @@ -0,0 +1,25 @@ +# DataSourceManagerProvider + +用于提供 `DataSourceManager` 实例。 + + +## Hooks + +### useDataSourceManager() + +获取 `DataSourceManager` 实例。 + +- 类型 + +```tsx | pure +function useDataSourceManager(): DataSourceManager; +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSourceManager = useDataSourceManager(); + dataSourceManager.removeDataSources(['my-data-source']); +} +``` diff --git a/packages/core/client/docs/zh-CN/core/data-source/data-source-manager.md b/packages/core/client/docs/zh-CN/core/data-source/data-source-manager.md new file mode 100644 index 000000000..fd4b43c05 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/data-source-manager.md @@ -0,0 +1,331 @@ +# DataSourceManager + +Nocobase 数据源前端体系包含如下内容: + +- [DataSourceManager](./data-source-manager.md) + - [DataSource](./data-source) + - [CollectionManager](./collection-manager) + - [Collection](./collection) + - [Field](./collection-field) + - [CollectionTemplateManager](./collection-template-manager) + - [CollectionTemplate](./collection-template) + - [CollectionFieldInterfaceManager](./collection-field-interface-manager) + - [CollectionFieldInterface](./collection-field-interface) + - [CollectionMixins](./collection-mixins) + + +## 实例属性 + +- collectionTemplateManager + +用于管理 `CollectionTemplate` 实例。 + +```tsx | pure +import { Plugin, CollectionTemplate } from '@nocobase/client'; + +class MyCollectionTemplate extends CollectionTemplate { + name = 'my-collection-template'; +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.collectionTemplateManager.addCollectionTemplates([MyCollectionTemplate]) + } +} +``` + +详细请参考:[CollectionTemplateManager](./collection-template-manager) + +- collectionFieldInterfaceManager + +用于管理 `CollectionFieldInterface` 实例。 + +```tsx | pure +import { Plugin, CollectionFieldInterface } from '@nocobase/client'; + +class MyCollectionFieldInterface extends CollectionFieldInterface { + name = 'my-collection-field-interface'; +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.collectionFieldInterfaceManager.addFieldInterfaces([MyCollectionFieldInterface]) + } +} +``` + +详细请参考:[CollectionTemplateManager](./collection-template-manager) + +## 实例方法 + +### addCollectionTemplates() + +是 `CollectionTemplateManager` 的快捷方法,用于添加 `CollectionTemplate`。 + +- 类型 + +```tsx | pure +class DataSourceManager { + addCollectionTemplates(templates: CollectionTemplate[]): void; +} +``` + +- 示例 + +```tsx | pure +import { Plugin, CollectionTemplate } from '@nocobase/client'; + +class MyCollectionTemplate extends CollectionTemplate { + name = 'my-collection-template'; +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.collectionTemplateManager.addCollectionTemplates([MyCollectionTemplate]) + } +} +``` + +更多详细请参考:[CollectionTemplateManager](./collection-template-manager) + +### addFieldInterfaces() + +是 `CollectionFieldInterfaceManager` 的快捷方法,用于添加 `CollectionFieldInterface`。 + +- 类型 + +```tsx | pure +class DataSourceManager { + addFieldInterfaces(fieldInterfaces: CollectionFieldInterface[]): void; +} +``` + +- 示例 + +```tsx | pure +import { Plugin, CollectionFieldInterface } from '@nocobase/client'; + +class MyCollectionFieldInterface extends CollectionFieldInterface { + name = 'my-collection-field-interface'; +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.collectionFieldInterfaceManager.addFieldInterfaces([MyCollectionFieldInterface]) + } +} +``` + +更多详细请参考:[CollectionFieldInterfaceManager](./collection-field-interface-manager) + +### addCollectionMixins() + +用于添加 `Collection` 的 Mixins。 + +- 类型 + +```tsx | pure +class DataSourceManager { + addCollectionMixins(mixins: (typeof Collection)[]): void; +} +``` + +- 示例 + +```tsx | pure +import { Plugin, Collection } from '@nocobase/client'; + +class MyCollectionMixin extends Collection { + otherMethod() { + console.log('otherMethod'); + } +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.addCollectionMixins([MyCollectionMixin]) + } +} + +const MyComponent = () => { + const collection = useCollection(); + collection.otherMethod(); +} +``` + +更多详细请参考:[CollectionMixins](./collection-mixins) + +### addDataSource() + +用于添加 `DataSource`。 + +- 类型 + +```tsx | pure +class DataSourceManager { + addDataSource(DataSource: DataSource, options: DataSourceOptions): void; +} +``` + +- 示例 + +```tsx | pure +import { Plugin, DataSource, DataSourceOptions } from '@nocobase/client'; + +class MyDataSource extends DataSource { + async getDataSource() { + return { + status: 'loaded', + collections: [{ name: 'users' }] + } + } +} + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.addDataSource(MyDataSource, { + key: 'my-data-source', + displayName: 'My Data Source', + }) + } +} +``` + +更多详细请参考:[DataSource](./data-source) + +### removeDataSources() + +移除 `DataSource`。 + +- 类型 + +```tsx | pure +class DataSourceManager { + removeDataSources(keys: string[]): void; +} +``` + +- 示例 + +```tsx | pure + +const MyComponent = () => { + const dataSourceManager = useDataSourceManager(); + dataSourceManager.removeDataSources(['my-data-source']); +} +``` + +### getDataSources() + +获取全部 `DataSource` 实例列表。 + +- 类型 + +```tsx | pure +class DataSourceManager { + getDataSources(): DataSource[]; +} +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSourceManager = useDataSourceManager(); + const dataSources = dataSourceManager.getDataSources(); + + return ( +
+ {dataSources.map(dataSource => ( +
{dataSource.displayName}
+ ))} +
+ ) +} +``` + +### getDataSource() + +获取 `DataSource` 实例。 + +- 类型 + +```tsx | pure +class DataSourceManager { + getDataSource(key: string): DataSource; +} +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSourceManager = useDataSourceManager(); + const dataSource = dataSourceManager.getDataSource('my-data-source'); + + return ( +
+ {dataSource.displayName} +
+ ) +} +``` + +### getAllCollections() + +获取所有 DataSource 的所有 Collection 实例。 + +- 类型 + +```tsx | pure + +class DataSourceManager { + getAllCollections(predicate?: (collection: Collection) => boolean): (DataSourceOptions & { collections: Collection[] })[]; +} +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSourceManager = useDataSourceManager(); + const collections = dataSourceManager.getAllCollections(); + + return ( +
+ {collections.map(({ key, displayName, collections }) => ( +
+

{displayName}

+
    + {collections.map(collection => ( +
  • {collection.name}
  • + ))} +
+
+ ))} +
+ ) +} +``` + +### reload() + +重载所有 `DataSource`。 + +- 类型 + +```tsx | pure +class DataSourceManager { + reload(): Promise; +} +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSourceManager = useDataSourceManager(); + dataSourceManager.reload(); +} +``` + diff --git a/packages/core/client/docs/zh-CN/core/data-source/data-source-provider.md b/packages/core/client/docs/zh-CN/core/data-source/data-source-provider.md new file mode 100644 index 000000000..909a4d894 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/data-source-provider.md @@ -0,0 +1,66 @@ +# DataSourceProvider + +用于提供 `DataSource` 的实例。 + +## 组件 + +- 类型 + +```tsx | pure +interface DataSourceProviderProps { + dataSource?: string; + children?: ReactNode; +} +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + return ( + + + + ); +} +``` + +## Hooks + +### useDataSource() + +获取 `DataSource` 实例。 + +- 类型 + +```tsx | pure +function useDataSource(): DataSource; +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSource = useDataSource(); + return
{dataSource.displayName}
+} +``` + +### useDataSourceKey() + +获取 `DataSource` 的 key。 + +- 类型 + +```tsx | pure +function useDataSourceKey(): string; +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSourceKey = useDataSourceKey(); + return
{dataSourceKey}
+} +``` diff --git a/packages/core/client/docs/zh-CN/core/data-source/data-source.md b/packages/core/client/docs/zh-CN/core/data-source/data-source.md new file mode 100644 index 000000000..558a37089 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/data-source.md @@ -0,0 +1,176 @@ +# DataSource + +主要是用于获取数据源和数据源的数据表结构列表,并在获取后交给 [CollectionManager](./collection-manager.md) 进行管理,其被 [DataSourceManager](./data-source-manager.md) 管理。 + +## 数据源定义 + +数据源的定义需要继承 `DataSource` 类,并实现 `getDataSource` 方法,当调用 `reload` 方法时,会调用 `getDataSource` 方法获取数据表结构。 + +```tsx | pure +import { DataSource } from '@nocobase/client'; + +class MyDataSource extends DataSource { + async getDataSource() { + return this.app.request({ + url: 'xxx', + method: 'GET', + }); + } +} +``` + +### 数据源注册 + +数据源需要在插件中注册,通过 `DataSourceManager` 的 `addDataSource` 方法进行注册。 + +初始化添加的时候 `collections` 可以为空,当调用 `reload` 方法时,会调用 `getDataSource` 方法获取数据表结构。 + +```tsx | pure +import { Plugin, DataSource, DataSourceOptions } from '@nocobase/client'; + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.addDataSource(MyDataSource, { + key: 'my-data-source', + displayName: 'My Data Source', + status: 'loaded', + collections: [ + { + name: 'users', + fields: [ + { + name: 'name', + type: 'string', + }, + ], + }, + ], + }); + } +} +``` + +- `key`:数据源的唯一标识 +- `displayName`:数据源的显示名称 +- `status`:数据源的状态,`loaded` 表示已加载,`loading` 表示正在加载,`loading-failed` 表示加载失败 +- `collections`:数据表结构 +- `errorMessage`:错误信息 + +## 实例方法 + +### getDataSource() + +用于获取数据源信息,其会被 `reload` 方法内部调用,外部不需要调用。 + +### addReloadCallback() + +用于添加数据源加载完成后的回调函数。 + +- 类型 + +```tsx | pure +type LoadCallback = (collections: CollectionOptions[]) => void; + +class DataSource { + addReloadCallback(callback: LoadCallback): void; +} +``` + +### removeReloadCallback() + +用于移除数据源加载完成后的回调函数。 + +- 类型 + +```tsx | pure +type LoadCallback = (collections: CollectionOptions[]) => void; +class DataSource { + removeReloadCallback(callback: LoadCallback): void; +} +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSource = useDataSource(); + + useEffect(() => { + const callback = (collections) => { + console.log(collections); + }; + dataSource.addReloadCallback(callback); + return () => { + dataSource.removeReloadCallback(callback); + }; + }, []); +}; +``` + +### reload() + +用于重新加载数据源,会调用 `getDataSource` 方法获取数据表结构,并内部调用 `addReloadCallback` 添加的回调函数。 + +- 类型 + +```tsx | pure +class DataSource { + reload(): Promise; +} +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSource = useDataSource(); + + const handleClick = async () => { + await dataSource.reload(); + }; +}; +``` + +### getOptions() + +获取数据源的配置信息。 + +- 类型 + +```tsx | pure +interface DataSourceOptions { + key: string; + displayName: string; + collections?: CollectionOptions[]; + errorMessage?: string; + status?: 'loaded' | 'loading-failed' | 'loading'; +} + +class DataSource { + getOptions(): DataSourceOptions; +} +``` + +### getOption() + +获取数据源的配置信息。 + +- 类型 + +```tsx | pure +class DataSource { + getOption(key: string): any; +} +``` + +- 示例 + +```tsx | pure +const MyComponent = () => { + const dataSource = useDataSource(); + + const handleClick = async () => { + console.log(dataSource.getOption('key')); + }; +}; +``` diff --git a/packages/core/client/docs/zh-CN/core/data-source/demos/collection-field/demo1.tsx b/packages/core/client/docs/zh-CN/core/data-source/demos/collection-field/demo1.tsx new file mode 100644 index 000000000..385554a57 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/demos/collection-field/demo1.tsx @@ -0,0 +1,136 @@ +import { ISchema, observer, useForm } from '@formily/react'; +import { + Action, + Application, + CollectionProvider, + Form, + SchemaComponent, + FormItem, + InheritanceCollectionMixin, + Input, + SchemaComponentOptions, +} from '@nocobase/client'; +import React from 'react'; + +const collection = { + name: 'tests', + fields: [ + { + type: 'string', + name: 'title1', + interface: 'input', + uiSchema: { + title: 'Title1', + type: 'string', + 'x-component': 'Input', + required: true, + description: 'description1', + } as ISchema, + }, + { + type: 'string', + name: 'title2', + interface: 'input', + uiSchema: { + title: 'Title2', + type: 'string', + 'x-component': 'Input', + description: 'description', + default: 'ttt', + }, + }, + { + type: 'string', + name: 'title3', + }, + ], +}; + +const Root = observer(() => { + const schema: ISchema = { + type: 'object', + properties: { + form1: { + type: 'void', + 'x-component': 'Form', + properties: { + // 字段 title1 直接使用全局提供的 uiSchema + title1: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + default: '111', + }, + // 等同于 + // title1: { + // type: 'string', + // title: 'Title', + // required: true, + // 'x-component': 'Input', + // 'x-decorator': 'FormItem', + // }, + title2: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + title: 'Title4', // 覆盖全局已定义的 Title2 + required: true, // 扩展的配置参数 + description: 'description4', + }, + // 等同于 + // title2: { + // type: 'string', + // title: 'Title22', + // required: true, + // 'x-component': 'Input', + // 'x-decorator': 'FormItem', + // }, + // 字段 title3 没有提供 uiSchema,自行处理 + title3: { + 'x-component': 'Input', + 'x-decorator': 'FormItem', + title: 'Title3', + required: true, + }, + action1: { + // type: 'void', + 'x-component': 'Action', + title: 'Submit', + 'x-component-props': { + type: 'primary', + useAction: '{{ useSubmit }}', + }, + }, + }, + }, + }, + }; + + const useSubmit = () => { + const form = useForm(); + return { + async run() { + form.submit(() => { + console.log(form.values); + }); + }, + }; + }; + + return ( + + + + + + ); +}); + +const app = new Application({ + providers: [Root], + components: { Action, Form, Input, FormItem }, + dataSourceManager: { + collections: [collection], + collectionMixins: [InheritanceCollectionMixin], + }, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/data-source/demos/collection-field/demo2.tsx b/packages/core/client/docs/zh-CN/core/data-source/demos/collection-field/demo2.tsx new file mode 100644 index 000000000..31df89f79 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/demos/collection-field/demo2.tsx @@ -0,0 +1,89 @@ +import { ISchema, observer } from '@formily/react'; +import { + Application, + CollectionProvider, + Form, + FormItem, + InheritanceCollectionMixin, + Input, + SchemaComponent, + SchemaSettings, +} from '@nocobase/client'; +import React from 'react'; + +const collection = { + name: 'tests', + fields: [ + { + type: 'string', + name: 'title1', + interface: 'input', + uiSchema: { + title: 'Title1', + type: 'string', + 'x-component': 'Input', + required: true, + description: 'description1', + } as ISchema, + }, + ], +}; + +const Root = observer(() => { + const schema: ISchema = { + type: 'object', + properties: { + form1: { + type: 'void', + 'x-component': 'Form', + properties: { + title1: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-settings': 'FormItemSettings', + }, + }, + }, + }, + }; + + return ( + + + + ); +}); + +const formSettings = new SchemaSettings({ + name: 'FormItemSettings', + items: [ + { + name: 'required', + type: 'switch', + useComponentProps() { + // const collectionField = useCollectionField(); // 报错 + return { + checked: true, + // checked: !!collectionField?.uiSchema?.required, + title: 'Required', + onChange(v) { + // ? + }, + }; + }, + }, + ], +}); + +const app = new Application({ + providers: [Root], + components: { Form, Input, FormItem }, + dataSourceManager: { + collections: [collection], + collectionMixins: [InheritanceCollectionMixin], + }, + schemaSettings: [formSettings], + designable: true, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/data-source/demos/collection/demo1.tsx b/packages/core/client/docs/zh-CN/core/data-source/demos/collection/demo1.tsx new file mode 100644 index 000000000..a01748165 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/demos/collection/demo1.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { CollectionProvider, useCollection } from '@nocobase/client'; +import { createApp } from '../createApp'; + +const Demo = () => { + const collection = useCollection(); + return
{JSON.stringify(collection.getField('username'), null, 2)}
; +}; + +const Root = () => { + return ( + + + + ); +}; + +export default createApp(Root); diff --git a/packages/core/client/docs/zh-CN/core/data-source/demos/collections.json b/packages/core/client/docs/zh-CN/core/data-source/demos/collections.json new file mode 100644 index 000000000..4d3aa45f8 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/demos/collections.json @@ -0,0 +1,198 @@ +[ + { + "key": "h7b9i8khc3q", + "name": "users", + "inherit": false, + "hidden": false, + "description": null, + "category": [], + "namespace": "users.users", + "duplicator": { + "dumpable": "optional", + "with": "rolesUsers" + }, + "sortable": "sort", + "model": "UserModel", + "createdBy": true, + "updatedBy": true, + "logging": true, + "from": "db2cm", + "title": "{{t(\"Users\")}}", + "rawTitle": "{{t(\"Users\")}}", + "fields": [ + { + "uiSchema": { + "type": "number", + "title": "{{t(\"ID\")}}", + "x-component": "InputNumber", + "x-read-pretty": true, + "rawTitle": "{{t(\"ID\")}}" + }, + "key": "ffp1f2sula0", + "name": "id", + "type": "bigInt", + "interface": "id", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "autoIncrement": true, + "primaryKey": true, + "allowNull": false + }, + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Nickname\")}}", + "x-component": "Input", + "rawTitle": "{{t(\"Nickname\")}}" + }, + "key": "vrv7yjue90g", + "name": "nickname", + "type": "string", + "interface": "input", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null + }, + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Username\")}}", + "x-component": "Input", + "x-validator": { + "username": true + }, + "required": true, + "rawTitle": "{{t(\"Username\")}}" + }, + "key": "2ccs6evyrub", + "name": "username", + "type": "string", + "interface": "input", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "unique": true + }, + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Email\")}}", + "x-component": "Input", + "x-validator": "email", + "required": true, + "rawTitle": "{{t(\"Email\")}}" + }, + "key": "rrskwjl5wt1", + "name": "email", + "type": "string", + "interface": "email", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "unique": true + }, + { + "key": "t09bauwm0wb", + "name": "roles", + "type": "belongsToMany", + "interface": "m2m", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "target": "roles", + "foreignKey": "userId", + "otherKey": "roleName", + "onDelete": "CASCADE", + "sourceKey": "id", + "targetKey": "name", + "through": "rolesUsers", + "uiSchema": { + "type": "array", + "title": "{{t(\"Roles\")}}", + "x-component": "AssociationField", + "x-component-props": { + "multiple": true, + "fieldNames": { + "label": "title", + "value": "name" + } + } + } + } + ] + }, + { + "key": "pqnenvqrzxr", + "name": "roles", + "inherit": false, + "hidden": false, + "description": null, + "category": [], + "namespace": "acl.acl", + "duplicator": { + "dumpable": "required", + "with": "uiSchemas" + }, + "autoGenId": false, + "model": "RoleModel", + "filterTargetKey": "name", + "sortable": true, + "from": "db2cm", + "title": "{{t(\"Roles\")}}", + "rawTitle": "{{t(\"Roles\")}}", + "fields": [ + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Role UID\")}}", + "x-component": "Input", + "rawTitle": "{{t(\"Role UID\")}}" + }, + "key": "jbz9m80bxmp", + "name": "name", + "type": "uid", + "interface": "input", + "description": null, + "collectionName": "roles", + "parentKey": null, + "reverseKey": null, + "prefix": "r_", + "primaryKey": true + }, + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Role name\")}}", + "x-component": "Input", + "rawTitle": "{{t(\"Role name\")}}" + }, + "key": "faywtz4sf3u", + "name": "title", + "type": "string", + "interface": "input", + "description": null, + "collectionName": "roles", + "parentKey": null, + "reverseKey": null, + "unique": true, + "translation": true + }, + { + "key": "1enkovm9sye", + "name": "description", + "type": "string", + "interface": null, + "description": null, + "collectionName": "roles", + "parentKey": null, + "reverseKey": null + } + ] + } +] diff --git a/packages/core/client/docs/zh-CN/core/data-source/demos/createApp.tsx b/packages/core/client/docs/zh-CN/core/data-source/demos/createApp.tsx new file mode 100644 index 000000000..127f2e7c8 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/demos/createApp.tsx @@ -0,0 +1,98 @@ +import { + Application, + ApplicationOptions, + CardItem, + Plugin, + CollectionPlugin, + DataBlockProvider, + DEFAULT_DATA_SOURCE_KEY, + DEFAULT_DATA_SOURCE_TITLE, + LocalDataSource, +} from '@nocobase/client'; +import MockAdapter from 'axios-mock-adapter'; +import { ComponentType } from 'react'; +import collections from './collections.json'; + +const defaultMocks = { + 'users:list': { + data: [ + { + id: '1', + username: 'jack', + nickname: 'Jack Ma', + email: 'test@gmail.com', + }, + { + id: '2', + username: 'jim', + nickname: 'Jim Green', + }, + { + id: '3', + username: 'tom', + nickname: 'Tom Cat', + email: 'tom@gmail.com', + }, + ], + }, + 'roles:list': { + data: [ + { + name: 'root', + title: 'Root', + description: 'Root', + }, + { + name: 'admin', + title: 'Admin', + description: 'Admin description', + }, + ], + }, +}; + +export function createApp( + Demo: ComponentType, + options: ApplicationOptions = {}, + mocks: Record = defaultMocks, +) { + class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.addDataSource(LocalDataSource, { + key: DEFAULT_DATA_SOURCE_KEY, + displayName: DEFAULT_DATA_SOURCE_TITLE, + collections: collections as any, + }); + } + } + const app = new Application({ + apiClient: { + baseURL: 'http://localhost:8000', + }, + providers: [Demo], + ...options, + components: { + ...options.components, + DataBlockProvider, + CardItem, + }, + plugins: [CollectionPlugin, MyPlugin, ...(options.plugins || [])], + designable: true, + }); + + const mock = new MockAdapter(app.apiClient.axios); + + Object.entries(mocks).forEach(([url, data]) => { + mock.onGet(url).reply(async (config) => { + const res = typeof data === 'function' ? data(config) : data; + return [200, res]; + }); + mock.onPost(url).reply(async (config) => { + const res = typeof data === 'function' ? data(config) : data; + return [200, res]; + }); + }); + + const Root = app.getRootComponent(); + return Root; +} diff --git a/packages/core/client/docs/zh-CN/core/data-source/demos/data-source-manager/mixins.tsx b/packages/core/client/docs/zh-CN/core/data-source/demos/data-source-manager/mixins.tsx new file mode 100644 index 000000000..f2f26151f --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/demos/data-source-manager/mixins.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { CollectionProvider, Collection, useCollection, Plugin } from '@nocobase/client'; +import { createApp } from '../createApp'; + +class TestMixin extends Collection { + test() { + const { name } = this.options; + return 'test ' + name; + } +} + +class Test2Mixin extends Collection { + test2() { + const { name } = this.options; + return 'test2 ' + name; + } +} + +const Demo = () => { + const collection = useCollection(); + return ( +
+
test: {collection.test()}
+
test2: {collection.test2()}
+
fields.length: {collection.getFields().length}
+
+ ); +}; + +const Root = () => { + return ( + + + + ); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.addCollectionMixins([TestMixin, Test2Mixin]); + } +} + +export default createApp(Root, { + plugins: [MyPlugin], +}); diff --git a/packages/core/client/docs/zh-CN/core/data-source/extend-collections-provider.md b/packages/core/client/docs/zh-CN/core/data-source/extend-collections-provider.md new file mode 100644 index 000000000..8b033ff0f --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/extend-collections-provider.md @@ -0,0 +1,79 @@ +# ExtendCollectionsProvider + +用于扩展 [Collection](./collection.md) 。 + +## 组件 + +- 类型 + +```tsx | pure +interface ExtendCollectionsProviderProps { + collections: CollectionOptions[]; + children?: ReactNode; +} +``` + +- 示例 + +```tsx | pure +import { ExtendCollectionsProvider, CollectionOptions } from '@nocobase/client'; + +const userCollection: CollectionOptions = { + name: 'users', + title: '{{t("Users")}}', + fields: [ + { + type: 'string', + name: 'name', + title: '{{t("Name")}}', + }, + { + type: 'string', + name: 'email', + title: '{{t("Email")}}', + }, + ], +} + +const MyPlugin = () => { + return ( + + + + + + ); +} + +const ChildComponent = () => { + const collection = useCollection(); + + return ( +
+

{collection.name}

+
+ ); +} +``` + +## Hooks + +### useExtendCollections() + +获取扩展的数据表。 + +```tsx | pure +import { useExtendCollections } from '@nocobase/client'; + +const MyComponent = () => { + const collections = useExtendCollections(); + + return ( +
+ {collections.map((collection) => ( +
{collection.title}
+ ))} +
+ ); +} +``` diff --git a/packages/core/client/docs/zh-CN/core/data-source/images/collection-field-interface-form.png b/packages/core/client/docs/zh-CN/core/data-source/images/collection-field-interface-form.png new file mode 100644 index 000000000..9a6029e8a Binary files /dev/null and b/packages/core/client/docs/zh-CN/core/data-source/images/collection-field-interface-form.png differ diff --git a/packages/core/client/docs/zh-CN/core/data-source/images/collection-field-interface.png b/packages/core/client/docs/zh-CN/core/data-source/images/collection-field-interface.png new file mode 100644 index 000000000..6d04d0e2b Binary files /dev/null and b/packages/core/client/docs/zh-CN/core/data-source/images/collection-field-interface.png differ diff --git a/packages/core/client/docs/zh-CN/core/data-source/images/collection-template-form.png b/packages/core/client/docs/zh-CN/core/data-source/images/collection-template-form.png new file mode 100644 index 000000000..6223be103 Binary files /dev/null and b/packages/core/client/docs/zh-CN/core/data-source/images/collection-template-form.png differ diff --git a/packages/core/client/docs/zh-CN/core/data-source/images/collection-template.png b/packages/core/client/docs/zh-CN/core/data-source/images/collection-template.png new file mode 100644 index 000000000..45a9de527 Binary files /dev/null and b/packages/core/client/docs/zh-CN/core/data-source/images/collection-template.png differ diff --git a/packages/core/client/docs/zh-CN/core/data-source/images/field-groups.png b/packages/core/client/docs/zh-CN/core/data-source/images/field-groups.png new file mode 100644 index 000000000..3f9381a70 Binary files /dev/null and b/packages/core/client/docs/zh-CN/core/data-source/images/field-groups.png differ diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-basic.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-basic.tsx index 40a0c5a29..e285d6ccd 100644 --- a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-basic.tsx +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-basic.tsx @@ -54,11 +54,14 @@ const myInitializer = new SchemaInitializer({ ], }); -const AddBlockButton = observer(() => { - const fieldSchema = useFieldSchema(); - const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); - return render(); -}); +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); const Page = observer( (props) => { diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-component.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-component.tsx index 642705068..4fe9c9f7b 100644 --- a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-component.tsx +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-component.tsx @@ -62,11 +62,14 @@ const myInitializer = new SchemaInitializer({ ], }); -const AddBlockButton = observer(() => { - const fieldSchema = useFieldSchema(); - const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); - return render(); -}); +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); const Page = observer( (props) => { diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-items.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-items.tsx index 9c7ae48c3..33ca587a9 100644 --- a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-items.tsx +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-items.tsx @@ -75,11 +75,14 @@ const myInitializer = new SchemaInitializer({ ], }); -const AddBlockButton = observer(() => { - const fieldSchema = useFieldSchema(); - const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); - return render(); -}); +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); const Page = observer( (props) => { diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-popover.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-popover.tsx index a5890ba9f..3db26240b 100644 --- a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-popover.tsx +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-popover.tsx @@ -43,11 +43,14 @@ const myInitializer = new SchemaInitializer({ Component: MyInitializerComponent, }); -const AddBlockButton = observer(() => { - const fieldSchema = useFieldSchema(); - const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); - return render(); -}); +const AddBlockButton = observer( + () => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); + }, + { displayName: 'AddBlockButton' }, +); const Page = observer( (props) => { diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-options-item-children.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-options-item-children.tsx index 45fba8c3b..102c312b2 100644 --- a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-options-item-children.tsx +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-options-item-children.tsx @@ -34,7 +34,7 @@ const mySettings = new SchemaSettings({ ], }, { - name: 'a', + name: 'b', type: 'itemGroup', componentProps: { title: '动态 children', @@ -44,7 +44,9 @@ const mySettings = new SchemaSettings({ { name: 'a1', type: 'item', - title: 'A 1', + componentProps: { + title: 'A 1', + }, onClick: () => { alert('a-1'); }, @@ -52,7 +54,9 @@ const mySettings = new SchemaSettings({ { name: 'a2', type: 'item', - title: 'A 2', + componentProps: { + title: 'A 2', + }, }, ]; }, diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/data/index.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/index.md index a88afb3b9..d47422275 100644 --- a/packages/core/client/docs/zh-CN/ui-schema/blocks/data/index.md +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/index.md @@ -13,7 +13,7 @@ collection 示例 } ``` -association 示例 +association 示例 ```js { @@ -41,12 +41,12 @@ association 示例 - collection、association、action 是配置里存好的 - sourceId、filterByTk 由上下文提供 -## RecordProvider +## CollectionRecordProvider Record ```ts -class Record { +class CollectionRecord { protected current = {}; protected parent?: Record; public isNew = false; @@ -63,69 +63,69 @@ a:list(**list 外层不套空 RecordPicker**) ```jsx | pure {list.map((item) => { - const record = new Record({ current: item }); - return + const record = new CollectionRecord({ current: item }); + return })} ``` a:get(view、edit) ```jsx | pure -const record = new Record({ current: item }); - +const record = new CollectionRecord({ current: item }); + ``` a:create ```jsx | pure -const record = new Record({ current, isNew: true }); - +const record = new CollectionRecord({ current, isNew: true }); + ``` a.b:list ```jsx | pure - + {list.map((item) => { - const recordB = new Record({ + const recordB = new CollectionRecord({ current: item, }); - return + return })} - + ``` a.b:get ```jsx | pure -const recordA = new Record({ +const recordA = new CollectionRecord({ current: itemA, }); -const recordB = new Record({ +const recordB = new CollectionRecord({ current: itemB, parent: recordA, }); // 或者 recordB.setParent(recordA); - - - + + + ``` a.b:create ```jsx | pure -const recordA = new Record({ +const recordA = new CollectionRecord({ current: itemA, }); -const recordB = new Record({ +const recordB = new CollectionRecord({ isNew: true, parent: recordA, }); - - - + + + ``` ## 区块 @@ -178,9 +178,9 @@ DataBlockProvider 内容 - + {props.children} - + @@ -189,7 +189,7 @@ DataBlockProvider 内容 有当前父级记录的区块,如对多关系的表格区块,区块本身不套 RecordPicker ```tsx | pure - + @@ -208,7 +208,7 @@ DataBlockProvider 内容
- + ``` DataBlockProvider 内容 @@ -228,14 +228,14 @@ DataBlockProvider 内容 有父级记录也有当前记录的区块,如关系的表单 ```tsx | pure - +
-
+ ``` DataBlockProvider 内容 @@ -245,9 +245,9 @@ DataBlockProvider 内容 - + {props.children} - + diff --git a/packages/core/client/package.json b/packages/core/client/package.json index 513d1af2e..8d2a3983d 100644 --- a/packages/core/client/package.json +++ b/packages/core/client/package.json @@ -6,6 +6,7 @@ "module": "es/index.mjs", "types": "es/index.d.ts", "dependencies": { + "@ahooksjs/use-url-state": "3.5.1", "@ant-design/cssinjs": "^1.11.1", "@ant-design/icons": "^5.1.4", "@ant-design/pro-layout": "^7.16.11", diff --git a/packages/core/client/src/acl/ACLProvider.tsx b/packages/core/client/src/acl/ACLProvider.tsx index 52074922c..21e9cc5aa 100644 --- a/packages/core/client/src/acl/ACLProvider.tsx +++ b/packages/core/client/src/acl/ACLProvider.tsx @@ -2,14 +2,16 @@ import { Field } from '@formily/core'; import { Schema, useField, useFieldSchema } from '@formily/react'; import React, { createContext, useContext, useEffect } from 'react'; import { Navigate } from 'react-router-dom'; +import { omit } from 'lodash'; import { useAPIClient, useRequest } from '../api-client'; import { useAppSpin } from '../application/hooks/useAppSpin'; import { useBlockRequestContext } from '../block-provider/BlockProvider'; -import { useCollection, useCollectionManager } from '../collection-manager'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../collection-manager'; import { useResourceActionContext } from '../collection-manager/ResourceActionProvider'; import { useRecord } from '../record-provider'; import { SchemaComponentOptions, useDesignable } from '../schema-component'; import { useApp } from '../application'; +import { useDataSourceKey } from '../data-source/data-source/DataSourceProvider'; export const ACLContext = createContext({}); @@ -91,7 +93,9 @@ export const ACLActionParamsContext = createContext({}); export const useACLRolesCheck = () => { const ctx = useContext(ACLContext); - const data = ctx?.data?.data; + const dataSourceName = useDataSourceKey(); + const { dataSources: dataSourcesAcl } = ctx?.data?.meta || {}; + const data = { ...ctx?.data?.data, ...omit(dataSourcesAcl?.[dataSourceName], 'snippets') }; const getActionAlias = (actionPath: string) => { const actionName = actionPath.split(':').pop(); return data?.actionAlias?.[actionName] || actionName; @@ -138,8 +142,8 @@ const getIgnoreScope = (options: any = {}) => { const useAllowedActions = () => { const service = useResourceActionContext(); - const result = useBlockRequestContext() || { service }; - return result?.allowedActions ?? result?.service?.data?.meta?.allowedActions; + const result = useBlockRequestContext(); + return result?.allowedActions ?? service?.data?.meta?.allowedActions; }; const useResourceName = () => { @@ -151,7 +155,7 @@ const useResourceName = () => { export function useACLRoleContext() { const { data, getActionAlias, inResources, getResourceActionParams, getStrategyActionParams } = useACLRolesCheck(); const allowedActions = useAllowedActions(); - const { getCollectionJoinField } = useCollectionManager(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); const verifyScope = (actionName: string, recordPkValue: any) => { const actionAlias = getActionAlias(actionName); if (!Array.isArray(allowedActions?.[actionAlias])) { @@ -208,14 +212,14 @@ export const useACLActionParamsContext = () => { }; export const useRecordPkValue = () => { - const { getPrimaryKey } = useCollection(); + const { getPrimaryKey } = useCollection_deprecated(); const record = useRecord(); const primaryKey = getPrimaryKey(); return record?.[primaryKey]; }; export const ACLActionProvider = (props) => { - const { template, writableView } = useCollection(); + const { template, writableView } = useCollection_deprecated(); const recordPkValue = useRecordPkValue(); const resource = useResourceName(); const { parseAction } = useACLRoleContext(); diff --git a/packages/core/client/src/acl/ACLShortcut.tsx b/packages/core/client/src/acl/ACLShortcut.tsx index 4ef83c3e6..0ba8e1df9 100644 --- a/packages/core/client/src/acl/ACLShortcut.tsx +++ b/packages/core/client/src/acl/ACLShortcut.tsx @@ -17,7 +17,7 @@ const schema2: ISchema = { export const ACLPane = () => { return ( - + ); }; diff --git a/packages/core/client/src/acl/Configuration/ConfigureCenter.tsx b/packages/core/client/src/acl/Configuration/ConfigureCenter.tsx index d2f3769a8..9260f99a2 100644 --- a/packages/core/client/src/acl/Configuration/ConfigureCenter.tsx +++ b/packages/core/client/src/acl/Configuration/ConfigureCenter.tsx @@ -7,6 +7,7 @@ import { useRecord } from '../../record-provider'; import { useStyles } from '../style'; import { useApp } from '../../application'; import { useCompile } from '../../schema-component'; +import { omit } from 'lodash'; const getParentKeys = (tree, func, path = []) => { if (!tree) return []; @@ -43,6 +44,7 @@ export const SettingsCenterConfigure = () => { const api = useAPIClient(); const compile = useCompile(); const settings = app.pluginSettingsManager.getList(false); + console.log(settings); const allAclSnippets = app.pluginSettingsManager.getAclSnippets(); const [snippets, setSnippets] = useState([]); const allChecked = useMemo( @@ -130,7 +132,16 @@ export const SettingsCenterConfigure = () => { }, }, ]} - dataSource={settings} + dataSource={settings + .filter((v) => { + return v.isTopLevel !== false; + }) + .map((v) => { + if (v.showTabs !== false) { + return v; + } + return omit(v, 'children'); + })} /> ); }; diff --git a/packages/core/client/src/acl/Configuration/RoleConfigure.tsx b/packages/core/client/src/acl/Configuration/RoleConfigure.tsx index 011831243..b793971f5 100644 --- a/packages/core/client/src/acl/Configuration/RoleConfigure.tsx +++ b/packages/core/client/src/acl/Configuration/RoleConfigure.tsx @@ -98,14 +98,14 @@ export const RoleConfigure = () => { 'x-decorator': 'FormItem', 'x-component': 'SnippetCheckboxGroup', }, - 'strategy.actions': { - title: t('Global action permissions'), - description: t( - 'All collections use general action permissions by default; permission configured individually will override the default one.', - ), - 'x-component': 'StrategyActions', - 'x-decorator': 'FormItem', - }, + // 'strategy.actions': { + // title: t('Global action permissions'), + // description: t( + // 'All collections use general action permissions by default; permission configured individually will override the default one.', + // ), + // 'x-component': 'StrategyActions', + // 'x-decorator': 'FormItem', + // }, allowNewMenu: { title: t('Menu permissions'), 'x-decorator': 'FormItem', diff --git a/packages/core/client/src/acl/Configuration/RolesResourcesActions.tsx b/packages/core/client/src/acl/Configuration/RolesResourcesActions.tsx index 07d4ce2e5..7bab834d6 100644 --- a/packages/core/client/src/acl/Configuration/RolesResourcesActions.tsx +++ b/packages/core/client/src/acl/Configuration/RolesResourcesActions.tsx @@ -5,7 +5,7 @@ import { Checkbox, Table, Tag } from 'antd'; import { isEmpty } from 'lodash'; import React, { createContext } from 'react'; import { useTranslation } from 'react-i18next'; -import { useCollectionManager, useCompile, useRecord } from '../..'; +import { useCollectionManager_deprecated, useCompile, useRecord } from '../..'; import { useStyles } from '../style'; import { useAvailableActions } from './RoleTable'; import { ScopeSelect } from './ScopeSelect'; @@ -38,7 +38,7 @@ export const RolesResourcesActions = connect((props) => { const form = useForm(); const roleCollection = useRecord(); const availableActions = useAvailableActions(); - const { getCollection, getCollectionFields } = useCollectionManager(); + const { getCollection, getCollectionFields } = useCollectionManager_deprecated(); const collection = getCollection(roleCollection.collectionName); const collectionFields = getCollectionFields(roleCollection.collectionName); const compile = useCompile(); diff --git a/packages/core/client/src/acl/Configuration/index.tsx b/packages/core/client/src/acl/Configuration/index.tsx index 456a310c1..391deb24b 100644 --- a/packages/core/client/src/acl/Configuration/index.tsx +++ b/packages/core/client/src/acl/Configuration/index.tsx @@ -4,3 +4,6 @@ export { RolesResourcesActions } from './RolesResourcesActions'; export { RoleTable } from './RoleTable'; export { StrategyActions } from './StrategyActions'; export { SettingsCenterConfigure, SettingCenterProvider } from './ConfigureCenter'; +export * from './PermisionProvider'; +export * from './RolesResourcesActions'; +export * from './ScopeSelect'; diff --git a/packages/core/client/src/acl/Configuration/schemas/roles.ts b/packages/core/client/src/acl/Configuration/schemas/roles.ts index d28b41b87..b06999c1d 100644 --- a/packages/core/client/src/acl/Configuration/schemas/roles.ts +++ b/packages/core/client/src/acl/Configuration/schemas/roles.ts @@ -69,7 +69,7 @@ export const roleSchema: ISchema = { }, }, }, - 'x-component': 'CollectionProvider', + 'x-component': 'CollectionProvider_deprecated', 'x-component-props': { collection, }, @@ -257,15 +257,15 @@ export const roleSchema: ISchema = { }, }, }, - tab2: { - type: 'void', - title: '{{t("Action permissions")}}', - 'x-component': 'Tabs.TabPane', - 'x-component-props': {}, - properties: { - roleCollectionsSchema, - }, - }, + // tab2: { + // type: 'void', + // title: '{{t("Action permissions")}}', + // 'x-component': 'Tabs.TabPane', + // 'x-component-props': {}, + // properties: { + // roleCollectionsSchema, + // }, + // }, tab3: { type: 'void', title: '{{t("Menu permissions")}}', diff --git a/packages/core/client/src/acl/index.ts b/packages/core/client/src/acl/index.ts index 21c06ebdf..fe86e2894 100644 --- a/packages/core/client/src/acl/index.ts +++ b/packages/core/client/src/acl/index.ts @@ -1,5 +1,6 @@ export * from './ACLProvider'; export * from './ACLShortcut'; +export * from './Configuration'; import { Plugin } from '../application/Plugin'; import { diff --git a/packages/core/client/src/api-client/hooks/useRequest.ts b/packages/core/client/src/api-client/hooks/useRequest.ts index 4af956d53..04bd8ea41 100644 --- a/packages/core/client/src/api-client/hooks/useRequest.ts +++ b/packages/core/client/src/api-client/hooks/useRequest.ts @@ -1,10 +1,11 @@ import { merge } from '@formily/shared'; import { useRequest as useReq, useSetState } from 'ahooks'; -import { Options } from 'ahooks/es/useRequest/src/types'; +import { Options, Result } from 'ahooks/es/useRequest/src/types'; import { AxiosRequestConfig } from 'axios'; import cloneDeep from 'lodash/cloneDeep'; import { assign } from './assign'; import { useAPIClient } from './useAPIClient'; +import { SetState } from 'ahooks/lib/useSetState'; type FunctionService = (...args: any[]) => Promise; @@ -15,12 +16,17 @@ export type ResourceActionOptions

= { resourceOf?: any; action?: string; params?: P; + url?: string; }; -export function useRequest

( - service: AxiosRequestConfig

| ResourceActionOptions

| FunctionService, - options: Options & { uid?: string } = {}, -) { +export type UseRequestService

= AxiosRequestConfig

| ResourceActionOptions

| FunctionService; +export type UseRequestOptions = Options & { uid?: string }; +export interface UseRequestResult

extends Result { + state: any; + setState: SetState<{}>; +} + +export function useRequest

(service: UseRequestService

, options: UseRequestOptions = {}): UseRequestResult

{ // 缓存用途 const [state, setState] = useSetState({}); const api = useAPIClient(); @@ -31,9 +37,9 @@ export function useRequest

( tempService = service; } else if (service) { tempService = async (params = {}) => { - const { resource } = service as ResourceActionOptions; + const { resource, url } = service as ResourceActionOptions; let args = cloneDeep(service); - if (resource) { + if (resource || url) { args.params = args.params || {}; assign(args.params, params); } else { diff --git a/packages/core/client/src/application/Application.tsx b/packages/core/client/src/application/Application.tsx index e6c36f8c0..2629d334f 100644 --- a/packages/core/client/src/application/Application.tsx +++ b/packages/core/client/src/application/Application.tsx @@ -15,7 +15,6 @@ import { PluginManager, PluginType } from './PluginManager'; import { PluginSettingOptions, PluginSettingsManager } from './PluginSettingsManager'; import { ComponentTypeAndString, RouterManager, RouterOptions } from './RouterManager'; import { WebSocketClient, WebSocketClientOptions } from './WebSocketClient'; - import { APIClient, APIClientProvider } from '../api-client'; import { i18n } from '../i18n'; import { AppComponent, BlankComponent, defaultAppComponents } from './components'; @@ -26,6 +25,11 @@ import { compose, normalizeContainer } from './utils'; import { defineGlobalDeps } from './utils/globalDeps'; import { getRequireJs } from './utils/requirejs'; +import { type DataSourceManagerOptions, DataSourceManager } from '../data-source/data-source/DataSourceManager'; +import { DataSourceApplicationProvider } from '../data-source/components/DataSourceApplicationProvider'; +import { CollectionField } from '../data-source/collection-field/CollectionField'; +import { DataBlockProvider } from '../data-source/data-block/DataBlockProvider'; + import { AppSchemaComponentProvider } from './AppSchemaComponentProvider'; import type { Plugin } from './Plugin'; import type { RequireJS } from './utils/requirejs'; @@ -40,7 +44,7 @@ export type DevDynamicImport = (packageName: string) => Promise<{ default: typeo export type ComponentAndProps = [ComponentType, T]; export interface ApplicationOptions { name?: string; - apiClient?: APIClientOptions; + apiClient?: APIClientOptions | APIClient; ws?: WebSocketClientOptions | boolean; i18n?: i18next; providers?: (ComponentType | ComponentAndProps)[]; @@ -54,6 +58,7 @@ export interface ApplicationOptions { designable?: boolean; loadRemotePlugins?: boolean; devDynamicImport?: DevDynamicImport; + dataSourceManager?: DataSourceManagerOptions; } export class Application { @@ -64,8 +69,10 @@ export class Application { public ws: WebSocketClient; public apiClient: APIClient; public components: Record | any> = { + DataBlockProvider, ...defaultAppComponents, ...schemaInitializerComponents, + CollectionField, }; public pluginManager: PluginManager; public pluginSettingsManager: PluginSettingsManager; @@ -74,6 +81,7 @@ export class Application { public notification; public schemaInitializerManager: SchemaInitializerManager; public schemaSettingsManager: SchemaSettingsManager; + public dataSourceManager: DataSourceManager; public name: string; @@ -96,16 +104,14 @@ export class Application { this.devDynamicImport = options.devDynamicImport; this.scopes = merge(this.scopes, options.scopes); this.components = merge(this.components, options.components); - this.apiClient = new APIClient(options.apiClient); + this.apiClient = options.apiClient instanceof APIClient ? options.apiClient : new APIClient(options.apiClient); this.apiClient.app = this; this.i18n = options.i18n || i18n; - this.router = new RouterManager({ - ...options.router, - renderComponent: this.renderComponent.bind(this), - }); + this.router = new RouterManager(options.router, this); this.schemaSettingsManager = new SchemaSettingsManager(options.schemaSettings, this); this.pluginManager = new PluginManager(options.plugins, options.loadRemotePlugins, this); this.schemaInitializerManager = new SchemaInitializerManager(options.schemaInitializers, this); + this.dataSourceManager = new DataSourceManager(options.dataSourceManager, this); this.addDefaultProviders(); this.addReactRouterComponents(); this.addProviders(options.providers || []); @@ -133,6 +139,7 @@ export class Application { scope: this.scopes, }); this.use(AntdAppProvider); + this.use(DataSourceApplicationProvider, { dataSourceManager: this.dataSourceManager }); } private addReactRouterComponents() { @@ -146,10 +153,14 @@ export class Application { private addRoutes() { this.router.add('not-found', { path: '*', - Component: this.components['AppNotFound'] || BlankComponent, + Component: this.components['AppNotFound'], }); } + getCollectionManager(dataSource?: string) { + return this.dataSourceManager.getDataSource(dataSource)?.collectionManager; + } + getComposeProviders() { const Providers = compose(...this.providers)(BlankComponent); Providers.displayName = 'Providers'; @@ -217,7 +228,7 @@ export class Application { }); } loadFailed = true; - const others = error?.response?.data?.error || error?.response?.data?.errors?.[0] || error; + const others = error?.response?.data?.error || error?.response?.data?.errors?.[0] || { message: error?.message }; this.error = { code: 'LOAD_ERROR', ...others, diff --git a/packages/core/client/src/application/Plugin.ts b/packages/core/client/src/application/Plugin.ts index 853aabe1c..61c9836ce 100644 --- a/packages/core/client/src/application/Plugin.ts +++ b/packages/core/client/src/application/Plugin.ts @@ -34,6 +34,10 @@ export class Plugin { return this.app.schemaSettingsManager; } + get dataSourceManager() { + return this.app.dataSourceManager; + } + async afterAdd() {} async beforeLoad() {} diff --git a/packages/core/client/src/application/PluginManager.ts b/packages/core/client/src/application/PluginManager.ts index f020040dc..001f09e3c 100644 --- a/packages/core/client/src/application/PluginManager.ts +++ b/packages/core/client/src/application/PluginManager.ts @@ -50,7 +50,8 @@ export class PluginManager { devDynamicImport: this.app.devDynamicImport, }); for await (const [name, pluginClass] of plugins) { - await this.add(pluginClass, { name }); + const info = pluginList.find((item) => item.name === name); + await this.add(pluginClass, info); } } diff --git a/packages/core/client/src/application/PluginSettingsManager.ts b/packages/core/client/src/application/PluginSettingsManager.ts index f6e305300..8c9d2741d 100644 --- a/packages/core/client/src/application/PluginSettingsManager.ts +++ b/packages/core/client/src/application/PluginSettingsManager.ts @@ -1,5 +1,5 @@ import { set } from 'lodash'; -import { createElement } from 'react'; +import React, { createElement } from 'react'; import { Outlet } from 'react-router-dom'; import { Icon } from '../icon'; @@ -11,7 +11,7 @@ export const ADMIN_SETTINGS_PATH = '/admin/settings/'; export const SNIPPET_PREFIX = 'pm.'; export interface PluginSettingOptions { - title: string; + title: string | React.ReactElement; /** * @default Outlet */ @@ -27,8 +27,8 @@ export interface PluginSettingOptions { } export interface PluginSettingsPageType { - label?: string; - title: string; + label?: string | React.ReactElement; + title: string | React.ReactElement; key: string; icon: any; path: string; @@ -44,11 +44,9 @@ export interface PluginSettingsPageType { export class PluginSettingsManager { protected settings: Record = {}; protected aclSnippets: string[] = []; + public app: Application; - constructor( - _pluginSettings: Record, - protected app: Application, - ) { + constructor(_pluginSettings: Record, app: Application) { this.app = app; Object.entries(_pluginSettings || {}).forEach(([name, pluginSettingOptions]) => { this.add(name, pluginSettingOptions); @@ -75,8 +73,13 @@ export class PluginSettingsManager { add(name: string, options: PluginSettingOptions) { const nameArr = name.split('.'); const topLevelName = nameArr[0]; - this.settings[name] = { Component: Outlet, ...options, name, topLevelName }; - + this.settings[name] = { + ...this.settings[name], + Component: Outlet, + ...options, + name, + topLevelName: options.topLevelName || topLevelName, + }; // add children if (nameArr.length > 1) { set(this.settings, nameArr.join('.children.'), this.settings[name]); diff --git a/packages/core/client/src/application/RouterManager.tsx b/packages/core/client/src/application/RouterManager.tsx index f109805e2..588f60c4c 100644 --- a/packages/core/client/src/application/RouterManager.tsx +++ b/packages/core/client/src/application/RouterManager.tsx @@ -1,4 +1,4 @@ -import set from 'lodash/set'; +import { set, get } from 'lodash'; import React, { ComponentType } from 'react'; import { BrowserRouter, @@ -11,6 +11,7 @@ import { useRoutes, } from 'react-router-dom'; import { BlankComponent, RouterContextCleaner } from './components'; +import { Application } from './Application'; export interface BrowserRouterOptions extends Omit { type?: 'browser'; @@ -32,9 +33,12 @@ export type RenderComponentType = (Component: ComponentTypeAndString, props?: an export class RouterManager { protected routes: Record = {}; + protected options: RouterOptions; + public app: Application; - constructor(protected options: RouterOptions) { - this.options = options || {}; + constructor(options: RouterOptions = {}, app: Application) { + this.options = options; + this.app = app; } getRoutesTree(): RouteObject[] { @@ -47,7 +51,7 @@ export class RouterManager { * { a: { name: '1', children: { b: { name: '2' }, c: {name: '3'} } } } */ for (const [name, route] of Object.entries(this.routes)) { - set(routes, name.split('.').join('.children.'), route); + set(routes, name.split('.').join('.children.'), { ...get(routes, name.split('.').join('.children.')), ...route }); } /** @@ -67,7 +71,7 @@ export class RouterManager { let ele = element; if (Component) { if (typeof Component === 'string') { - ele = this.options.renderComponent ? this.options.renderComponent(Component) : Component; + ele = this.app.renderComponent(Component); } else { ele = React.createElement(Component); } @@ -99,7 +103,7 @@ export class RouterManager { } getRouterComponent() { - const { type = 'browser', ...opts } = this.options || {}; + const { type = 'browser', ...opts } = this.options; const Routers = { hash: HashRouter, browser: BrowserRouter, @@ -146,6 +150,6 @@ export class RouterManager { } } -export function createRouterManager(options?: RouterOptions) { - return new RouterManager(options); +export function createRouterManager(options?: RouterOptions, app?: Application) { + return new RouterManager(options, app); } diff --git a/packages/core/client/src/application/__tests__/Application.test.tsx b/packages/core/client/src/application/__tests__/Application.test.tsx index e13b6f4fe..2d39cb004 100644 --- a/packages/core/client/src/application/__tests__/Application.test.tsx +++ b/packages/core/client/src/application/__tests__/Application.test.tsx @@ -16,7 +16,7 @@ describe('Application', () => { }); const router: any = { type: 'memory', initialEntries: ['/'] }; - const initialProvidersLength = 6; + const initialProvidersLength = 7; it('basic', () => { const app = new Application({ router }); expect(app.i18n).toBeDefined(); @@ -242,8 +242,7 @@ describe('Application', () => { expect(screen.getByText('AboutComponent')).toBeInTheDocument(); }); - // TODO: 会一直 loading,暂时不知道怎么解决,先跳过 - it.skip('mount', async () => { + it('mount', async () => { const Hello = () =>

Hello
; const app = new Application({ router, diff --git a/packages/core/client/src/application/__tests__/Plugin.test.ts b/packages/core/client/src/application/__tests__/Plugin.test.ts index 6f33063d0..836a4ff78 100644 --- a/packages/core/client/src/application/__tests__/Plugin.test.ts +++ b/packages/core/client/src/application/__tests__/Plugin.test.ts @@ -4,13 +4,6 @@ import { Application } from '../Application'; import { Plugin } from '../Plugin'; describe('Plugin', () => { - beforeAll(() => { - const mock = new MockAdapter(axios); - mock.onGet('pm:listEnabled').reply(200, { - data: [], - }); - }); - it('lifecycle', async () => { const afterAdd = vitest.fn(); const beforeLoad = vitest.fn(); @@ -66,7 +59,64 @@ describe('PluginManager', () => { expect(fn2).toBeCalledWith(config); }); - it('dynamic Plugins', async () => { + it('remote plugins', async () => { + const mock = new MockAdapter(axios); + mock.onGet('pm:listEnabled').reply(200, { + data: [ + { + name: '@nocobase/demo', + packageName: '@nocobase/demo', + url: 'https://demo1.com', + }, + { + name: '@nocobase/demo2', + packageName: '@nocobase/demo2', + url: 'https://demo2.com', + }, + ], + }); + + // mock requirejs + const remoteFn = vi.fn(); + + const demo1Mock = vi.fn(); + const demo2Mock = vi.fn(); + class Demo1Plugin extends Plugin { + async load() { + demo1Mock(); + } + } + + class Demo2Plugin extends Plugin { + async load() { + demo2Mock(); + } + } + + const mockPluginsModules = (pluginData, resolve) => { + remoteFn(); + resolve({ default: Demo1Plugin }, { default: Demo2Plugin }); + }; + + const requirejs: any = { + requirejs: mockPluginsModules, + }; + + requirejs.requirejs.config = vi.fn(); + requirejs.requirejs.requirejs = vi.fn(); + + const app = new Application({ + loadRemotePlugins: true, + }); + app.requirejs = requirejs; + + await app.load(); + + expect(remoteFn).toBeCalledTimes(1); + expect(demo1Mock).toBeCalledTimes(1); + }); + + it('Load other plugins through plugins', async () => { const fn2 = vitest.fn(); const config = { a: 1 }; class Demo2 extends Plugin { @@ -93,6 +143,10 @@ describe('PluginManager', () => { async afterAdd() { expect(this.pm).toBe(this.app.pm); expect(this.router).toBe(this.app.router); + expect(this.pluginManager).toBe(this.app.pluginManager); + expect(this.pluginSettingsManager).toBe(this.app.pluginSettingsManager); + expect(this.schemaInitializerManager).toBe(this.app.schemaInitializerManager); + expect(this.schemaSettingsManager).toBe(this.app.schemaSettingsManager); } } const app = new Application({ plugins: [[DemoPlugin, { name: 'demo' }]] }); diff --git a/packages/core/client/src/application/__tests__/SettingsCenter.test.ts b/packages/core/client/src/application/__tests__/PluginSettingsManager.test.ts similarity index 81% rename from packages/core/client/src/application/__tests__/SettingsCenter.test.ts rename to packages/core/client/src/application/__tests__/PluginSettingsManager.test.ts index 45728141e..f001237d8 100644 --- a/packages/core/client/src/application/__tests__/SettingsCenter.test.ts +++ b/packages/core/client/src/application/__tests__/PluginSettingsManager.test.ts @@ -55,6 +55,17 @@ describe('PluginSettingsManager', () => { expect(list[0]).toMatchObject(getRes); }); + it('constructor init', () => { + const app = new Application({ + pluginSettings: { + test: test, + }, + }); + const name = 'test'; + const settingRes = { ...test, name }; + expect(app.pluginSettingsManager.getSetting('test')).toMatchObject(settingRes); + }); + it('multi', () => { app.pluginSettingsManager.add('test1', test1); app.pluginSettingsManager.add('test2', test2); @@ -85,11 +96,20 @@ describe('PluginSettingsManager', () => { expect(app.pluginSettingsManager.getList().length).toBe(0); }); + it('has', () => { + app.pluginSettingsManager.add('test', test); + expect(app.pluginSettingsManager.has('test')).toBeTruthy(); + expect(app.pluginSettingsManager.has('test1')).toBeFalsy(); + }); + it('acl', () => { app.pluginSettingsManager.setAclSnippets(['!pm.test']); app.pluginSettingsManager.add('test', test); + expect(app.pluginSettingsManager.get('test')).toBeFalsy(); expect(app.pluginSettingsManager.hasAuth('test')).toBeFalsy(); + expect(app.pluginSettingsManager.has('test')).toBeFalsy(); + expect(app.pluginSettingsManager.get('test', false)).toMatchObject({ ...test, isAllow: false, @@ -101,13 +121,7 @@ describe('PluginSettingsManager', () => { }); }); - it('has', () => { - app.pluginSettingsManager.add('test', test); - expect(app.pluginSettingsManager.has('test')).toBeTruthy(); - expect(app.pluginSettingsManager.has('test1')).toBeFalsy(); - }); - - it('getAclSnippet', () => { + it('getAclSnippet()', () => { app.pluginSettingsManager.add('test1', test1); app.pluginSettingsManager.add('test2', { ...test2, @@ -117,14 +131,21 @@ describe('PluginSettingsManager', () => { expect(app.pluginSettingsManager.getAclSnippet('test2')).toBe('any.string'); }); - it('getRouteName', () => { + it('getAclSnippets()', () => { + app.pluginSettingsManager.add('test', test); + app.pluginSettingsManager.add('test1', test1); + + expect(app.pluginSettingsManager.getAclSnippets()).toEqual(['pm.test', 'pm.test1']); + }); + + it('getRouteName()', () => { app.pluginSettingsManager.add('test1', test1); app.pluginSettingsManager.add('test1.test2', test2); expect(app.pluginSettingsManager.getRouteName('test1')).toBe('admin.settings.test1'); expect(app.pluginSettingsManager.getRouteName('test1.test2')).toBe('admin.settings.test1.test2'); }); - it('getRoutePath', () => { + it('getRoutePath()', () => { app.pluginSettingsManager.add('test1', test1); app.pluginSettingsManager.add('test1.test2', test2); expect(app.pluginSettingsManager.getRoutePath('test1')).toBe('/admin/settings/test1'); @@ -142,4 +163,18 @@ describe('PluginSettingsManager', () => { } `); }); + + it('When icon is a string, it will be converted to the Icon component', () => { + const name = 'test'; + const icon = 'test-icon'; + app.pluginSettingsManager.add(name, { + ...test, + icon, + }); + expect(app.pluginSettingsManager.get(name).icon).toMatchInlineSnapshot(` + + `); + }); }); diff --git a/packages/core/client/src/application/__tests__/RouterManager.test.tsx b/packages/core/client/src/application/__tests__/RouterManager.test.tsx index b2085dc1c..b711fa4b2 100644 --- a/packages/core/client/src/application/__tests__/RouterManager.test.tsx +++ b/packages/core/client/src/application/__tests__/RouterManager.test.tsx @@ -1,11 +1,11 @@ -import { render, screen, userEvent } from '@nocobase/test/client'; +import { render, screen, userEvent, sleep, waitFor } from '@nocobase/test/client'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import React, { FC } from 'react'; import { Link, Outlet } from 'react-router-dom'; import { beforeAll } from 'vitest'; import { Application } from '../Application'; -import { RouteType, RouterManager } from '../RouterManager'; +import { RouteType, RouterManager, createRouterManager } from '../RouterManager'; describe('Router', () => { let app: Application; @@ -21,7 +21,7 @@ describe('Router', () => { let router: RouterManager; beforeEach(() => { - router = new RouterManager({ type: 'memory', initialEntries: ['/'] }); + router = new RouterManager({ type: 'memory', initialEntries: ['/'] }, app); }); it('basic', () => { @@ -39,8 +39,26 @@ describe('Router', () => { }; router.add('test2', route2); + expect(router.getRoutesTree()).toHaveLength(2); - expect(router.getRoutesTree()).toEqual([route1, route2]); + expect(router.getRoutesTree()).toMatchObject([route1, route2]); + expect(router.getRoutes()).toEqual({ + test: route1, + test2: route2, + }); + expect(router.get('test')).toEqual(route1); + expect(router.get('test2')).toEqual(route2); + expect(router.has('test')).toBeTruthy(); + }); + + it('createRouterManager', () => { + const router = createRouterManager({ type: 'memory', initialEntries: ['/'] }, app); + const route: RouteType = { + path: '/', + element:
, + }; + router.add('test', route); + expect(router.has('test')).toBeTruthy(); }); it('nested route', () => { @@ -89,11 +107,13 @@ describe('Router', () => { }); it('Component is string', () => { - const router = new RouterManager({ - type: 'memory', - initialEntries: ['/'], - renderComponent: app.renderComponent.bind(app), - }); + const router = new RouterManager( + { + type: 'memory', + initialEntries: ['/'], + }, + app, + ); const Hello = () =>
; app.addComponents({ Hello }); const route: RouteType = { @@ -109,7 +129,7 @@ describe('Router', () => { let router: RouterManager; beforeEach(() => { - router = new RouterManager({ type: 'memory', initialEntries: ['/'] }); + router = new RouterManager({ type: 'memory', initialEntries: ['/'] }, app); }); it('basic', () => { const route1: RouteType = { @@ -125,7 +145,7 @@ describe('Router', () => { describe('getRouterComponent', () => { it('basic', async () => { - const router = new RouterManager({ type: 'memory', initialEntries: ['/'] }); + const router = new RouterManager({ type: 'memory', initialEntries: ['/'] }, app); const Layout = () => { return (
@@ -156,8 +176,28 @@ describe('Router', () => { expect(screen.getByText('AboutComponent')).toBeInTheDocument(); }); + it('basename and type', async () => { + const router = new RouterManager({ type: 'browser' }, app); + router.setType('hash'); + router.setBasename('/admin'); + router.add('home', { + path: '/', + element:
123
, + }); + + const RouterComponent = router.getRouterComponent(); + render(); + expect(screen.queryByTestId('content')).not.toBeInTheDocument(); + + window.location.hash = '#/admin'; + + await waitFor(() => { + expect(screen.getByTestId('content')).toBeInTheDocument(); + }); + }); + it('BaseLayout', () => { - const router = new RouterManager({ type: 'memory', initialEntries: ['/'] }); + const router = new RouterManager({ type: 'memory', initialEntries: ['/'] }, app); router.add('home', { path: '/', element:
HomeComponent
, @@ -172,10 +212,10 @@ describe('Router', () => { }); it('nested router', () => { - const router = new RouterManager({ type: 'memory', initialEntries: ['/'] }); + const router = new RouterManager({ type: 'memory', initialEntries: ['/'] }, app); const Test = () => { - const router2 = new RouterManager({ type: 'memory', initialEntries: ['/'] }); + const router2 = new RouterManager({ type: 'memory', initialEntries: ['/'] }, app); router2.add('rooter2', { path: '/', element:
Router2
, diff --git a/packages/core/client/src/application/__tests__/SchemaToolbar.test.tsx b/packages/core/client/src/application/__tests__/SchemaToolbar.test.tsx new file mode 100644 index 000000000..ce51b2eab --- /dev/null +++ b/packages/core/client/src/application/__tests__/SchemaToolbar.test.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { fireEvent, render, screen, waitFor } from '@nocobase/test/client'; +import { SchemaToolbarProvider, useSchemaToolbar, useSchemaToolbarRender } from '../schema-toolbar'; +import { SchemaComponent, SchemaComponentProvider, SortableContext, SortableProvider } from '../../schema-component'; +import { useFieldSchema } from '@formily/react'; +import { Application, ApplicationContext } from '@nocobase/client'; + +describe('SchemaToolbar', () => { + test('SchemaToolbarProvider & useSchemaToolbar', () => { + const Demo = () => { + const context = useSchemaToolbar(); + return
{context.test}
; + }; + + const Root = () => { + return ( + + + + ); + }; + + render(); + + expect(screen.getByTestId('content')).toHaveTextContent('123'); + }); + + describe('useSchemaToolbarRender()', () => { + const renderApp = (demoSchema: any, designable = true) => { + const Demo = () =>
Demo
; + + const CustomToolbar = (props) => { + return ( + <> +
CustomToolbar
+
{JSON.stringify(props)}
+ + ); + }; + + const schema = { + name: 'root', + type: 'object', + 'x-decorator': 'Wrapper', + 'x-component': 'Demo', + ...demoSchema, + }; + + const Wrapper = ({ children }) => { + const schema = useFieldSchema(); + const context = useSchemaToolbarRender(schema); + return ( + <> +
{context.render({ customProps: '123' })}
+
{JSON.stringify(context.exists)}
+ {children} + + ); + }; + + const app = new Application({}); + + render( + + + + + + + , + ); + }; + + test('Render x-designer if x-designer has a value', () => { + renderApp({ + 'x-designer': 'CustomToolbar', + }); + + expect(screen.getByTestId('custom-toolbar')).toHaveTextContent('CustomToolbar'); + expect(screen.getByTestId('custom-toolbar-props')).toHaveTextContent('{"customProps":"123"}'); + expect(screen.getByTestId('toolbar-exists')).toHaveTextContent('true'); + }); + + test('Render x-toolbar if it has a value', () => { + renderApp({ + 'x-toolbar': 'CustomToolbar', + }); + + expect(screen.getByTestId('custom-toolbar')).toHaveTextContent('CustomToolbar'); + }); + + test('Render x-toolbar if both x-toolbar and x-designer have values', () => { + renderApp({ + 'x-toolbar': 'CustomToolbar', + 'x-designer': 'CustomToolbar', + }); + + expect(screen.getByTestId('custom-toolbar')).toHaveTextContent('CustomToolbar'); + }); + + test('Render the default SchemaToolbar component if x-toolbar and x-designer have no values and x-settings has a value', () => { + renderApp({ + 'x-settings': 'DemoSettings', + }); + + expect(screen.getByTestId('toolbar').innerHTML.length > 0).toBe(true); + expect(screen.getByTestId('toolbar-exists')).toHaveTextContent('true'); + }); + + test('Do not render if x-toolbar and x-designer have no values and x-settings also has no value', () => { + renderApp({}); + + expect(screen.getByTestId('toolbar')).toHaveTextContent(''); + expect(screen.getByTestId('toolbar-exists')).toHaveTextContent('false'); + }); + + test('Do not render if the component corresponding to x-toolbar cannot be found', () => { + renderApp({ + 'x-toolbar': 'NotFound', + }); + + expect(screen.getByTestId('toolbar')).toHaveTextContent(''); + }); + + test('Do not render if designable is false', () => { + renderApp( + { + 'x-designer': 'CustomToolbar', + }, + false, + ); + + expect(screen.getByTestId('toolbar')).toHaveTextContent(''); + }); + + test('x-toolbar-props and custom Props', () => { + renderApp({ + 'x-toolbar': 'CustomToolbar', + 'x-toolbar-props': { + test: '123', + }, + }); + + expect(screen.getByTestId('custom-toolbar-props')).toHaveTextContent('{"test":"123","customProps":"123"}'); + }); + }); +}); diff --git a/packages/core/client/src/application/__tests__/hoc/withDynamicSchemaProps.test.tsx b/packages/core/client/src/application/__tests__/hoc/withDynamicSchemaProps.test.tsx new file mode 100644 index 000000000..6f4f538ee --- /dev/null +++ b/packages/core/client/src/application/__tests__/hoc/withDynamicSchemaProps.test.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import { SchemaComponent, SchemaComponentProvider } from '../../../schema-component'; +import { render, screen, sleep, userEvent, waitFor } from '@nocobase/test/client'; +import { withDynamicSchemaProps } from '../../hoc'; + +const HelloComponent = withDynamicSchemaProps((props: any) => ( +
{JSON.stringify(props)}
+)); +const HelloDecorator = withDynamicSchemaProps(({ children, ...others }) => ( +
+
{JSON.stringify(others)}
+ {children} +
+)); + +function withTestDemo(schema: any, scopes?: any) { + const Demo = () => { + return ( + + + + ); + }; + + return Demo; +} + +describe('withDynamicSchemaProps', () => { + test('x-use-component-props', () => { + function useComponentProps() { + return { + a: 'a', + }; + } + const schema = { + 'x-use-component-props': 'useComponentProps', + }; + const scopes = { useComponentProps }; + + const Demo = withTestDemo(schema, scopes); + const { getByTestId } = render(); + expect(getByTestId('component')).toHaveTextContent(JSON.stringify({ a: 'a' })); + }); + + test('x-use-component-props and x-component-props should merge', () => { + function useComponentProps() { + return { + a: 'a', + }; + } + const schema = { + 'x-use-component-props': 'useComponentProps', + 'x-component-props': { + b: 'b', + }, + }; + const scopes = { useComponentProps }; + + const Demo = withTestDemo(schema, scopes); + const { getByTestId } = render(); + expect(getByTestId('component')).toHaveTextContent(JSON.stringify({ a: 'a', b: 'b' })); + }); + + test('x-use-decorator-props', () => { + function useDecoratorProps() { + return { + a: 'a', + }; + } + const schema = { + 'x-use-decorator-props': 'useDecoratorProps', + }; + const scopes = { useDecoratorProps }; + + const Demo = withTestDemo(schema, scopes); + const { getByTestId } = render(); + expect(getByTestId('decorator')).toHaveTextContent(JSON.stringify({ a: 'a' })); + }); + + test('x-use-decorator-props and x-decorator-props should merge', () => { + function useDecoratorProps() { + return { + a: 'a', + }; + } + const schema = { + 'x-use-decorator-props': 'useDecoratorProps', + 'x-decorator-props': { + b: 'b', + }, + }; + const scopes = { useDecoratorProps }; + + const Demo = withTestDemo(schema, scopes); + const { getByTestId } = render(); + expect(getByTestId('decorator')).toHaveTextContent(JSON.stringify({ a: 'a', b: 'b' })); + }); + + test('x-use-component-props and x-use-decorator-props exist simultaneously', () => { + function useDecoratorProps() { + return { + a: 'a', + }; + } + function useComponentProps() { + return { + c: 'c', + }; + } + const schema = { + 'x-use-decorator-props': 'useDecoratorProps', + 'x-decorator-props': { + b: 'b', + }, + 'x-use-component-props': 'useComponentProps', + 'x-component-props': { + d: 'd', + }, + }; + + const scopes = { useDecoratorProps, useComponentProps }; + + const Demo = withTestDemo(schema, scopes); + const { getByTestId } = render(); + expect(getByTestId('decorator')).toHaveTextContent(JSON.stringify({ a: 'a', b: 'b' })); + expect(getByTestId('component')).toHaveTextContent(JSON.stringify({ c: 'c', d: 'd' })); + }); + + test('no register scope', () => { + const schema = { + 'x-use-component-props': 'useComponentProps', + }; + const Demo = withTestDemo(schema); + const { getByTestId } = render(); + expect(getByTestId('component')).toHaveTextContent(JSON.stringify({})); + }); +}); diff --git a/packages/core/client/src/application/__tests__/hooks.test.tsx b/packages/core/client/src/application/__tests__/hooks.test.tsx index e9811661d..64e749a68 100644 --- a/packages/core/client/src/application/__tests__/hooks.test.tsx +++ b/packages/core/client/src/application/__tests__/hooks.test.tsx @@ -1,12 +1,12 @@ -import { render, sleep } from '@nocobase/test/client'; +import { render, sleep, screen, waitFor } from '@nocobase/test/client'; import React from 'react'; import { describe } from 'vitest'; import { Application } from '../Application'; import { Plugin } from '../Plugin'; -import { useApp, usePlugin, useRouter } from '../hooks'; +import { useApp, usePlugin, useRouter, useAppSpin } from '../hooks'; describe('Application Hooks', () => { - describe('useApp', () => { + describe('useApp()', () => { it('should return the application instance', async () => { const app = new Application(); const Hello = () => { @@ -22,7 +22,7 @@ describe('Application Hooks', () => { }); }); - describe('useRouter', () => { + describe('useRouter()', () => { it('should return the router instance', async () => { const app = new Application(); const Hello = () => { @@ -38,7 +38,7 @@ describe('Application Hooks', () => { }); }); - describe('usePlugin', () => { + describe('usePlugin()', () => { it('should return the plugin instance', async () => { class DemoPlugin extends Plugin { test = 'test'; @@ -59,4 +59,37 @@ describe('Application Hooks', () => { await sleep(10); }); }); + + describe('useAppSpin()', () => { + test('no app, should render ant-design Spin', async () => { + const Demo = () => { + const spin = useAppSpin(); + return spin.render(); + }; + + render(); + + await waitFor(() => { + expect(document.querySelector('.ant-spin')).toBeTruthy(); + }); + }); + + test('has app, should render AppSpin Component', () => { + const Demo = () => { + const spin = useAppSpin(); + return spin.render(); + }; + + const app = new Application({ + providers: [Demo], + components: { + AppSpin: () =>
test
, + }, + }); + const Root = app.getRootComponent(); + render(); + expect(document.querySelector('.ant-spin')).toBeFalsy(); + expect(screen.getByTestId('content')).toBeTruthy(); + }); + }); }); diff --git a/packages/core/client/src/application/__tests__/utils/remotePlugins.test.ts b/packages/core/client/src/application/__tests__/utils/remotePlugins.test.ts new file mode 100644 index 000000000..18ca15cb3 --- /dev/null +++ b/packages/core/client/src/application/__tests__/utils/remotePlugins.test.ts @@ -0,0 +1,313 @@ +import { Plugin } from '../../Plugin'; +import { + configRequirejs, + defineDevPlugins, + definePluginClient, + getPlugins, + getRemotePlugins, + processRemotePlugins, +} from '../../utils/remotePlugins'; + +describe('remotePlugins', () => { + afterEach(() => { + window.define = undefined; + }); + + describe('defineDevPlugins()', () => { + it('should define plugins', () => { + class DemoPlugin extends Plugin {} + + const plugins = { + '@nocobase/demo': DemoPlugin, + }; + + const mockDefine: any = vi.fn(); + window.define = mockDefine; + + defineDevPlugins(plugins); + + expect(mockDefine).toBeCalledTimes(1); + expect(mockDefine).toBeCalledWith('@nocobase/demo/client', expect.any(Function)); + }); + + it('should return Plugin', () => { + class DemoPlugin extends Plugin {} + const plugins = { + '@nocobase/demo': DemoPlugin, + }; + const define: any = function (packageName: string, load: any) { + expect(packageName).toEqual('@nocobase/demo/client'); + expect(load()).toEqual(DemoPlugin); + }; + window.define = define; + + defineDevPlugins(plugins); + }); + }); + + describe('definePluginClient()', () => { + it('should define plugins', () => { + const mockDefine: any = vi.fn(); + window.define = mockDefine; + + definePluginClient('@nocobase/demo'); + + expect(mockDefine).toBeCalledTimes(1); + expect(mockDefine).toBeCalledWith('@nocobase/demo/client', ['exports', '@nocobase/demo'], expect.any(Function)); + }); + + it('should proxy', () => { + const mockDefine: any = vi.fn(); + window.define = mockDefine; + + definePluginClient('@nocobase/demo'); + + const exports: any = { + a: 'a', + }; + const pluginExports = { + a: 1, + b: 2, + }; + const proxy = mockDefine.mock.calls[0][2]; + proxy(exports, pluginExports); + + expect(exports.__esModule).toBe(true); + expect(exports.a).toBe(1); + expect(exports.b).toBe(2); + }); + }); + + test('should config requirejs', () => { + const requirejs = { + requirejs: { + config: vi.fn(), + }, + }; + const pluginData: any = [ + { + packageName: '@nocobase/demo', + url: 'https://demo.com', + }, + ]; + configRequirejs(requirejs, pluginData); + + expect(requirejs.requirejs.config).toBeCalledTimes(1); + expect(requirejs.requirejs.config).toBeCalledWith({ + waitSeconds: 120, + paths: { + '@nocobase/demo': 'https://demo.com', + }, + }); + }); + + describe('processRemotePlugins()', () => { + it('should resolve', () => { + const pluginData: any = [ + { + name: '@nocobase/demo', + packageName: '@nocobase/demo', + url: 'https://demo.com', + }, + ]; + const resolve = vi.fn(); + const process = processRemotePlugins(pluginData, resolve); + + const pluginModules: any = [ + { + default: 'default', + }, + ]; + process(...pluginModules); + + expect(resolve).toBeCalledTimes(1); + expect(resolve).toBeCalledWith([['@nocobase/demo', 'default']]); + }); + + it('should filter', () => { + const pluginData: any = [ + { + name: '@nocobase/demo', + packageName: '@nocobase/demo', + url: 'https://demo.com', + }, + ]; + const resolve = vi.fn(); + const process = processRemotePlugins(pluginData, resolve); + + const pluginModules: any = [null]; + process(...pluginModules); + + expect(resolve).toBeCalledTimes(1); + expect(resolve).toBeCalledWith([]); + }); + }); + + describe('getRemotePlugins()', () => { + it('should get remote plugins', async () => { + const mockPluginsModules = (pluginData, resolve) => { + resolve({ default: 'default' }); + }; + + const requirejs: any = { + requirejs: mockPluginsModules, + }; + + requirejs.requirejs.config = vi.fn(); + requirejs.requirejs.requirejs = vi.fn(); + const pluginData: any = [ + { + name: '@nocobase/demo', + packageName: '@nocobase/demo', + url: 'https://demo.com', + }, + ]; + const mockDefine: any = vi.fn(); + window.define = mockDefine; + const plugins = await getRemotePlugins(requirejs, pluginData); + expect(plugins).toEqual([['@nocobase/demo', 'default']]); + }); + }); + + describe('getPlugins()', () => { + it('If there is no devDynamicImport, all plugins are obtained through API requests', async () => { + const remoteFn = vi.fn(); + const mockPluginsModules = (pluginData, resolve) => { + remoteFn(); + resolve({ default: 'default' }, { default: 'default' }); + }; + + const requirejs: any = { + requirejs: mockPluginsModules, + }; + + requirejs.requirejs.config = vi.fn(); + requirejs.requirejs.requirejs = vi.fn(); + const pluginData: any = [ + { + name: '@nocobase/demo', + packageName: '@nocobase/demo', + url: 'https://demo1.com', + }, + { + name: '@nocobase/demo2', + packageName: '@nocobase/demo2', + url: 'https://demo2.com', + }, + ]; + const mockDefine: any = vi.fn(); + window.define = mockDefine; + + const plugins = await getPlugins({ requirejs, pluginData }); + expect(plugins).toEqual([ + ['@nocobase/demo', 'default'], + ['@nocobase/demo2', 'default'], + ]); + expect(remoteFn).toBeCalledTimes(1); + expect(mockDefine).toBeCalledTimes(2); + expect(requirejs.requirejs.config).toBeCalledWith({ + waitSeconds: 120, + paths: { + '@nocobase/demo': 'https://demo1.com', + '@nocobase/demo2': 'https://demo2.com', + }, + }); + }); + + it('If there is devDynamicImport and devDynamicImport returns all, remote API will not be requested', async () => { + const remoteFn = vi.fn(); + const mockPluginsModules = (pluginData, resolve) => { + remoteFn(); + resolve({ default: 'default' }, { default: 'default' }); + }; + + const requirejs: any = { + requirejs: mockPluginsModules, + }; + + requirejs.requirejs.config = vi.fn(); + requirejs.requirejs.requirejs = vi.fn(); + const pluginData: any = [ + { + name: '@nocobase/demo', + packageName: '@nocobase/demo', + url: 'https://demo1.com', + }, + { + name: '@nocobase/demo2', + packageName: '@nocobase/demo2', + url: 'https://demo2.com', + }, + ]; + const mockDefine: any = vi.fn(); + window.define = mockDefine; + + const plugins = await getPlugins({ + requirejs, + pluginData, + devDynamicImport: (() => { + return Promise.resolve({ default: 'default' }); + }) as any, + }); + expect(plugins).toEqual([ + ['@nocobase/demo', 'default'], + ['@nocobase/demo2', 'default'], + ]); + expect(remoteFn).toBeCalledTimes(0); + expect(mockDefine).toBeCalledTimes(2); + expect(requirejs.requirejs.config).toBeCalledTimes(0); + }); + + it('If there is devDynamicImport and devDynamicImport returns partial, remote API will be requested', async () => { + const remoteFn = vi.fn(); + const mockPluginsModules = (pluginData, resolve) => { + remoteFn(); + resolve({ default: 'default' }); + }; + + const requirejs: any = { + requirejs: mockPluginsModules, + }; + + requirejs.requirejs.config = vi.fn(); + requirejs.requirejs.requirejs = vi.fn(); + const pluginData: any = [ + { + name: '@nocobase/demo', + packageName: '@nocobase/demo', + url: 'https://demo1.com', + }, + { + name: '@nocobase/demo2', + packageName: '@nocobase/demo2', + url: 'https://demo2.com', + }, + ]; + const mockDefine: any = vi.fn(); + window.define = mockDefine; + + const plugins = await getPlugins({ + requirejs, + pluginData, + devDynamicImport: ((packageName) => { + if (packageName === '@nocobase/demo') { + return Promise.resolve({ default: 'default' }); + } + return Promise.resolve(null); + }) as any, + }); + expect(plugins).toEqual([ + ['@nocobase/demo', 'default'], + ['@nocobase/demo2', 'default'], + ]); + expect(remoteFn).toBeCalled(); + expect(mockDefine).toBeCalledTimes(2); + expect(requirejs.requirejs.config).toBeCalledWith({ + waitSeconds: 120, + paths: { + '@nocobase/demo2': 'https://demo2.com', + }, + }); + }); + }); +}); diff --git a/packages/core/client/src/application/components/AppComponent.tsx b/packages/core/client/src/application/components/AppComponent.tsx index c549df8bb..1721d1d2e 100644 --- a/packages/core/client/src/application/components/AppComponent.tsx +++ b/packages/core/client/src/application/components/AppComponent.tsx @@ -11,7 +11,10 @@ export interface AppComponentProps { export const AppComponent: FC = observer((props) => { const { app } = props; const handleErrors = useCallback((error: Error, info: { componentStack: string }) => { - console.error(error, info); + console.error(error); + const err = new Error(); + err.stack = info.componentStack.trim(); + console.error(err); }, []); useEffect(() => { app.load(); diff --git a/packages/core/client/src/application/hoc/index.ts b/packages/core/client/src/application/hoc/index.ts new file mode 100644 index 000000000..94568d10d --- /dev/null +++ b/packages/core/client/src/application/hoc/index.ts @@ -0,0 +1 @@ +export * from './withDynamicSchemaProps'; diff --git a/packages/core/client/src/application/hoc/withDynamicSchemaProps.tsx b/packages/core/client/src/application/hoc/withDynamicSchemaProps.tsx new file mode 100644 index 000000000..fc3d918ad --- /dev/null +++ b/packages/core/client/src/application/hoc/withDynamicSchemaProps.tsx @@ -0,0 +1,53 @@ +import { merge, omit } from 'lodash'; +import React, { ComponentType, useMemo } from 'react'; +import { useDesignable, useSchemaComponentContext } from '../../schema-component'; + +const useDefaultSchemaProps = () => undefined; + +interface WithSchemaHookOptions { + displayName?: string; +} + +export function withDynamicSchemaProps(Component: ComponentType, options: WithSchemaHookOptions = {}) { + const displayName = options.displayName || Component.displayName || Component.name; + const ComponentWithProps: ComponentType = (props) => { + const { dn, findComponent } = useDesignable(); + const { scope } = useSchemaComponentContext(); + const useComponentPropsStr = useMemo(() => { + const xComponent = dn.getSchemaAttribute('x-component'); + const xDecorator = dn.getSchemaAttribute('x-decorator'); + const xUseComponentProps = dn.getSchemaAttribute('x-use-component-props'); + const xUseDecoratorProps = dn.getSchemaAttribute('x-use-decorator-props'); + + if (xComponent && xUseComponentProps && findComponent(xComponent) === ComponentWithProps) { + return xUseComponentProps; + } + + if (xDecorator && xUseDecoratorProps && findComponent(xDecorator) === ComponentWithProps) { + return xUseDecoratorProps; + } + }, [dn]); + const useSchemaProps = useMemo(() => { + let res = undefined; + if (useComponentPropsStr) { + res = scope[useComponentPropsStr]; + if (!res) { + console.error(`${useComponentPropsStr} is not registered`); + } + } + return res || useDefaultSchemaProps; + }, [scope, useComponentPropsStr]); + const schemaProps = useSchemaProps(props); + + const memoProps = useMemo(() => { + return merge(omit(schemaProps, 'children'), omit(props, 'children')); + }, [schemaProps, props]); + + return {props.children}; + }; + + Component.displayName = displayName; + ComponentWithProps.displayName = `withSchemaProps(${displayName})`; + + return ComponentWithProps; +} diff --git a/packages/core/client/src/application/hooks/index.ts b/packages/core/client/src/application/hooks/index.ts index 81a2cbab1..dbb36261e 100644 --- a/packages/core/client/src/application/hooks/index.ts +++ b/packages/core/client/src/application/hooks/index.ts @@ -1,4 +1,4 @@ export * from './useApp'; -export * from './useAppPluginLoad'; export * from './usePlugin'; export * from './useRouter'; +export * from './useAppSpin'; diff --git a/packages/core/client/src/application/hooks/useAppPluginLoad.ts b/packages/core/client/src/application/hooks/useAppPluginLoad.ts deleted file mode 100644 index 09cb971c6..000000000 --- a/packages/core/client/src/application/hooks/useAppPluginLoad.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useEffect, useState } from 'react'; -import type { Application } from '../Application'; - -export function useAppPluginLoad(app: Application) { - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - useEffect(() => { - async function run() { - try { - await app.load(); - } catch (err) { - console.error(err); - setError(err); - } - setLoading(false); - } - run(); - }, [app]); - return { loading, error }; -} diff --git a/packages/core/client/src/application/hooks/useAppSpin.ts b/packages/core/client/src/application/hooks/useAppSpin.ts index 28469aa10..8160735e6 100644 --- a/packages/core/client/src/application/hooks/useAppSpin.ts +++ b/packages/core/client/src/application/hooks/useAppSpin.ts @@ -5,6 +5,6 @@ import { useApp } from './useApp'; export const useAppSpin = () => { const app = useApp(); return { - render: () => (app ? app?.renderComponent?.('AppSpin') : React.createElement(Spin)), + render: () => (app?.renderComponent ? app?.renderComponent?.('AppSpin') : React.createElement(Spin)), }; }; diff --git a/packages/core/client/src/application/index.ts b/packages/core/client/src/application/index.ts index acfc735ef..55afe7657 100644 --- a/packages/core/client/src/application/index.ts +++ b/packages/core/client/src/application/index.ts @@ -8,4 +8,5 @@ export * from './schema-initializer'; export * from './schema-settings'; export * from './schema-toolbar'; export * from './PluginSettingsManager'; +export * from './hoc'; export { ApplicationContext } from './context'; diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerChildren.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerChildren.tsx index da35e2446..fb4342981 100644 --- a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerChildren.tsx +++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerChildren.tsx @@ -1,8 +1,8 @@ -import React, { FC, useMemo } from 'react'; +import React, { FC, memo, useMemo } from 'react'; -import { SchemaInitializerItemType } from '../types'; -import { SchemaInitializerItemContext } from '../context'; import { useFindComponent } from '../../../schema-component'; +import { SchemaInitializerItemContext } from '../context'; +import { SchemaInitializerItemType } from '../types'; export const SchemaInitializerChildren: FC<{ children: SchemaInitializerItemType[] }> = (props) => { const { children } = props; if (!children) return null; @@ -28,7 +28,7 @@ const typeComponentMap: Record = { const useChildrenDefault = () => undefined; const useVisibleDefault = () => true; const useComponentPropsDefault = () => undefined; -export const SchemaInitializerChild: FC = (props) => { +export const SchemaInitializerChild: FC = memo((props) => { const { type, Component, @@ -37,7 +37,7 @@ export const SchemaInitializerChild: FC = (props) => useVisible = useVisibleDefault, useChildren = useChildrenDefault, useComponentProps = useComponentPropsDefault, - checkChildrenLength, + hideIfNoChildren, componentProps, sort: _unUse, ...others @@ -67,7 +67,7 @@ export const SchemaInitializerChild: FC = (props) => if (!C) { return null; } - if (checkChildrenLength && Array.isArray(componentChildren) && componentChildren.length === 0) { + if (hideIfNoChildren && Array.isArray(componentChildren) && componentChildren.length === 0) { return null; } @@ -78,4 +78,5 @@ export const SchemaInitializerChild: FC = (props) => ); -}; +}); +SchemaInitializerChild.displayName = 'SchemaInitializerChild'; diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx index a1804c513..51aec3641 100644 --- a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx +++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx @@ -1,13 +1,12 @@ import { uid } from '@formily/shared'; import classNames from 'classnames'; -import React, { ReactNode } from 'react'; +import React, { ReactNode, memo, useMemo } from 'react'; import { Icon } from '../../../icon'; import { useCompile } from '../../../schema-component'; import { useSchemaInitializerItem } from '../context'; import { useAriaAttributeOfMenuItem, useSchemaInitializerMenuItems } from '../hooks'; import { SchemaInitializerMenu } from './SchemaInitializerSubMenu'; import { useSchemaInitializerStyles } from './style'; -import { MenuProps } from 'antd'; export interface SchemaInitializerItemProps { style?: React.CSSProperties; @@ -21,57 +20,61 @@ export interface SchemaInitializerItemProps { children?: ReactNode; } -export const SchemaInitializerItem = React.forwardRef((props, ref) => { - const { style, name = uid(), applyMenuStyle = true, className, items, icon, title, onClick, children } = props; - const compile = useCompile(); - const childrenItems = useSchemaInitializerMenuItems(items, name, onClick); - const { componentCls, hashId } = useSchemaInitializerStyles(); - const { attribute } = useAriaAttributeOfMenuItem(); +export const SchemaInitializerItem = memo( + React.forwardRef((props, ref) => { + const { style, name = uid(), applyMenuStyle = true, className, items, icon, title, onClick, children } = props; + const compile = useCompile(); + const childrenItems = useSchemaInitializerMenuItems(items, name, onClick); + const { componentCls, hashId } = useSchemaInitializerStyles(); + const { attribute } = useAriaAttributeOfMenuItem(); - if (items && items.length > 0) { - return ( - { - if (info.key !== name) return; - onClick?.({ ...info, item: props }); - }, - icon: typeof icon === 'string' ? : icon, - children: childrenItems, + const menuItems = useMemo(() => { + if (!(items && items.length > 0)) return undefined; + return [ + { + key: name, + style: style, + className: className, + label: children || compile(title), + onClick: (info) => { + if (info.key !== name) return; + onClick?.({ ...info, item: props }); }, - ]} - > - ); - } + icon: typeof icon === 'string' ? : icon, + children: childrenItems, + }, + ]; + }, [name, style, className, children, title, onClick, icon, childrenItems]); - return ( -
{ - event.stopPropagation(); - onClick?.({ event, item: props }); - }} - > + if (items && items.length > 0) { + return ; + } + + return (
{ + event.stopPropagation(); + onClick?.({ event, item: props }); + }} > - {children || ( - <> - {icon && typeof icon === 'string' ? : icon} - {compile(title)} - - )} +
+ {children || ( + <> + {icon && typeof icon === 'string' ? : icon} + {compile(title)} + + )} +
-
- ); -}); + ); + }), +); +SchemaInitializerItem.displayName = 'SchemaInitializerItem'; export const SchemaInitializerItemInternal = () => { const itemConfig = useSchemaInitializerItem(); diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSelect.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSelect.tsx index 69d2c1914..f220bcdd1 100644 --- a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSelect.tsx +++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSelect.tsx @@ -14,10 +14,13 @@ export const SchemaInitializerSelect: FC = (pr const { title, options, value, onChange, openOnHover, onClick: _onClick, ...others } = props; const [open, setOpen] = useState(false); - const onClick = (...args) => { - setOpen(false); - _onClick?.(...args); - }; + const onClick = useCallback( + (...args) => { + setOpen(false); + _onClick?.(...args); + }, + [setOpen, _onClick], + ); const onMouseEnter = useCallback(() => setOpen(true), []); // 鼠标 hover 时,打开下拉框 diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSubMenu.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSubMenu.tsx index f66627724..98daa7019 100644 --- a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSubMenu.tsx +++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSubMenu.tsx @@ -13,6 +13,7 @@ export interface SchemaInitializerSubMenuProps { name: string; title?: string; onClick?: (args: any) => void; + onOpenChange?: (openKeys: string[]) => void; icon?: string | ReactNode; children?: SchemaInitializerOptions['items']; } @@ -66,9 +67,10 @@ export const SchemaInitializerMenu: FC = (props) => { }; export const SchemaInitializerSubMenu: FC = (props) => { - const { children, title, name = uid(), icon, ...others } = props; + const { children, title, name = uid(), onOpenChange, icon, ...others } = props; const compile = useCompile(); const childrenItems = useSchemaInitializerMenuItems(children, name); + const items = useMemo(() => { return [ { @@ -80,7 +82,7 @@ export const SchemaInitializerSubMenu: FC = (prop }, ]; }, [childrenItems, compile, icon, name, others, title]); - return ; + return ; }; export const SchemaInitializerSubMenuInternal = () => { diff --git a/packages/core/client/src/application/schema-initializer/context/index.ts b/packages/core/client/src/application/schema-initializer/context/index.ts index 67c98d9b6..6a1154a25 100644 --- a/packages/core/client/src/application/schema-initializer/context/index.ts +++ b/packages/core/client/src/application/schema-initializer/context/index.ts @@ -4,8 +4,8 @@ import { InsertType, SchemaInitializerItemType, SchemaInitializerOptions } from export const SchemaInitializerContext = createContext<{ insert: InsertType; options: SchemaInitializerOptions; - visible?: boolean; - setVisible?: (v: boolean) => void; + visible: boolean; + setVisible: (v: boolean) => void; }>({} as any); SchemaInitializerContext.displayName = 'SchemaInitializerContext'; @@ -16,14 +16,7 @@ export const useSchemaInitializer = () => { export const SchemaInitializerItemContext = createContext< Omit< SchemaInitializerItemType, - | 'type' - | 'Component' - | 'component' - | 'useVisible' - | 'useChildren' - | 'checkChildrenLength' - | 'sort' - | 'componentProps' + 'type' | 'Component' | 'component' | 'useVisible' | 'useChildren' | 'hideIfNoChildren' | 'sort' | 'componentProps' > >({} as any); SchemaInitializerItemContext.displayName = 'SchemaInitializerItemContext'; diff --git a/packages/core/client/src/application/schema-initializer/hoc/index.tsx b/packages/core/client/src/application/schema-initializer/hoc/index.tsx index 36279f040..2f3540893 100644 --- a/packages/core/client/src/application/schema-initializer/hoc/index.tsx +++ b/packages/core/client/src/application/schema-initializer/hoc/index.tsx @@ -3,6 +3,8 @@ import { ConfigProvider, Popover, theme } from 'antd'; import React, { ComponentType, useCallback, useMemo, useState } from 'react'; import { css } from '@emotion/css'; +import { useNiceDropdownMaxHeight } from '../../../common/useNiceDropdownHeight'; +import { useFlag } from '../../../flag-provider'; import { useDesignable } from '../../../schema-component'; import { useSchemaInitializerStyles } from '../components/style'; import { SchemaInitializerContext } from '../context'; @@ -13,6 +15,7 @@ const defaultWrap = (s: ISchema) => s; export function withInitializer(C: ComponentType) { const WithInitializer = observer((props: SchemaInitializerOptions) => { const { designable, insertAdjacent } = useDesignable(); + const { isInSubTable } = useFlag() || {}; const { insert, useInsert, @@ -32,9 +35,9 @@ export function withInitializer(C: ComponentType) { const insertSchema = useCallback( (schema) => { if (insertCallback) { - insertCallback(wrap(schema)); + insertCallback(wrap(schema, { isInSubTable })); } else { - insertAdjacent(insertPosition, wrap(schema), { onSuccess }); + insertAdjacent(insertPosition, wrap(schema, { isInSubTable }), { onSuccess }); } }, [insertCallback, wrap, insertAdjacent, insertPosition, onSuccess], @@ -43,6 +46,7 @@ export function withInitializer(C: ComponentType) { const { wrapSSR, hashId, componentCls } = useSchemaInitializerStyles(); const [visible, setVisible] = useState(false); const { token } = theme.useToken(); + const dropdownMaxHeight = useNiceDropdownMaxHeight([visible]); const cProps = useMemo( () => ({ @@ -88,9 +92,8 @@ export function withInitializer(C: ComponentType) {
void) { + const getMenuItems = useGetSchemaInitializerMenuItems(onClick); + return getMenuItems(items, name); +} + +export function useGetSchemaInitializerMenuItems(onClick?: (args: any) => void) { const compile = useCompile(); const getMenuItems = useCallback( - (items: SchemaInitializerItemType[], parentKey: string) => { + (items: any[], parentKey: string) => { if (!items?.length) { return []; } return items.map((item: any, indexA) => { const ItemComponent = item.component || item.Component; let element: ReactNode; - const compiledTitle = item.title ? compile(item.title) : undefined; + const compiledTitle = item.title || item.label ? compile(item.title || item.label) : undefined; if (ItemComponent) { element = React.createElement(SchemaInitializerChild, { ...item, title: compiledTitle }); if (!element) return; } if (item.type === 'divider') { - return { type: 'divider', key: `divider-${indexA}` }; + return { type: 'divider', key: item.key || `divider-${indexA}` }; } if (item.type === 'item' && ItemComponent) { if (!item.key) { - item.key = `${item.title}-${indexA}`; + item.key = `${compiledTitle}-${indexA}`; } return { key: item.key, @@ -39,8 +44,8 @@ export function useSchemaInitializerMenuItems(items: any[], name?: string, onCli }; } if (item.type === 'itemGroup') { - const label = typeof item.title === 'string' ? compiledTitle : item.title; - const key = `${parentKey}-item-group-${indexA}`; + const label = typeof compiledTitle === 'string' ? compiledTitle : item.title; + const key = item.key || `${parentKey}-item-group-${indexA}`; return { type: 'group', key, @@ -51,7 +56,7 @@ export function useSchemaInitializerMenuItems(items: any[], name?: string, onCli } if (item.type === 'subMenu') { const label = compiledTitle; - const key = `${parentKey}-sub-menu-${indexA}`; + const key = item.key || item.name || `${parentKey}-sub-menu-${indexA}`; return { key, label, @@ -64,7 +69,7 @@ export function useSchemaInitializerMenuItems(items: any[], name?: string, onCli } const label = element || compiledTitle || item.label; - const key = `${parentKey}-${item.title}-${indexA}`; + const key = item.key || `${parentKey}-${compiledTitle}-${indexA}`; return { key, label, @@ -82,7 +87,7 @@ export function useSchemaInitializerMenuItems(items: any[], name?: string, onCli [compile, onClick], ); - return getMenuItems(items, name); + return getMenuItems; } const InitializerComponent: FC> = React.memo((options) => { @@ -107,6 +112,7 @@ export function useSchemaInitializerRender( options?: Omit, 'name'>, ) { const app = useApp(); + const renderCache = React.useRef>>({}); const initializer = useMemo( () => app.schemaInitializerManager.get(name), [app.schemaInitializerManager, name], @@ -128,8 +134,17 @@ export function useSchemaInitializerRender( } return { exists: true, - render: (props?: Omit, 'name'>) => - React.createElement(InitializerComponent, { ...initializer.options, ...options, ...props }), + render: (props?: Omit, 'name'>) => { + const key = JSON.stringify(props) || '{}'; + if (renderCache.current[key]) { + return renderCache.current[key]; + } + return (renderCache.current[key] = React.createElement(InitializerComponent, { + ...initializer.options, + ...options, + ...props, + })); + }, }; }, [initializer, name, options]); diff --git a/packages/core/client/src/application/schema-initializer/hooks/useAriaAttributeOfMenuItem.ts b/packages/core/client/src/application/schema-initializer/hooks/useAriaAttributeOfMenuItem.ts index 018735aa7..04b2779dc 100644 --- a/packages/core/client/src/application/schema-initializer/hooks/useAriaAttributeOfMenuItem.ts +++ b/packages/core/client/src/application/schema-initializer/hooks/useAriaAttributeOfMenuItem.ts @@ -1,16 +1,18 @@ +import { useMemo } from 'react'; import { useSchemaInitializerSubMenuContext } from '../components/SchemaInitializerSubMenu'; export const useAriaAttributeOfMenuItem = () => { const { isInMenu } = useSchemaInitializerSubMenuContext(); - let attribute = {}; - // 在 Menu 中,每一项的 role 已经被标记为 menuitem 了,不需要再次标记; // 而在其他地方,每一项的根元素都是 div,需要标记为 menuitem; - if (!isInMenu) { - attribute = { - role: 'menuitem', - }; - } + const attribute = useMemo(() => { + if (!isInMenu) { + return { + role: 'menuitem', + }; + } + return {}; + }, [isInMenu]); return { attribute, diff --git a/packages/core/client/src/application/schema-initializer/types.ts b/packages/core/client/src/application/schema-initializer/types.ts index f57b9beca..d57e03186 100644 --- a/packages/core/client/src/application/schema-initializer/types.ts +++ b/packages/core/client/src/application/schema-initializer/types.ts @@ -29,7 +29,7 @@ export interface SchemaInitializerItemComponentType { useComponentProps?: () => Omit; useVisible?: () => boolean; children?: SchemaInitializerItemType[]; - checkChildrenLength?: boolean; + hideIfNoChildren?: boolean; useChildren?: () => SchemaInitializerItemType[]; [index: string]: any; } @@ -57,14 +57,14 @@ export type SchemaInitializerItemActionModalType = { export type SchemaInitializerItemGroupType = { type: 'itemGroup'; children?: SchemaInitializerItemType[]; - checkChildrenLength?: boolean; + hideIfNoChildren?: boolean; useChildren?: () => SchemaInitializerItemType[]; } & SchemaInitializerItemBuiltInType; export type SchemaInitializerSubMenuType = { type: 'subMenu'; children?: SchemaInitializerItemType[]; - checkChildrenLength?: boolean; + hideIfNoChildren?: boolean; useChildren?: () => SchemaInitializerItemType[]; } & SchemaInitializerItemBuiltInType; @@ -100,7 +100,7 @@ export interface SchemaInitializerOptions { insertPosition?: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd'; designable?: boolean; - wrap?: (s: ISchema) => ISchema; + wrap?: (s: ISchema, options?: any) => ISchema; onSuccess?: (data: any) => void; insert?: InsertType; useInsert?: () => InsertType; diff --git a/packages/core/client/src/application/schema-settings/components/SchemaSettingsChildren.tsx b/packages/core/client/src/application/schema-settings/components/SchemaSettingsChildren.tsx index 07c022cc3..1db8f2feb 100644 --- a/packages/core/client/src/application/schema-settings/components/SchemaSettingsChildren.tsx +++ b/packages/core/client/src/application/schema-settings/components/SchemaSettingsChildren.tsx @@ -1,7 +1,6 @@ -import React, { FC, useMemo, useRef, useEffect } from 'react'; +import React, { FC, memo, useEffect, useMemo, useRef } from 'react'; import { useFindComponent } from '../../../schema-component'; -import { SchemaSettingsItemType } from '../types'; import { SchemaSettingsActionModalItem, SchemaSettingsCascaderItem, @@ -17,6 +16,8 @@ import { useSchemaSettings, } from '../../../schema-settings/SchemaSettings'; import { SchemaSettingItemContext } from '../context'; +import { SchemaSettingsItemType } from '../types'; +import { useFieldComponentName } from '../../../common/useFieldComponentName'; export interface SchemaSettingsChildrenProps { children: SchemaSettingsItemType[]; @@ -40,6 +41,8 @@ export const SchemaSettingsChildren: FC = (props) = const { children } = props; const { visible } = useSchemaSettings(); const firstVisible = useRef(false); + const fieldComponentName = useFieldComponentName(); + useEffect(() => { if (visible) { firstVisible.current = true; @@ -52,9 +55,14 @@ export const SchemaSettingsChildren: FC = (props) = <> {children .sort((a, b) => (a.sort || 0) - (b.sort || 0)) - .map((item) => ( - - ))} + .map((item) => { + // 当动态切换 SchemaSettings 列表时(比如切换 field component 时,列表会动态变化),切换前和切换后的 item.name 可能相同, + // 此时如果使用 item.name 作为 key,会导致 React 认为其前后是同一个组件;因为 SchemaSettingsChild 的某些 hooks 是通过 props 传入的, + // 两次渲染之间 props 可能发生变化,就可能报 hooks 调用顺序的错误。所以这里使用 fieldComponentName 和 item.name 拼成 + // 一个不会重复的 key,保证每次渲染都是新的组件。 + const key = `${fieldComponentName ? fieldComponentName + '-' : ''}${item.name}`; + return ; + })} ); }; @@ -62,7 +70,7 @@ export const SchemaSettingsChildren: FC = (props) = const useChildrenDefault = () => undefined; const useComponentPropsDefault = () => undefined; const useVisibleDefault = () => true; -export const SchemaSettingsChild: FC = (props) => { +export const SchemaSettingsChild: FC = memo((props) => { const { useVisible = useVisibleDefault, useChildren = useChildrenDefault, @@ -70,7 +78,7 @@ export const SchemaSettingsChild: FC = (props) => { type, Component, children, - checkChildrenLength, + hideIfNoChildren = true, componentProps, } = props as any; const useChildrenRes = useChildren(); @@ -89,7 +97,7 @@ export const SchemaSettingsChild: FC = (props) => { if (!C) { return null; } - if (checkChildrenLength && Array.isArray(componentChildren) && componentChildren.length === 0) { + if (hideIfNoChildren && Array.isArray(componentChildren) && componentChildren.length === 0) { return null; } @@ -102,4 +110,5 @@ export const SchemaSettingsChild: FC = (props) => { ); -}; +}); +SchemaSettingsChild.displayName = 'SchemaSettingsChild'; diff --git a/packages/core/client/src/application/schema-settings/components/SchemaSettingsWrapper.tsx b/packages/core/client/src/application/schema-settings/components/SchemaSettingsWrapper.tsx index 7f63285e6..5d7f7ab7f 100644 --- a/packages/core/client/src/application/schema-settings/components/SchemaSettingsWrapper.tsx +++ b/packages/core/client/src/application/schema-settings/components/SchemaSettingsWrapper.tsx @@ -20,8 +20,6 @@ export const SchemaSettingsWrapper: FC> = (props) => { }), [componentProps, props, style], ); - Component.displayName = `${Component.displayName || Component.name}(${name})`; - return ( = Omit, 'nam export function useSchemaSettingsRender(name: string, options?: UseSchemaSettingsRenderOptions) { const app = useApp(); const schemaSetting = useMemo(() => app.schemaSettingsManager.get(name), [app.schemaSettingsManager, name]); + const renderCache = React.useRef>>({}); if (!name) { return { exists: false, @@ -34,7 +35,16 @@ export function useSchemaSettingsRender(name: string, options?: UseSchem } return { exists: true, - render: (options2?: UseSchemaSettingsRenderOptions) => - React.createElement(SchemaSettingsWrapper, { ...schemaSetting.options, ...options, ...options2 }), + render: (options2?: UseSchemaSettingsRenderOptions) => { + const key = JSON.stringify(options) || '{}'; + if (renderCache.current[key]) { + return renderCache.current[key]; + } + return (renderCache.current[key] = React.createElement(SchemaSettingsWrapper, { + ...schemaSetting.options, + ...options, + ...options2, + })); + }, }; } diff --git a/packages/core/client/src/application/schema-settings/types.ts b/packages/core/client/src/application/schema-settings/types.ts index 3d61fe7ad..89ce3a71e 100644 --- a/packages/core/client/src/application/schema-settings/types.ts +++ b/packages/core/client/src/application/schema-settings/types.ts @@ -24,7 +24,10 @@ interface SchemaSettingsItemCommon { useVisible?: () => boolean; children?: SchemaSettingsItemType[]; useChildren?: () => SchemaSettingsItemType[]; - checkChildrenLength?: boolean; + /** + * @default true + */ + hideIfNoChildren?: boolean; componentProps?: Omit; useComponentProps?: () => Omit; } diff --git a/packages/core/client/src/application/schema-toolbar/hooks/index.tsx b/packages/core/client/src/application/schema-toolbar/hooks/index.tsx index 5ee00713c..429ee853e 100644 --- a/packages/core/client/src/application/schema-toolbar/hooks/index.tsx +++ b/packages/core/client/src/application/schema-toolbar/hooks/index.tsx @@ -1,13 +1,13 @@ import React, { useMemo } from 'react'; import { ISchema } from '@formily/json-schema'; import { useComponent, useDesignable } from '../../../schema-component'; -import { SchemaToolbar, SchemaToolbarProps } from '../../../schema-settings'; +import { SchemaToolbar, SchemaToolbarProps } from '../../../schema-settings/GeneralSchemaDesigner'; export const useSchemaToolbarRender = (fieldSchema: ISchema) => { const { designable } = useDesignable(); const toolbar = useMemo(() => { - if (fieldSchema['x-designer'] || fieldSchema['x-toolbar']) { - return fieldSchema['x-toolbar']; + if (fieldSchema['x-toolbar'] || fieldSchema['x-designer']) { + return fieldSchema['x-toolbar'] || fieldSchema['x-designer']; } if (fieldSchema['x-settings']) { @@ -17,7 +17,7 @@ export const useSchemaToolbarRender = (fieldSchema: ISchema) => { const C = useComponent(toolbar); return { - render(props?: SchemaToolbarProps) { + render(props?: SchemaToolbarProps & { [index: string]: any }) { if (!designable || !C) { return null; } diff --git a/packages/core/client/src/application/utils/index.tsx b/packages/core/client/src/application/utils/index.tsx index 8a69d8272..14c41009e 100644 --- a/packages/core/client/src/application/utils/index.tsx +++ b/packages/core/client/src/application/utils/index.tsx @@ -28,7 +28,7 @@ export const compose = (...components: [ComponentType, any][]) => { {children} ); - ComposeComponent.displayName = Child.displayName || Child.name; + ComposeComponent.displayName = `compose(${Child.displayName || Child.name})`; return ComposeComponent; }, BlankComponent); diff --git a/packages/core/client/src/application/utils/remotePlugins.ts b/packages/core/client/src/application/utils/remotePlugins.ts index fef3d1521..adfcabc6c 100644 --- a/packages/core/client/src/application/utils/remotePlugins.ts +++ b/packages/core/client/src/application/utils/remotePlugins.ts @@ -10,27 +10,24 @@ export function defineDevPlugins(plugins: Record) { } export function definePluginClient(packageName: string) { - window.define(`${packageName}/client`, ['exports', packageName], function (_exports: any, _plugin: any) { + window.define(`${packageName}/client`, ['exports', packageName], function (_exports: any, _pluginExports: any) { Object.defineProperty(_exports, '__esModule', { value: true, }); - Object.keys(_plugin).forEach(function (key) { + Object.keys(_pluginExports).forEach(function (key) { if (key === '__esModule') return; - if (key in _exports && _exports[key] === _plugin[key]) return; + if (key in _exports && _exports[key] === _pluginExports[key]) return; Object.defineProperty(_exports, key, { enumerable: true, get: function () { - return _plugin[key]; + return _pluginExports[key]; }, }); }); }); } -export function getRemotePlugins( - requirejs: any, - pluginData: PluginData[] = [], -): Promise> { +export function configRequirejs(requirejs: any, pluginData: PluginData[]) { requirejs.requirejs.config({ waitSeconds: 120, paths: pluginData.reduce>((acc, cur) => { @@ -38,34 +35,42 @@ export function getRemotePlugins( return acc; }, {}), }); +} + +export function processRemotePlugins(pluginData: PluginData[], resolve: (plugins: [string, typeof Plugin][]) => void) { + return (...pluginModules: (typeof Plugin & { default?: typeof Plugin })[]) => { + const res: [string, typeof Plugin][] = pluginModules + .map<[string, typeof Plugin]>((item, index) => [pluginData[index].name, item?.default || item]) + .filter((item) => item[1]); + resolve(res); + + const emptyPlugins = pluginModules + .map((item, index) => (!item ? index : null)) + .filter((i) => i !== null) + .map((i) => pluginData[i].packageName); + + if (emptyPlugins.length > 0) { + console.error( + '[nocobase load plugin error]: These plugins do not have an `export.default` exported content or there is an error in the plugins. error plugins: \r\n%s', + emptyPlugins.join(', \r\n'), + ); + } + }; +} + +export function getRemotePlugins( + requirejs: any, + pluginData: PluginData[] = [], +): Promise> { + configRequirejs(requirejs, pluginData); const packageNames = pluginData.map((item) => item.packageName); packageNames.forEach((packageName) => { definePluginClient(packageName); }); + return new Promise((resolve, reject) => { - requirejs.requirejs( - packageNames, - (...pluginModules: (typeof Plugin & { default?: typeof Plugin })[]) => { - const res = pluginModules - .map<[string, typeof Plugin]>((item, index) => [pluginData[index].name, item.default || item]) - .filter((item) => item[1]); - resolve(res); - - const emptyPlugins = pluginModules - .map((item, index) => (!item ? index : null)) - .filter((i) => i !== null) - .map((i) => pluginData[i].packageName); - - if (emptyPlugins.length > 0) { - console.error( - '[nocobase load plugin error]: These plugins do not have an `export.default` exported content or there is an error in the plugins. error plugins: \r\n%s', - emptyPlugins.join(', \r\n'), - ); - } - }, - reject, - ); + requirejs.requirejs(packageNames, processRemotePlugins(pluginData, resolve), reject); }); } diff --git a/packages/core/client/src/application/utils/requirejs.ts b/packages/core/client/src/application/utils/requirejs.ts index d0d7117a7..9a0d77edf 100644 --- a/packages/core/client/src/application/utils/requirejs.ts +++ b/packages/core/client/src/application/utils/requirejs.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ // @ts-nocheck /* eslint-disable */ /* prettier-ignore */ diff --git a/packages/core/client/src/application/utils/types.ts b/packages/core/client/src/application/utils/types.ts index 55737e53e..ac87b9b0e 100644 --- a/packages/core/client/src/application/utils/types.ts +++ b/packages/core/client/src/application/utils/types.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ /** vim: et:ts=4:sw=4:sts=4 * @license RequireJS 2.3.6 Copyright jQuery Foundation and other contributors. * Released under MIT license, https://github.com/requirejs/requirejs/blob/master/LICENSE diff --git a/packages/core/client/src/block-provider/BlockProvider.tsx b/packages/core/client/src/block-provider/BlockProvider.tsx index a75715c7a..89556f7d5 100644 --- a/packages/core/client/src/block-provider/BlockProvider.tsx +++ b/packages/core/client/src/block-provider/BlockProvider.tsx @@ -1,30 +1,46 @@ import { css } from '@emotion/css'; import { Field, GeneralField } from '@formily/core'; import { RecursionField, useField, useFieldSchema } from '@formily/react'; -import { useRequest } from 'ahooks'; import { Col, Row } from 'antd'; import merge from 'deepmerge'; import template from 'lodash/template'; -import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; import { + DataBlockProvider, TableFieldResource, WithoutTableFieldResource, - useAPIClient, - useActionContext, + useCollectionManager, + useDataBlockProps, + useDataBlockRequest, + useDataBlockResource, useDesignable, + useCollectionParentRecord, + useCollectionParentRecordData, + useCollectionRecord, useRecord, } from '../'; import { ACLCollectionProvider } from '../acl/ACLProvider'; -import { CollectionProvider, useCollection, useCollectionManager } from '../collection-manager'; -import { FilterBlockRecord } from '../filter-provider/FilterProvider'; -import { useRecordIndex } from '../record-provider'; -import { useTemplateBlockContext } from './TemplateBlockProvider'; +import { + CollectionProvider_deprecated, + useCollectionManager_deprecated, + useCollection_deprecated, +} from '../collection-manager'; +import { DataBlockCollector } from '../filter-provider/FilterProvider'; +import { RecordProvider, useRecordIndex } from '../record-provider'; import { useAssociationNames } from './hooks'; +import { useDataBlockSourceId } from './hooks/useDataBlockSourceId'; +/** + * @deprecated + */ export const BlockResourceContext = createContext(null); export const BlockAssociationContext = createContext(null); -export const BlockRequestContext = createContext<{ + +/** + * @deprecated + */ +export const BlockRequestContext_deprecated = createContext<{ block?: string; props?: any; field?: GeneralField; @@ -36,7 +52,8 @@ export const BlockRequestContext = createContext<{ }>({}); export const useBlockResource = () => { - return useContext(BlockResourceContext); + const resource = useDataBlockResource(); + return useContext(BlockResourceContext) || resource; }; interface UseResourceProps { @@ -44,12 +61,13 @@ interface UseResourceProps { association?: any; useSourceId?: any; collection?: any; + dataSource?: any; block?: any; } -export const useAssociation = (props) => { +const useAssociation = (props) => { const { association } = props; - const { getCollectionField } = useCollectionManager(); + const { getCollectionField } = useCollectionManager_deprecated(); if (typeof association === 'string') { return getCollectionField(association); } else if (association?.collectionName && association?.name) { @@ -57,139 +75,35 @@ export const useAssociation = (props) => { } }; -const useResource = (props: UseResourceProps) => { - const { block, collection, resource, useSourceId } = props; - const record = useRecord(); - const api = useAPIClient(); - const { fieldSchema } = useActionContext(); - const isCreateAction = fieldSchema?.['x-action'] === 'create'; - const association = useAssociation(props); - const sourceId = useSourceId?.(); - const field = useField(); - const withoutTableFieldResource = useContext(WithoutTableFieldResource); - const __parent = useContext(BlockRequestContext); - if (block === 'TableField') { - const options = { - field, - api, - resource, - sourceId: !isCreateAction - ? sourceId || record[association?.sourceKey || 'id'] || record?.__parent?.[association?.sourceKey || 'id'] - : undefined, - }; - return new TableFieldResource(options); - } - - if ( - !withoutTableFieldResource && - __parent?.block === 'TableField' && - __parent?.resource instanceof TableFieldResource - ) { - return __parent.resource; - } - if (!association) { - return api.resource(resource); - } - if (sourceId) { - return api.resource(resource, sourceId); - } - if (record?.__parent?.[association?.sourceKey || 'id']) { - return api.resource(resource, record.__parent[association?.sourceKey || 'id']); - } - if (record?.[association?.sourceKey || 'id']) { - return api.resource(resource, record[association?.sourceKey || 'id']); - } - return api.resource(collection); -}; - const useActionParams = (props) => { const { useParams } = props; const params = useParams?.() || {}; return { ...props.params, ...params }; }; -const useResourceAction = (props, opts = {}) => { - /** - * fieldName: 来自 TableFieldProvider - */ - const { resource, action, fieldName: tableFieldName, runWhenParamsChanged = false } = props; - const { fields } = useCollection(); - const params = useActionParams(props); - const api = useAPIClient(); - const fieldSchema = useFieldSchema(); - const { snapshot } = useActionContext(); - const { templateFinshed } = useTemplateBlockContext(); - const record = useRecord(); - const isTemplate = fieldSchema['x-template-key']; - if (!Reflect.has(params, 'appends')) { - const appends = fields?.filter((field) => field.target).map((field) => field.name); - if (appends?.length) { - params['appends'] = appends; - } - } - const result = useRequest( - snapshot - ? async () => ({ - data: record[tableFieldName] ?? [], - }) - : (opts) => { - if (!action || (isTemplate && !templateFinshed)) { - return Promise.resolve({}); - } - const actionParams = { ...params, ...opts }; - if (params?.appends) { - actionParams.appends = params.appends; - } - return resource[action](actionParams).then((res) => res.data); - }, - { - ...opts, - onSuccess(data, params) { - opts?.['onSuccess']?.(data, params); - if (fieldSchema['x-uid']) { - api.services[fieldSchema['x-uid']] = result; - } - }, - defaultParams: [params], - refreshDeps: [runWhenParamsChanged ? null : JSON.stringify(params.appends), templateFinshed], - }, - ); - // automatic run service when params has changed - const firstRun = useRef(false); - useEffect(() => { - if (!runWhenParamsChanged) { - return; - } - if (firstRun.current) { - result?.run({ ...result?.params?.[0], ...params }); - } - firstRun.current = true; - }, [JSON.stringify(params), runWhenParamsChanged]); - - return result; -}; - export const MaybeCollectionProvider = (props) => { const { collection } = props; return collection ? ( - + {props.children} - + ) : ( props.children ); }; -export const BlockRequestProvider = (props) => { +/** + * @deprecated + * @param props + * @returns + */ +export const BlockRequestProvider_deprecated = (props) => { const field = useField(); - const resource = useBlockResource(); + const resource = useDataBlockResource(); const [allowedActions, setAllowedActions] = useState({}); - const service = useResourceAction( - { ...props, resource }, - { - ...props.requestOptions, - }, - ); + const service = useDataBlockRequest(); + const record = useCollectionRecord(); + const parentRecord = useCollectionParentRecord(); // Infinite scroll support const serviceAllowedActions = (service?.data as any)?.meta?.allowedActions; @@ -200,9 +114,9 @@ export const BlockRequestProvider = (props) => { }); }, [serviceAllowedActions]); - const __parent = useContext(BlockRequestContext); + const __parent = useBlockRequestContext(); return ( - { updateAssociationValues: props?.updateAssociationValues || [], }} > - {props.children} - + {/* 用于兼容旧版 record.__parent 的写法 */} + + {props.children} + + ); }; +/** + * @deprecated + */ export const useBlockRequestContext = () => { - return useContext(BlockRequestContext); + return useContext(BlockRequestContext_deprecated); }; export const RenderChildrenWithAssociationFilter: React.FC = (props) => { @@ -294,54 +214,69 @@ export const useBlockContext = () => { return useContext(BlockContext); }; +/** + * @deprecated use `DataBlockProvider` instead + */ export const BlockProvider = (props: { name: string; resource: any; collection?: any; association?: any; + dataSource?: string; params?: any; children?: any; + parentRecord?: any; + /** @deprecated */ + useSourceId?: any; + /** @deprecated */ + useParams?: any; }) => { - const { collection, association, name } = props; - const resource = useResource(props); - const { getAssociationAppends } = useAssociationNames(); + const { name, dataSource, association, useParams, parentRecord } = props; + const sourceId = useDataBlockSourceId({ association }); + const paramsFromHook = useParams?.(); + const { getAssociationAppends } = useAssociationNames(dataSource); const { appends, updateAssociationValues } = getAssociationAppends(); const params = useMemo(() => { if (!props.params?.['appends']) { - return { ...props.params, appends }; + return { ...props.params, appends, ...paramsFromHook }; } - return { ...props.params }; - }, [appends, props.params]); + return { ...props.params, ...paramsFromHook }; + }, [appends, paramsFromHook, props.params]); const blockValue = useMemo(() => ({ name }), [name]); return ( - - - - - - {props.children} - - - - - + + + + {props.children} + + + ); }; export const useBlockAssociationContext = () => { - return useContext(BlockAssociationContext); + const { association } = useDataBlockProps(); + return useContext(BlockAssociationContext) || association; }; export const useFilterByTk = () => { - const { resource, __parent } = useContext(BlockRequestContext); + const { resource, __parent } = useBlockRequestContext(); const recordIndex = useRecordIndex(); const record = useRecord(); - const collection = useCollection(); - const { getCollectionField } = useCollectionManager(); - const assoc = useContext(BlockAssociationContext); + const collection = useCollection_deprecated(); + const { getCollectionField } = useCollectionManager_deprecated(); + const assoc = useBlockAssociationContext(); const withoutTableFieldResource = useContext(WithoutTableFieldResource); if (!withoutTableFieldResource) { if (resource instanceof TableFieldResource || __parent?.block === 'TableField') { @@ -356,32 +291,41 @@ export const useFilterByTk = () => { return record?.[collection.filterTargetKey || 'id']; }; +/** + * @deprecated + * 已弃用,应使用 useSourceIdFromParentRecord + * @returns + */ export const useSourceIdFromRecord = () => { const record = useRecord(); - const { getCollectionField } = useCollectionManager(); - const assoc = useContext(BlockAssociationContext); - if (assoc) { - const association = getCollectionField(assoc); - return record?.[association.sourceKey || 'id']; + const { getCollectionField } = useCollectionManager_deprecated(); + const association = useBlockAssociationContext(); + if (association) { + const collectionField = getCollectionField(association); + return record?.[collectionField.sourceKey || 'id']; } }; export const useSourceIdFromParentRecord = () => { - const record = useRecord(); - const { getCollectionField } = useCollectionManager(); - const assoc = useContext(BlockAssociationContext); - if (assoc) { - const association = getCollectionField(assoc); - return record?.__parent?.[association.sourceKey || 'id']; + const cm = useCollectionManager(); + const parentRecordData = useCollectionParentRecordData(); + const { getCollectionField } = useCollectionManager_deprecated(); + const association = useBlockAssociationContext(); + if (association) { + const collectionField = getCollectionField(association); + const collection = cm.getCollection(collectionField.collectionName); + return parentRecordData?.[ + collectionField.sourceKey || collection.filterTargetKey || collection.getPrimaryKey() || 'id' + ]; } }; export const useParamsFromRecord = () => { const filterByTk = useFilterByTk(); const record = useRecord(); - const { fields } = useCollection(); + const { fields } = useCollection_deprecated(); const fieldSchema = useFieldSchema(); - const { getCollectionJoinField } = useCollectionManager(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); const collectionField = getCollectionJoinField(fieldSchema?.['x-decorator-props']?.resource); const filterFields = fields .filter((v) => { diff --git a/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx b/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx index 5192b1208..fc08fc1e1 100644 --- a/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx +++ b/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx @@ -1,5 +1,9 @@ import React from 'react'; import { Plugin } from '../application/Plugin'; +import { ActionSchemaToolbar } from '../modules/actions/ActionSchemaToolbar'; +import { CollapseItemSchemaToolbar } from '../modules/blocks/filter-blocks/collapse/CollapseItemSchemaToolbar'; +import { FormItemSchemaToolbar } from '../modules/blocks/data-blocks/form/FormItemSchemaToolbar'; +import { TableColumnSchemaToolbar } from '../modules/blocks/data-blocks/table/TableColumnSchemaToolbar'; import { SchemaComponentOptions } from '../schema-component'; import { RecordLink, useParamsFromRecord, useSourceIdFromParentRecord, useSourceIdFromRecord } from './BlockProvider'; import { DetailsBlockProvider, useDetailsBlockProps } from './DetailsBlockProvider'; @@ -10,6 +14,7 @@ import { TableBlockProvider, useTableBlockProps } from './TableBlockProvider'; import { TableFieldProvider, useTableFieldProps } from './TableFieldProvider'; import { TableSelectorProvider, useTableSelectorProps } from './TableSelectorProvider'; import * as bp from './hooks'; +import { BlockSchemaToolbar } from '../modules/blocks/BlockSchemaToolbar'; // TODO: delete this, replaced by `BlockSchemaComponentPlugin` export const BlockSchemaComponentProvider: React.FC = (props) => { @@ -59,6 +64,11 @@ export class BlockSchemaComponentPlugin extends Plugin { FormFieldProvider, DetailsBlockProvider, RecordLink, + BlockSchemaToolbar, + ActionSchemaToolbar, + FormItemSchemaToolbar, + CollapseItemSchemaToolbar, + TableColumnSchemaToolbar, }); } diff --git a/packages/core/client/src/block-provider/DetailsBlockProvider.tsx b/packages/core/client/src/block-provider/DetailsBlockProvider.tsx index 57f0eb21c..e0987b2e2 100644 --- a/packages/core/client/src/block-provider/DetailsBlockProvider.tsx +++ b/packages/core/client/src/block-provider/DetailsBlockProvider.tsx @@ -3,7 +3,8 @@ import { useField } from '@formily/react'; import { Spin } from 'antd'; import _ from 'lodash'; import React, { createContext, useContext, useEffect, useMemo } from 'react'; -import { RecordProvider, useRecord } from '../record-provider'; +import { useCollectionParentRecord } from '../data-source/collection-record/CollectionRecordProvider'; +import { RecordProvider } from '../record-provider'; import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { useParsedFilter } from './hooks'; @@ -20,7 +21,7 @@ const InternalDetailsBlockProvider = (props) => { [], ); const { resource, service } = useBlockRequestContext(); - const record = useRecord(); + const parentRecord = useCollectionParentRecord(); const currentRecord = service?.data?.data?.[0] || {}; const detailsBLockValue = useMemo(() => { return { @@ -34,7 +35,6 @@ const InternalDetailsBlockProvider = (props) => { const { filter } = useParsedFilter({ filterOption: service?.params?.[0]?.filter, - currentRecord: { ...currentRecord, __parent: record, __collectionName: props.collection }, }); useEffect(() => { if (!_.isEmpty(filter)) { @@ -49,7 +49,9 @@ const InternalDetailsBlockProvider = (props) => { return ( - {props.children} + + {props.children} + ); }; diff --git a/packages/core/client/src/block-provider/FormBlockProvider.tsx b/packages/core/client/src/block-provider/FormBlockProvider.tsx index 263fec308..cce62abcb 100644 --- a/packages/core/client/src/block-provider/FormBlockProvider.tsx +++ b/packages/core/client/src/block-provider/FormBlockProvider.tsx @@ -1,15 +1,15 @@ import { createForm } from '@formily/core'; import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; import { Spin } from 'antd'; -import _, { isEmpty } from 'lodash'; import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react'; -import { useCollection } from '../collection-manager'; +import { useCollection_deprecated } from '../collection-manager'; +import { useCollectionParentRecordData, useCollectionRecord } from '../data-source'; import { RecordProvider, useRecord } from '../record-provider'; import { useActionContext, useDesignable } from '../schema-component'; import { Templates as DataTemplateSelect } from '../schema-component/antd/form-v2/Templates'; import { BlockProvider, useBlockRequestContext } from './BlockProvider'; -import { FormActiveFieldsProvider } from './hooks'; import { TemplateBlockProvider } from './TemplateBlockProvider'; +import { FormActiveFieldsProvider } from './hooks/useFormActiveFields'; export const FormBlockContext = createContext({}); @@ -26,7 +26,7 @@ const InternalFormBlockProvider = (props) => { ); const { resource, service, updateAssociationValues } = useBlockRequestContext(); const formBlockRef = useRef(); - const record = useRecord(); + const record = useCollectionRecord(); const formBlockValue = useMemo(() => { return { ...ctx, @@ -47,32 +47,16 @@ const InternalFormBlockProvider = (props) => { if (service.loading && Object.keys(form?.initialValues)?.length === 0 && action) { return ; } - let content = ( -
- -
- ); - if (readPretty) { - content = ( - - {content} - - ); - } else if ( - formBlockValue.type === 'create' && - // 关系表单区块的 record 应该是空的,因为其是一个创建数据的表单; - !_.isEmpty(_.omit(record, ['__parent', '__collectionName'])) && - // association 不为空,说明是关系区块 - association - ) { - content = ( - - {content} - - ); - } - return {content}; + return ( + + +
+ +
+
+
+ ); }; /** @@ -92,9 +76,10 @@ export const useIsDetailBlock = () => { export const FormBlockProvider = (props) => { const record = useRecord(); + const parentRecordData = useCollectionParentRecordData(); const { collection, isCusomeizeCreate } = props; const { __collection } = record; - const currentCollection = useCollection(); + const currentCollection = useCollection_deprecated(); const { designable } = useDesignable(); const isDetailBlock = useIsDetailBlock(); let detailFlag = false; @@ -106,16 +91,19 @@ export const FormBlockProvider = (props) => { } const createFlag = (currentCollection.name === (collection?.name || collection) && !isDetailBlock) || !currentCollection.name; + + if (!detailFlag && !createFlag && !isCusomeizeCreate) { + return null; + } + return ( - (detailFlag || createFlag || isCusomeizeCreate) && ( - - - - - - - - ) + + + + + + + ); }; diff --git a/packages/core/client/src/block-provider/FormFieldProvider.tsx b/packages/core/client/src/block-provider/FormFieldProvider.tsx index 6166ceb29..8d7a023a8 100644 --- a/packages/core/client/src/block-provider/FormFieldProvider.tsx +++ b/packages/core/client/src/block-provider/FormFieldProvider.tsx @@ -4,6 +4,7 @@ import { autorun } from '@formily/reactive'; import { forEach } from '@nocobase/utils/client'; import { Spin } from 'antd'; import React, { createContext, useContext, useEffect, useMemo } from 'react'; +import { useCollectionParentRecordData } from '../data-source/collection-record/CollectionRecordProvider'; import { RecordProvider } from '../record-provider'; import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { useFormBlockContext } from './FormBlockProvider'; @@ -13,6 +14,7 @@ export const FormFieldContext = createContext({}); const InternalFormFieldProvider = (props) => { const { action, readPretty, fieldName } = props; const formBlockCtx = useFormBlockContext(); + const parentRecordData = useCollectionParentRecordData(); if (!formBlockCtx?.updateAssociationValues?.includes(fieldName)) { formBlockCtx?.updateAssociationValues?.push(fieldName); @@ -55,7 +57,7 @@ const InternalFormFieldProvider = (props) => { } return ( - + { const resourceName = props.resource; const params = useMemo(() => ({ ...props.params }), [props.params]); const fieldSchema = useFieldSchema(); - const { getCollection, getCollectionField } = useCollectionManager(); - const record = useRecord(); - - const collection = getCollection(props.collection); + const { getCollection, getCollectionField } = useCollectionManager_deprecated(props.dataSource); + const collection = getCollection(props.collection, props.dataSource); const { treeTable, dragSortBy } = fieldSchema?.['x-decorator-props'] || {}; - if (props.dragSort) { - params['sort'] = dragSortBy || ['sort']; + if (props.dragSort && dragSortBy) { + params['sort'] = dragSortBy; } let childrenColumnName = 'children'; if (collection?.tree && treeTable !== false) { @@ -104,7 +101,6 @@ export const TableBlockProvider = (props) => { const form = useMemo(() => createForm(), [treeTable]); const { filter: parsedFilter } = useParsedFilter({ filterOption: params?.filter, - currentRecord: { __parent: record, __collectionName: props.collection }, }); const paramsWithFilter = useMemo(() => { return { @@ -152,7 +148,7 @@ export const useTableBlockProps = () => { childrenColumnName: ctx.childrenColumnName, loading: ctx?.service?.loading, showIndex: ctx.showIndex, - dragSort: ctx.dragSort, + dragSort: ctx.dragSort && ctx.dragSortBy, rowKey: ctx.rowKey || 'id', pagination: ctx?.params?.paginate !== false @@ -170,6 +166,7 @@ export const useTableBlockProps = () => { await ctx.resource.move({ sourceId: from[ctx.rowKey || 'id'], targetId: to[ctx.rowKey || 'id'], + sortField: ctx.dragSort && ctx.dragSortBy, }); ctx.service.refresh(); }, diff --git a/packages/core/client/src/block-provider/TableSelectorProvider.tsx b/packages/core/client/src/block-provider/TableSelectorProvider.tsx index a9ff00751..9f7fe8738 100644 --- a/packages/core/client/src/block-provider/TableSelectorProvider.tsx +++ b/packages/core/client/src/block-provider/TableSelectorProvider.tsx @@ -3,7 +3,8 @@ import { Schema, useField, useFieldSchema } from '@formily/react'; import _ from 'lodash'; import uniq from 'lodash/uniq'; import React, { createContext, useContext, useEffect, useState } from 'react'; -import { useCollectionManager } from '../collection-manager'; +import { useCollectionManager_deprecated } from '../collection-manager'; +import { useCollectionParentRecordData } from '../data-source/collection-record/CollectionRecordProvider'; import { isInFilterFormBlock } from '../filter-provider'; import { mergeFilter } from '../filter-provider/utils'; import { RecordProvider, useRecord } from '../record-provider'; @@ -49,11 +50,12 @@ const InternalTableSelectorProvider = (props) => { const field = useField(); const { resource, service } = useBlockRequestContext(); const [expandFlag, setExpandFlag] = useState(false); + const parentRecordData = useCollectionParentRecordData(); // if (service.loading) { // return ; // } return ( - + { }; const useAssociationNames2 = (collection) => { - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(); const names = getCollectionFields(collection) ?.filter((field) => field.target) .map((field) => field.name); @@ -91,7 +93,7 @@ export const recursiveParent = (schema: Schema, component) => { }; const useAssociationNames = (collection) => { - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(); const collectionFields = getCollectionFields(collection); const associationFields = new Set(); for (const collectionField of collectionFields) { @@ -140,13 +142,14 @@ const useAssociationNames = (collection) => { export const TableSelectorProvider = (props: TableSelectorProviderProps) => { const parentParams = useTableSelectorParams(); const fieldSchema = useFieldSchema(); - const { getCollectionJoinField, getCollectionFields } = useCollectionManager(); + const { getCollectionJoinField, getCollectionFields } = useCollectionManager_deprecated(); const record = useRecord(); - const { getCollection } = useCollectionManager(); - const collection = getCollection(props.collection); + const { getCollection } = useCollectionManager_deprecated(); const { treeTable } = fieldSchema?.['x-decorator-props'] || {}; const collectionFieldSchema = recursiveParent(fieldSchema, 'CollectionField'); const collectionField = getCollectionJoinField(collectionFieldSchema?.['x-collection-field']); + const collection = getCollection(collectionField?.collectionName); + const primaryKey = collection?.getPrimaryKey(); const appends = useAssociationNames(props.collection); let params = { ...props.params }; if (props.dragSort) { @@ -157,8 +160,8 @@ export const TableSelectorProvider = (props: TableSelectorProviderProps) => { if (collectionFieldSchema.name === 'parent') { params.filter = { ...(params.filter ?? {}), - id: record.id && { - $ne: record.id, + id: record[primaryKey] && { + $ne: record[primaryKey], }, }; } @@ -241,7 +244,6 @@ export const TableSelectorProvider = (props: TableSelectorProviderProps) => { const { filter: parsedFilter } = useParsedFilter({ filterOption: params?.filter, - currentRecord: { __parent: record, __collectionName: props.collection }, }); if (!_.isEmpty(params?.filter) && _.isEmpty(parsedFilter)) { @@ -269,7 +271,7 @@ export const useTableSelectorProps = () => { const field = useField(); const ctx = useTableSelectorContext(); const fieldSchema = useFieldSchema(); - const { getCollectionJoinField } = useCollectionManager(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); const collectionFieldSchema = recursiveParent(fieldSchema, 'CollectionField'); const collectionField = getCollectionJoinField(collectionFieldSchema?.['x-collection-field']); useEffect(() => { diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index d669f9700..b8602f593 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -8,9 +8,16 @@ import { ChangeEvent, useCallback, useContext, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { useReactToPrint } from 'react-to-print'; -import { AssociationFilter, useFormActiveFields, useFormBlockContext, useTableBlockContext } from '../..'; +import { + AssociationFilter, + useCollectionManager, + useDataSourceHeaders, + useFormActiveFields, + useFormBlockContext, + useTableBlockContext, +} from '../..'; import { useAPIClient, useRequest } from '../../api-client'; -import { useCollection, useCollectionManager } from '../../collection-manager'; +import { useCollectionManager_deprecated, useCollection_deprecated } from '../../collection-manager'; import { useFilterBlock } from '../../filter-provider/FilterProvider'; import { mergeFilter, transformToFilter } from '../../filter-provider/utils'; import { useRecord } from '../../record-provider'; @@ -99,7 +106,7 @@ export function useCollectValuesToSubmit() { const form = useForm(); const filterByTk = useFilterByTk(); const { field, resource } = useBlockRequestContext(); - const { fields, getField, getTreeParentField, name } = useCollection(); + const { fields, getField, getTreeParentField, name } = useCollection_deprecated(); const fieldNames = fields.map((field) => field.name); const { fieldSchema } = useActionContext(); const { getActiveFieldsName } = useFormActiveFields() || {}; @@ -253,7 +260,7 @@ export const useAssociationCreateActionProps = () => { const { setVisible, fieldSchema } = useActionContext(); const actionSchema = useFieldSchema(); const actionField = useField(); - const { fields, getField, getTreeParentField, name } = useCollection(); + const { fields, getField, getTreeParentField, name } = useCollection_deprecated(); const compile = useCompile(); const filterByTk = useFilterByTk(); const currentRecord = useRecord(); @@ -383,8 +390,8 @@ export const useFilterBlockActionProps = () => { const actionField = useField(); const fieldSchema = useFieldSchema(); const { getDataBlocks } = useFilterBlock(); - const { name } = useCollection(); - const { getCollectionJoinField } = useCollectionManager(); + const { name } = useCollection_deprecated(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); actionField.data = actionField.data || {}; @@ -488,7 +495,7 @@ export const useCustomizeUpdateActionProps = () => { const { modal } = App.useApp(); const variables = useVariables(); const localVariables = useLocalVariables({ currentForm: form }); - const { name, getField } = useCollection(); + const { name, getField } = useCollection_deprecated(); return { async onClick() { @@ -576,8 +583,8 @@ export const useCustomizeBulkUpdateActionProps = () => { const { modal } = App.useApp(); const variables = useVariables(); const record = useRecord(); - const { name, getField } = useCollection(); - const localVariables = useLocalVariables({ currentRecord: { __parent: record, __collectionName: name } }); + const { name, getField } = useCollection_deprecated(); + const localVariables = useLocalVariables(); return { async onClick() { @@ -685,7 +692,7 @@ export const useCustomizeRequestActionProps = () => { const actionSchema = useFieldSchema(); const compile = useCompile(); const form = useForm(); - const { fields, getField } = useCollection(); + const { fields, getField } = useCollection_deprecated(); const { field, resource, __parent, service } = useBlockRequestContext(); const currentRecord = useRecord(); const currentUserContext = useCurrentUserContext(); @@ -777,7 +784,7 @@ export const useUpdateActionProps = () => { const { setVisible } = useActionContext(); const actionSchema = useFieldSchema(); const navigate = useNavigate(); - const { fields, getField, name } = useCollection(); + const { fields, getField, name } = useCollection_deprecated(); const compile = useCompile(); const actionField = useField(); const { updateAssociationValues } = useFormBlockContext(); @@ -1001,14 +1008,17 @@ export const useAssociationFilterProps = () => { const collectionField = AssociationFilter.useAssociationField(); const { service, props: blockProps } = useBlockRequestContext(); const fieldSchema = useFieldSchema(); - const valueKey = collectionField?.targetKey || 'id'; + const cm = useCollectionManager_deprecated(); + const valueKey = collectionField?.target ? cm.getCollection(collectionField.target)?.getPrimaryKey() : 'id'; const labelKey = fieldSchema['x-component-props']?.fieldNames?.label || valueKey; const field = useField(); const collectionFieldName = collectionField.name; + const headers = useDataSourceHeaders(blockProps?.dataSource); const { data, params, run } = useRequest<{ data: { [key: string]: any }[]; }>( { + headers, resource: collectionField.target, action: 'list', params: { @@ -1064,7 +1074,7 @@ export const useAssociationFilterProps = () => { }; export const useOptionalFieldList = () => { - const { currentFields = [] } = useCollection(); + const { currentFields = [] } = useCollection_deprecated(); return currentFields.filter((field) => isOptionalField(field) && field.uiSchema.enum); }; @@ -1081,10 +1091,13 @@ export const useAssociationFilterBlockProps = () => { const { getDataBlocks } = useFilterBlock(); const collectionFieldName = collectionField?.name; const field = useField(); + const { props: blockProps } = useBlockRequestContext(); + const headers = useDataSourceHeaders(blockProps?.dataSource); + const cm = useCollectionManager_deprecated(); let list, handleSearchInput, params, run, data, valueKey, labelKey, filterKey; - valueKey = collectionField?.targetKey || 'id'; + valueKey = collectionField?.target ? cm.getCollection(collectionField.target)?.getPrimaryKey() : 'id'; labelKey = fieldSchema['x-component-props']?.fieldNames?.label || valueKey; // eslint-disable-next-line prefer-const @@ -1092,6 +1105,7 @@ export const useAssociationFilterBlockProps = () => { data: { [key: string]: any }[]; }>( { + headers, resource: collectionField?.target, action: 'list', params: { @@ -1207,15 +1221,15 @@ export function getAssociationPath(str) { return str; } -export const useAssociationNames = () => { +export const useAssociationNames = (dataSource?: string) => { let updateAssociationValues = new Set([]); let appends = new Set([]); - const { getCollectionJoinField, getCollection } = useCollectionManager(); + const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated(dataSource); const fieldSchema = useFieldSchema(); const _getAssociationAppends = (schema, str) => { schema.reduceProperties((pre, s) => { const prefix = pre || str; - const collectionField = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field']); + const collectionField = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field'], dataSource); const isAssociationSubfield = s.name.includes('.'); const isAssociationField = collectionField && ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(collectionField.type); diff --git a/packages/core/client/src/block-provider/hooks/useDataBlockSourceId.tsx b/packages/core/client/src/block-provider/hooks/useDataBlockSourceId.tsx new file mode 100644 index 000000000..5089b848b --- /dev/null +++ b/packages/core/client/src/block-provider/hooks/useDataBlockSourceId.tsx @@ -0,0 +1,34 @@ +import { useFieldSchema } from '@formily/react'; +import { useCollection, useCollectionManager, useCollectionParentRecordData, useCollectionRecordData } from '../..'; + +/** + * 注意:这里有一个需要更改 schema 才能解决的问题,就是在获取 sourceId 的时候无法确定(在关系字段和当前表同表时) + * 是需要从 recordData 还是 parentRecordData 中获取;解决方法是通过更改 schema,在不同类型的关系区块中 + * (`通过点击关系字段按钮打开的弹窗中创建的非关系字段区块`和`关系字段区块`)使用不同的 hook。 + * @param param0 + * @returns + */ +export const useDataBlockSourceId = ({ association }: { association: string }) => { + const recordData = useCollectionRecordData(); + const parentRecordData = useCollectionParentRecordData(); + const cm = useCollectionManager(); + const collectionOutsideBlock = useCollection(); + + if (!association) return; + + const associationField = cm.getCollectionField(association); + const associationCollection = cm.getCollection(associationField.collectionName); + + if (collectionOutsideBlock.name === associationCollection.name) { + return recordData?.[ + associationField.sourceKey || + associationCollection.filterTargetKey || + associationCollection.getPrimaryKey() || + 'id' + ]; + } + + return parentRecordData?.[ + associationField.sourceKey || associationCollection.filterTargetKey || associationCollection.getPrimaryKey() || 'id' + ]; +}; diff --git a/packages/core/client/src/block-provider/hooks/useParsedFilter.ts b/packages/core/client/src/block-provider/hooks/useParsedFilter.ts index 8f7bd400b..aa732654d 100644 --- a/packages/core/client/src/block-provider/hooks/useParsedFilter.ts +++ b/packages/core/client/src/block-provider/hooks/useParsedFilter.ts @@ -8,8 +8,8 @@ import { getPath } from '../../variables/utils/getPath'; import { getVariableName } from '../../variables/utils/getVariableName'; import { isVariable } from '../../variables/utils/isVariable'; -export function useParsedFilter({ filterOption, currentRecord }: { filterOption: any; currentRecord?: any }) { - const { parseFilter, findVariable } = useParseDataScopeFilter({ currentRecord }); +export function useParsedFilter({ filterOption }: { filterOption: any }) { + const { parseFilter, findVariable } = useParseDataScopeFilter(); const [filter, setFilter] = useState({}); useEffect(() => { diff --git a/packages/core/client/src/collection-manager/CollectionFieldProvider.tsx b/packages/core/client/src/collection-manager/CollectionFieldProvider.tsx deleted file mode 100644 index f4e9351aa..000000000 --- a/packages/core/client/src/collection-manager/CollectionFieldProvider.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { SchemaKey, useFieldSchema } from '@formily/react'; -import React from 'react'; -import { CollectionFieldContext } from './context'; -import { useCollection, useCollectionManager } from './hooks'; -import { CollectionFieldOptions } from './types'; - -export const CollectionFieldProvider: React.FC<{ - name?: SchemaKey; - field?: CollectionFieldOptions; - fallback?: React.ReactElement; -}> = (props) => { - const { name, field, children, fallback = null } = props; - const fieldSchema = useFieldSchema(); - const { getField } = useCollection(); - const { getCollectionJoinField } = useCollectionManager(); - const value = getCollectionJoinField(fieldSchema?.['x-collection-field']) || field || getField(field?.name || name); - if (!value) { - return fallback; - } - return ( - - {children} - - ); -}; diff --git a/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx b/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx index 82accd272..ba42906ae 100644 --- a/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx +++ b/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx @@ -1,54 +1,29 @@ -import { keyBy } from 'lodash'; -import React, { useContext, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import React from 'react'; import { useAPIClient, useRequest } from '../api-client'; -import { useAppSpin } from '../application/hooks/useAppSpin'; -import { templateOptions } from '../collection-manager/Configuration/templates'; -import { useCollectionHistory } from './CollectionHistoryProvider'; import { CollectionManagerSchemaComponentProvider } from './CollectionManagerSchemaComponentProvider'; -import { CollectionCategroriesContext, CollectionManagerContext } from './context'; -import * as defaultInterfaces from './interfaces'; +import { CollectionCategroriesContext } from './context'; import { CollectionManagerOptions } from './types'; +import { CollectionManagerProvider } from '../data-source/collection/CollectionManagerProvider'; +import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider'; +import { useCollectionHistory } from './CollectionHistoryProvider'; +import { useAppSpin } from '../application/hooks/useAppSpin'; -export const CollectionManagerProvider: React.FC = (props) => { - const { service, interfaces, collections = [], refreshCM, updateCollection, templates } = props; - const defaultTemplates = keyBy(templateOptions(), (item) => item.name); - const ctx = useContext(CollectionManagerContext); +/** + * @deprecated use `CollectionManagerProvider` instead + */ +export const CollectionManagerProvider_deprecated: React.FC = (props) => { return ( - + {props.children} - + ); }; export const RemoteCollectionManagerProvider = (props: any) => { - const { t } = useTranslation('lm-collections'); const api = useAPIClient(); - const [contentLoading, setContentLoading] = useState(false); + const dm = useDataSourceManager(); const { refreshCH } = useCollectionHistory(); - const { render } = useAppSpin(); - const options = { - resource: 'collections', - action: 'list', - params: { - paginate: false, - appends: ['fields', 'category'], - filter: { - // inherit: false, - }, - sort: ['sort'], - }, - }; + const coptions = { url: 'collectionCategories:list', params: { @@ -58,67 +33,26 @@ export const RemoteCollectionManagerProvider = (props: any) => { }; const service = useRequest<{ data: any; - }>(options); + }>(() => { + return dm.reload().then(refreshCH); + }); const result = useRequest<{ data: any; }>(coptions); + const { render } = useAppSpin(); if (service.loading) { return render(); } - const refreshCM = async (opts) => { - if (opts?.reload) { - setContentLoading(true); - } - const { data } = await api.request(options); - service.mutate(data); - await refreshCH(); - if (opts?.reload) { - setContentLoading(false); - } - return data?.data || []; - }; + const refreshCategory = async () => { const { data } = await api.request(coptions); result.mutate(data); return data?.data || []; }; - - const updateCollection = (collection) => { - service.mutate({ data: collection }); - }; - - const collections = (service?.data?.data || []).map(({ rawTitle, title, fields, ...collection }) => ({ - ...collection, - title: rawTitle ? title : t(title), - rawTitle: rawTitle || title, - fields: fields.map(({ uiSchema, ...field }) => { - if (uiSchema?.title) { - const title = uiSchema.title; - uiSchema.title = uiSchema.rawTitle ? title : t(title); - uiSchema.rawTitle = uiSchema.rawTitle || title; - } - if (uiSchema?.enum) { - uiSchema.enum = uiSchema.enum.map((item) => ({ - ...item, - value: item?.value || item, - label: item.rawLabel ? item.label : t(item.label), - rawLabel: item.rawLabel || item.label, - })); - } - return { uiSchema, ...field }; - }), - })); - return ( - + ); }; diff --git a/packages/core/client/src/collection-manager/CollectionManagerSchemaComponentProvider.tsx b/packages/core/client/src/collection-manager/CollectionManagerSchemaComponentProvider.tsx index 8ce6ea2b9..70194df24 100644 --- a/packages/core/client/src/collection-manager/CollectionManagerSchemaComponentProvider.tsx +++ b/packages/core/client/src/collection-manager/CollectionManagerSchemaComponentProvider.tsx @@ -1,25 +1,17 @@ import React from 'react'; -import { SchemaComponentOptions } from '../'; -import { - CollectionField, - CollectionFieldProvider, - CollectionProvider, - ResourceActionProvider, - useDataSourceFromRAC, -} from './'; +import { SchemaComponentOptions } from '..'; +import { CollectionProvider_deprecated, ResourceActionProvider, useDataSourceFromRAC } from '.'; import * as hooks from './action-hooks'; -import { DataSourceProvider, ds, SubFieldDataSourceProvider } from './sub-table'; +import { DataSourceProvider_deprecated, ds, SubFieldDataSourceProvider_deprecated } from './sub-table'; export const CollectionManagerSchemaComponentProvider: React.FC = (props) => { return ( diff --git a/packages/core/client/src/collection-manager/CollectionProvider.tsx b/packages/core/client/src/collection-manager/CollectionProvider.tsx deleted file mode 100644 index 63aa61ea2..000000000 --- a/packages/core/client/src/collection-manager/CollectionProvider.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import { CollectionContext } from './context'; -import { useCollectionManager } from './hooks'; -import { CollectionOptions } from './types'; - -export const CollectionProvider: React.FC<{ allowNull?: boolean; name?: string; collection?: CollectionOptions }> = ( - props, -) => { - const { allowNull, name, collection, children } = props; - const { getCollection } = useCollectionManager(); - const value = getCollection(collection || name); - if (!value && !allowNull) { - return null; - } - return {children}; -}; diff --git a/packages/core/client/src/collection-manager/CollectionProvider_deprecated.tsx b/packages/core/client/src/collection-manager/CollectionProvider_deprecated.tsx new file mode 100644 index 000000000..56b66b26d --- /dev/null +++ b/packages/core/client/src/collection-manager/CollectionProvider_deprecated.tsx @@ -0,0 +1,32 @@ +import { FC, ReactNode } from 'react'; +import { CollectionProvider } from '../data-source/collection/CollectionProvider'; +import { CollectionManagerProvider } from '../data-source/collection/CollectionManagerProvider'; +import { CollectionOptions } from '../data-source/collection/Collection'; +import React from 'react'; + +/** + * @deprecated use `CollectionProvider` instead + */ +export const CollectionProvider_deprecated: FC<{ + name?: string; + collection?: CollectionOptions | string; + allowNull?: boolean; + children?: ReactNode; + dataSource?: string; +}> = ({ children, allowNull, name, dataSource, collection }) => { + if (dataSource) { + return ( + + + {children} + + + ); + } + + return ( + + {children} + + ); +}; diff --git a/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx b/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx index b76395cbf..82ea4e993 100644 --- a/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx @@ -11,10 +11,9 @@ import { RecordProvider, useRecord } from '../../record-provider'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; import { useCancelAction } from '../action-hooks'; -import { useCollectionManager } from '../hooks'; +import { useCollectionManager_deprecated } from '../hooks'; import * as components from './components'; import { TemplateSummary } from './components/TemplateSummary'; -import { templateOptions } from './templates'; const getSchema = (schema, category, compile): ISchema => { if (!schema) { @@ -97,105 +96,9 @@ const getSchema = (schema, category, compile): ISchema => { }; }; -const getDefaultCollectionFields = (values) => { - if (values?.template === 'view' || values?.template === 'sql') { - return values.fields; - } - const defaults = values.fields ? [...values.fields] : []; - const { autoGenId = true, createdAt = true, createdBy = true, updatedAt = true, updatedBy = true } = values; - if (autoGenId) { - const pk = values.fields.find((f) => f.primaryKey); - if (!pk) { - defaults.push({ - name: 'id', - type: 'bigInt', - autoIncrement: true, - primaryKey: true, - allowNull: false, - uiSchema: { type: 'number', title: '{{t("ID")}}', 'x-component': 'InputNumber', 'x-read-pretty': true }, - interface: 'id', - }); - } - } - if (createdAt) { - defaults.push({ - name: 'createdAt', - interface: 'createdAt', - type: 'date', - field: 'createdAt', - uiSchema: { - type: 'datetime', - title: '{{t("Created at")}}', - 'x-component': 'DatePicker', - 'x-component-props': {}, - 'x-read-pretty': true, - }, - }); - } - if (createdBy) { - defaults.push({ - name: 'createdBy', - interface: 'createdBy', - type: 'belongsTo', - target: 'users', - foreignKey: 'createdById', - uiSchema: { - type: 'object', - title: '{{t("Created by")}}', - 'x-component': 'AssociationField', - 'x-component-props': { - fieldNames: { - value: 'id', - label: 'nickname', - }, - }, - 'x-read-pretty': true, - }, - }); - } - if (updatedAt) { - defaults.push({ - type: 'date', - field: 'updatedAt', - name: 'updatedAt', - interface: 'updatedAt', - uiSchema: { - type: 'string', - title: '{{t("Last updated at")}}', - 'x-component': 'DatePicker', - 'x-component-props': {}, - 'x-read-pretty': true, - }, - }); - } - if (updatedBy) { - defaults.push({ - type: 'belongsTo', - target: 'users', - foreignKey: 'updatedById', - name: 'updatedBy', - interface: 'updatedBy', - uiSchema: { - type: 'object', - title: '{{t("Last updated by")}}', - 'x-component': 'AssociationField', - 'x-component-props': { - fieldNames: { - value: 'id', - label: 'nickname', - }, - }, - 'x-read-pretty': true, - }, - }); - } - // 其他 - return defaults; -}; - const useCreateCollection = (schema?: any) => { const form = useForm(); - const { refreshCM } = useCollectionManager(); + const { refreshCM } = useCollectionManager_deprecated(); const ctx = useActionContext(); const { refresh } = useResourceActionContext(); const { resource } = useResourceContext(); @@ -210,18 +113,14 @@ const useCreateCollection = (schema?: any) => { if (schema?.events?.beforeSubmit) { schema.events.beforeSubmit(values); } - const fields = getDefaultCollectionFields(values); if (!values.autoCreateReverseField) { delete values.reverseField; } - delete values.id; delete values.autoCreateReverseField; - await resource.create({ values: { logging: true, ...values, - fields, }, }); ctx.setVisible(false); @@ -237,18 +136,17 @@ const useCreateCollection = (schema?: any) => { }; export const AddCollection = (props) => { - const record = useRecord(); - return ; + const recordData = useRecord(); + return ; }; export const AddCollectionAction = (props) => { const { scope, getContainer, item: record, children, trigger, align } = props; - const { getTemplate } = useCollectionManager(); + const { getTemplate, templates: collectionTemplates } = useCollectionManager_deprecated(); const [visible, setVisible] = useState(false); const [schema, setSchema] = useState({}); const compile = useCompile(); const { t } = useTranslation(); - const collectionTemplates = useMemo(templateOptions, []); const items = useMemo(() => { const result = []; collectionTemplates.forEach((item) => { diff --git a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx index bf869e10e..74156fe1d 100644 --- a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx @@ -11,11 +11,11 @@ import { RecordProvider, useRecord } from '../../record-provider'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; import { useCancelAction } from '../action-hooks'; -import { useCollectionManager } from '../hooks'; +import { useCollectionManager_deprecated } from '../hooks'; import useDialect from '../hooks/useDialect'; import { IField } from '../interfaces/types'; import * as components from './components'; -import { getOptions } from './interfaces'; +import { useFieldInterfaceOptions } from './interfaces'; const getSchema = (schema: IField, record: any, compile) => { if (!schema) { @@ -29,24 +29,41 @@ const getSchema = (schema: IField, record: any, compile) => { properties.defaultValue.required = false; properties['defaultValue']['title'] = compile('{{ t("Default value") }}'); properties['defaultValue']['x-decorator'] = 'FormItem'; - properties['defaultValue']['x-reactions'] = { - dependencies: [ - 'uiSchema.x-component-props.gmt', - 'uiSchema.x-component-props.showTime', - 'uiSchema.x-component-props.dateFormat', - 'uiSchema.x-component-props.timeFormat', - ], - fulfill: { - state: { - componentProps: { - gmt: '{{$deps[0]}}', - showTime: '{{$deps[1]}}', - dateFormat: '{{$deps[2]}}', - timeFormat: '{{$deps[3]}}', + properties['defaultValue']['x-reactions'] = [ + { + dependencies: [ + 'uiSchema.x-component-props.gmt', + 'uiSchema.x-component-props.showTime', + 'uiSchema.x-component-props.dateFormat', + 'uiSchema.x-component-props.timeFormat', + ], + fulfill: { + state: { + componentProps: { + gmt: '{{$deps[0]}}', + showTime: '{{$deps[1]}}', + dateFormat: '{{$deps[2]}}', + timeFormat: '{{$deps[3]}}', + }, }, }, }, - }; + { + dependencies: ['primaryKey', 'unique', 'autoIncrement'], + when: '{{$deps[0]||$deps[1]||$deps[2]}}', + fulfill: { + state: { + hidden: true, + value: null, + }, + }, + otherwise: { + state: { + hidden: false, + }, + }, + }, + ]; } const initialValue: any = { name: `f_${uid()}`, @@ -140,7 +157,7 @@ export const useCollectionFieldFormValues = () => { const useCreateCollectionField = () => { const form = useForm(); - const { refreshCM } = useCollectionManager(); + const { refreshCM } = useCollectionManager_deprecated(); const ctx = useActionContext(); const { refresh } = useResourceActionContext(); const { resource } = useResourceContext(); @@ -178,13 +195,15 @@ export const AddCollectionField = (props) => { export const AddFieldAction = (props) => { const { scope, getContainer, item: record, children, trigger, align, database } = props; - const { getInterface, getTemplate, collections } = useCollectionManager(); + const { getInterface, getTemplate, collections, getCollection } = useCollectionManager_deprecated(); const [visible, setVisible] = useState(false); const [targetScope, setTargetScope] = useState(); const [schema, setSchema] = useState({}); const compile = useCompile(); const { t } = useTranslation(); const { isDialect } = useDialect(); + const options = useFieldInterfaceOptions(); + const fields = getCollection(record.name)?.options?.fields || record.fields || []; const currentCollections = useMemo(() => { return collections.map((v) => { @@ -196,15 +215,15 @@ export const AddFieldAction = (props) => { }, []); const getFieldOptions = useCallback(() => { const { availableFieldInterfaces } = getTemplate(record.template) || {}; - const { exclude, include } = availableFieldInterfaces || {}; + const { exclude, include } = (availableFieldInterfaces || {}) as any; const optionArr = []; - getOptions().forEach((v) => { + options.forEach((v) => { if (v.key === 'systemInfo') { optionArr.push({ ...v, children: v.children.filter((v) => { - if (v.value === 'id') { - return typeof record['autoGenId'] === 'boolean' ? record['autoGenId'] : true; + if (v.hidden) { + return false; } else if (v.value === 'tableoid') { if (include?.length) { return include.includes(v.value); @@ -219,7 +238,7 @@ export const AddFieldAction = (props) => { let children = []; if (include?.length) { include.forEach((k) => { - const field = v?.children?.find((h) => [k, k.interface].includes(h.value)); + const field = v?.children?.find((h) => [k, k.interface].includes(h.name)); field && children.push({ ...field, @@ -228,7 +247,7 @@ export const AddFieldAction = (props) => { }); } else if (exclude?.length) { children = v?.children?.filter((v) => { - return !exclude.includes(v.value); + return !exclude.includes(v.name); }); } else { children = v?.children; @@ -304,6 +323,18 @@ export const AddFieldAction = (props) => { items, }; }, [getInterface, items, record]); + const scopeKeyOptions = useMemo(() => { + return fields + .filter((v) => { + return ['string', 'bigInt', 'integer'].includes(v.type); + }) + .map((k) => { + return { + value: k.name, + label: compile(k.uiSchema?.title), + }; + }); + }, [fields?.length]); return ( record.template !== 'sql' && ( @@ -331,6 +362,8 @@ export const AddFieldAction = (props) => { collections: currentCollections, isDialect, disabledJSONB: false, + scopeKeyOptions, + createMainOnly: true, ...scope, }} /> diff --git a/packages/core/client/src/collection-manager/Configuration/AddSubFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/AddSubFieldAction.tsx index 72b305ba3..a8a723344 100644 --- a/packages/core/client/src/collection-manager/Configuration/AddSubFieldAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/AddSubFieldAction.tsx @@ -7,12 +7,13 @@ import { cloneDeep } from 'lodash'; import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useRequest } from '../../api-client'; +import { useCollectionRecordData } from '../../data-source'; import { RecordProvider } from '../../record-provider'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; -import { useCollectionManager } from '../hooks'; -import { useOptions } from '../hooks/useOptions'; +import { useCollectionManager_deprecated } from '../hooks'; import { IField } from '../interfaces/types'; import * as components from './components'; +import { useFieldInterfaceOptions } from './interfaces'; const getSchema = (schema: IField): ISchema => { if (!schema) { @@ -95,11 +96,11 @@ const useCreateSubField = () => { }; export const AddSubFieldAction = () => { - const { getInterface } = useCollectionManager(); + const { getInterface } = useCollectionManager_deprecated(); const [visible, setVisible] = useState(false); const [schema, setSchema] = useState({}); const compile = useCompile(); - const options = useOptions(); + const options = useFieldInterfaceOptions(); const { t } = useTranslation(); const items = useMemo(() => { return options.map((option) => { @@ -127,6 +128,7 @@ export const AddSubFieldAction = () => { items, }; }, [items]); + const recordData = useCollectionRecordData(); return ( @@ -135,7 +137,7 @@ export const AddSubFieldAction = () => { {t('Add field')} - + { const record = useRecord(); @@ -17,7 +22,7 @@ export const DeleteCollection = (props) => { export const useDestroyActionAndRefreshCM = () => { const { run } = useDestroyAction(); - const { refreshCM } = useCollectionManager(); + const { refreshCM } = useCollectionManager_deprecated(); return { async run() { await run(); @@ -27,7 +32,7 @@ export const useDestroyActionAndRefreshCM = () => { }; export const useBulkDestroyActionAndRefreshCM = () => { const { run } = useBulkDestroyAction(); - const { refreshCM } = useCollectionManager(); + const { refreshCM } = useCollectionManager_deprecated(); return { async run() { await run(); @@ -39,13 +44,11 @@ export const useDestroyAction = () => { const { refresh } = useResourceActionContext(); const { resource, targetKey } = useResourceContext(); const { [targetKey]: filterByTk } = useRecord(); - const ctx = useActionContext(); const form = useForm(); const { cascade } = form?.values || {}; return { async run() { await resource.destroy({ filterByTk, cascade }); - ctx?.setVisible?.(false); refresh(); }, }; diff --git a/packages/core/client/src/collection-manager/Configuration/EditCollectionAction.tsx b/packages/core/client/src/collection-manager/Configuration/EditCollectionAction.tsx index e36d86342..7595e29c5 100644 --- a/packages/core/client/src/collection-manager/Configuration/EditCollectionAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/EditCollectionAction.tsx @@ -10,7 +10,7 @@ import { RecordProvider, useRecord } from '../../record-provider'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; import { useCancelAction } from '../action-hooks'; -import { useCollectionManager } from '../hooks'; +import { useCollectionManager_deprecated } from '../hooks'; import { IField } from '../interfaces/types'; import * as components from './components'; @@ -101,7 +101,7 @@ export const useValuesFromRecord = (options) => { }; export const useUpdateCollectionActionAndRefreshCM = (options) => { - const { refreshCM } = useCollectionManager(); + const { refreshCM } = useCollectionManager_deprecated(); const form = useForm(); const ctx = useActionContext(); const { refresh } = useResourceActionContext(); @@ -131,7 +131,7 @@ export const EditCollection = (props) => { export const EditCollectionAction = (props) => { const { scope, getContainer, item: record, children, ...otherProps } = props; - const { getTemplate } = useCollectionManager(); + const { getTemplate } = useCollectionManager_deprecated(); const [visible, setVisible] = useState(false); const [schema, setSchema] = useState({}); const { t } = useTranslation(); @@ -143,7 +143,7 @@ export const EditCollectionAction = (props) => { { - const templateConf = getTemplate(record.template); + const templateConf: any = getTemplate(record.template); const schema = getSchema( { ...templateConf, diff --git a/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx index b5407d3a5..98a175f50 100644 --- a/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx @@ -7,11 +7,12 @@ import set from 'lodash/set'; import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAPIClient, useRequest } from '../../api-client'; +import { useCollectionParentRecordData } from '../../data-source/collection-record/CollectionRecordProvider'; import { RecordProvider, useRecord } from '../../record-provider'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; -import { useCancelAction, useUpdateAction } from '../action-hooks'; -import { useCollectionManager } from '../hooks'; +import { useCancelAction } from '../action-hooks'; +import { useCollectionManager_deprecated } from '../hooks'; import useDialect from '../hooks/useDialect'; import { IField } from '../interfaces/types'; import * as components from './components'; @@ -30,24 +31,41 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema properties.defaultValue.required = false; properties['defaultValue']['title'] = compile('{{ t("Default value") }}'); properties['defaultValue']['x-decorator'] = 'FormItem'; - properties['defaultValue']['x-reactions'] = { - dependencies: [ - 'uiSchema.x-component-props.gmt', - 'uiSchema.x-component-props.showTime', - 'uiSchema.x-component-props.dateFormat', - 'uiSchema.x-component-props.timeFormat', - ], - fulfill: { - state: { - componentProps: { - gmt: '{{$deps[0]}}', - showTime: '{{$deps[1]}}', - dateFormat: '{{$deps[2]}}', - timeFormat: '{{$deps[3]}}', + properties['defaultValue']['x-reactions'] = [ + { + dependencies: [ + 'uiSchema.x-component-props.gmt', + 'uiSchema.x-component-props.showTime', + 'uiSchema.x-component-props.dateFormat', + 'uiSchema.x-component-props.timeFormat', + ], + fulfill: { + state: { + componentProps: { + gmt: '{{$deps[0]}}', + showTime: '{{$deps[1]}}', + dateFormat: '{{$deps[2]}}', + timeFormat: '{{$deps[3]}}', + }, }, }, }, - }; + { + dependencies: ['primaryKey', 'unique', 'autoIncrement'], + when: '{{$deps[0]||$deps[1]||$deps[2]}}', + fulfill: { + state: { + hidden: true, + value: undefined, + }, + }, + otherwise: { + state: { + hidden: false, + }, + }, + }, + ]; } return { @@ -117,8 +135,7 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema const useUpdateCollectionField = () => { const form = useForm(); - const { run } = useUpdateAction(); - const { refreshCM } = useCollectionManager(); + const { refreshCM } = useCollectionManager_deprecated(); const ctx = useActionContext(); const { refresh } = useResourceActionContext(); const { resource, targetKey } = useResourceContext(); @@ -144,12 +161,13 @@ const useUpdateCollectionField = () => { export const EditCollectionField = (props) => { const record = useRecord(); - return ; + const parentRecordData = useCollectionParentRecordData(); + return ; }; export const EditFieldAction = (props) => { - const { scope, getContainer, item: record, children, ...otherProps } = props; - const { getInterface, collections } = useCollectionManager(); + const { scope, getContainer, item: record, parentItem: parentRecord, children, ...otherProps } = props; + const { getInterface, collections, getCollection } = useCollectionManager_deprecated(); const [visible, setVisible] = useState(false); const [schema, setSchema] = useState({}); const api = useAPIClient(); @@ -157,6 +175,21 @@ export const EditFieldAction = (props) => { const compile = useCompile(); const [data, setData] = useState({}); const { isDialect } = useDialect(); + const scopeKeyOptions = useMemo(() => { + return ( + record?.fields || + getCollection(record.collectionName) + .options.fields.filter((v) => { + return v.interface === 'select'; + }) + .map((k) => { + return { + value: k.name, + label: compile(k.uiSchema?.title), + }; + }) + ); + }, [record.name]); const currentCollections = useMemo(() => { return collections.map((v) => { @@ -168,7 +201,7 @@ export const EditFieldAction = (props) => { }, []); return ( - + { defaultValues.autoCreateReverseField = false; defaultValues.reverseField = interfaceConf?.default?.reverseField; set(defaultValues.reverseField, 'name', `f_${uid()}`); - set(defaultValues.reverseField, 'uiSchema.title', record.__parent.title); + set(defaultValues.reverseField, 'uiSchema.title', record.__parent?.title); } const schema = getSchema( { @@ -212,6 +245,8 @@ export const EditFieldAction = (props) => { collections: currentCollections, isDialect, disabledJSONB: true, + scopeKeyOptions, + createMainOnly: true, ...scope, }} /> diff --git a/packages/core/client/src/collection-manager/Configuration/EditSubFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/EditSubFieldAction.tsx index 574322abb..dbd9d1e30 100644 --- a/packages/core/client/src/collection-manager/Configuration/EditSubFieldAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/EditSubFieldAction.tsx @@ -8,7 +8,7 @@ import { useAPIClient, useRequest } from '../../api-client'; import { useRecord } from '../../record-provider'; import { ActionContextProvider, SchemaComponent } from '../../schema-component'; import { useUpdateAction } from '../action-hooks'; -import { useCollectionManager } from '../hooks'; +import { useCollectionManager_deprecated } from '../hooks'; import { IField } from '../interfaces/types'; import * as components from './components'; @@ -70,7 +70,7 @@ const getSchema = (schema: IField): ISchema => { const useUpdateCollectionField = () => { const form = useForm(); const { run } = useUpdateAction(); - const { refreshCM } = useCollectionManager(); + const { refreshCM } = useCollectionManager_deprecated(); return { async run() { await form.submit(); @@ -92,7 +92,7 @@ const useUpdateCollectionField = () => { export const EditSubFieldAction = (props) => { const record = useRecord(); - const { getInterface } = useCollectionManager(); + const { getInterface } = useCollectionManager_deprecated(); const [visible, setVisible] = useState(false); const [schema, setSchema] = useState({}); const api = useAPIClient(); diff --git a/packages/core/client/src/collection-manager/Configuration/OverridingCollectionField.tsx b/packages/core/client/src/collection-manager/Configuration/OverridingCollectionField.tsx index 261a062bf..709e05ec6 100644 --- a/packages/core/client/src/collection-manager/Configuration/OverridingCollectionField.tsx +++ b/packages/core/client/src/collection-manager/Configuration/OverridingCollectionField.tsx @@ -6,12 +6,13 @@ import cloneDeep from 'lodash/cloneDeep'; import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAPIClient, useRequest } from '../../api-client'; +import { useCollectionParentRecordData } from '../../data-source'; import { RecordProvider, useRecord } from '../../record-provider'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; -import { useCancelAction } from '../action-hooks'; -import { useCollectionManager } from '../hooks'; -import { IField } from '../interfaces/types'; import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; +import { useCancelAction } from '../action-hooks'; +import { useCollectionManager_deprecated } from '../hooks'; +import { IField } from '../interfaces/types'; import * as components from './components'; const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => { @@ -90,7 +91,7 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema const useOverridingCollectionField = () => { const form = useForm(); - const { refreshCM } = useCollectionManager(); + const { refreshCM } = useCollectionManager_deprecated(); const ctx = useActionContext(); const { refresh } = useResourceActionContext(); const { resource } = useResourceContext(); @@ -123,7 +124,8 @@ const useOverridingCollectionField = () => { export const OverridingCollectionField = (props) => { const record = useRecord(); - return ; + const parentRecordData = useCollectionParentRecordData(); + return ; }; const getIsOverriding = (currentFields, record) => { @@ -133,9 +135,10 @@ const getIsOverriding = (currentFields, record) => { return flag; }; export const OverridingFieldAction = (props) => { - const { scope, getContainer, item: record, children, currentCollection } = props; + const { scope, getContainer, item: record, parentItem: parentRecord, children, currentCollection } = props; const { target, through } = record; - const { getInterface, getCurrentCollectionFields, getChildrenCollections, collections } = useCollectionManager(); + const { getInterface, getCurrentCollectionFields, getChildrenCollections, collections } = + useCollectionManager_deprecated(); const [visible, setVisible] = useState(false); const [schema, setSchema] = useState({}); const api = useAPIClient(); @@ -161,7 +164,7 @@ export const OverridingFieldAction = (props) => { }); }, []); return ( - + { const useSyncFromDatabase = () => { const form = useForm(); - const { refreshCM } = useCollectionManager(); + const { refreshCM } = useCollectionManager_deprecated(); const ctx = useActionContext(); const { refresh } = useResourceActionContext(); const { targetKey } = useResourceContext(); diff --git a/packages/core/client/src/collection-manager/Configuration/SyncSQLFieldsAction.tsx b/packages/core/client/src/collection-manager/Configuration/SyncSQLFieldsAction.tsx index 4d6b03f2f..f8fd3904e 100644 --- a/packages/core/client/src/collection-manager/Configuration/SyncSQLFieldsAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/SyncSQLFieldsAction.tsx @@ -1,18 +1,19 @@ -import { RecordProvider, useRecord } from '../../record-provider'; -import React, { useEffect, useMemo, useState } from 'react'; -import { ActionContextProvider, FormProvider, SchemaComponent, useActionContext } from '../../schema-component'; import { SyncOutlined } from '@ant-design/icons'; -import { useTranslation } from 'react-i18next'; -import { Button } from 'antd'; -import { uid } from '@formily/shared'; -import { useCancelAction } from '../action-hooks'; -import { FieldsConfigure, PreviewTable, SQLRequestProvider } from '../templates/components/sql-collection'; -import { createForm } from '@formily/core'; import { FormLayout } from '@formily/antd-v5'; -import { useCollectionManager } from '../hooks'; -import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; -import { useAPIClient } from '../../api-client'; +import { createForm } from '@formily/core'; import { useField, useForm } from '@formily/react'; +import { uid } from '@formily/shared'; +import { Button } from 'antd'; +import React, { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useAPIClient } from '../../api-client'; +import { useCollectionParentRecordData } from '../../data-source/collection-record/CollectionRecordProvider'; +import { RecordProvider, useRecord } from '../../record-provider'; +import { ActionContextProvider, FormProvider, SchemaComponent, useActionContext } from '../../schema-component'; +import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; +import { useCancelAction } from '../action-hooks'; +import { useCollectionManager_deprecated } from '../hooks/useCollectionManager_deprecated'; +import { FieldsConfigure, PreviewTable, SQLRequestProvider } from '../templates/components/sql-collection'; const schema = { type: 'object', @@ -90,7 +91,7 @@ const schema = { const useSyncFromDB = (refreshCMList?: any) => { const form = useForm(); const ctx = useActionContext(); - const { refreshCM } = useCollectionManager(); + const { refreshCM } = useCollectionManager_deprecated(); const { refresh } = useResourceActionContext(); const { targetKey } = useResourceContext(); const { [targetKey]: filterByTk } = useRecord(); @@ -126,6 +127,7 @@ export const SyncSQLFieldsAction: React.FC<{ refreshCMList: any; }> = ({ refreshCMList }) => { const record = useRecord(); + const parentRecordData = useCollectionParentRecordData(); const { t } = useTranslation(); const [visible, setVisible] = useState(false); const form = useMemo( @@ -138,7 +140,7 @@ export const SyncSQLFieldsAction: React.FC<{ return ( record.template === 'sql' && ( - +
{ + const fields = getDefaultCollectionFields(selectedRows, form.values); + const config = { + autoGenId: !!fields.find((v) => v.name === 'id'), + createdAt: !!fields.find((v) => v.name === 'createdAt'), + createdBy: !!fields.find((v) => v.name === 'createdBy'), + updatedAt: !!fields.find((v) => v.name === 'updatedAt'), + updatedBy: !!fields.find((v) => v.name === 'updatedBy'), + }; + setSelectedRowKeys( + fields?.map?.((v) => { + return v.name; + }), + ); + form.setValues({ ...form.values, fields, ...config }); + }, + }} + /> + ); +}); diff --git a/packages/core/client/src/collection-manager/templates/components/PreviewFields.tsx b/packages/core/client/src/collection-manager/templates/components/PreviewFields.tsx index d5f1783eb..b7ca62c28 100644 --- a/packages/core/client/src/collection-manager/templates/components/PreviewFields.tsx +++ b/packages/core/client/src/collection-manager/templates/components/PreviewFields.tsx @@ -5,8 +5,8 @@ import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ResourceActionContext, useCompile } from '../../../'; import { useAPIClient } from '../../../api-client'; -import { getOptions } from '../../Configuration/interfaces'; -import { useCollectionManager } from '../../hooks/useCollectionManager'; +import { useFieldInterfaceOptions } from '../../Configuration/interfaces'; +import { useCollectionManager_deprecated } from '../../hooks/useCollectionManager_deprecated'; const getInterfaceOptions = (data, type) => { const interfaceOptions = []; @@ -32,9 +32,10 @@ const PreviewCom = (props) => { const field: any = useField(); const form = useForm(); const { getCollection, getInterface, getCollectionFields, getInheritCollections, getParentCollectionFields } = - useCollectionManager(); + useCollectionManager_deprecated(); const compile = useCompile(); - const initOptions = getOptions().filter((v) => !['relation', 'systemInfo'].includes(v.key)); + const options = useFieldInterfaceOptions(); + const initOptions = options.filter((v) => !['relation', 'systemInfo'].includes(v.key)); useEffect(() => { const data = []; sourceCollections.forEach((item) => { diff --git a/packages/core/client/src/collection-manager/templates/components/PreviewTable.tsx b/packages/core/client/src/collection-manager/templates/components/PreviewTable.tsx index dcddbd746..8b442d119 100644 --- a/packages/core/client/src/collection-manager/templates/components/PreviewTable.tsx +++ b/packages/core/client/src/collection-manager/templates/components/PreviewTable.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { EllipsisWithTooltip, useCompile } from '../../../'; import { useAPIClient } from '../../../api-client'; -import { useCollectionManager } from '../../hooks/useCollectionManager'; +import { useCollectionManager_deprecated } from '../../hooks/useCollectionManager_deprecated'; const mapFields = ['lineString', 'point', 'circle', 'polygon']; export const PreviewTable = (props) => { @@ -13,7 +13,7 @@ export const PreviewTable = (props) => { const [previewData, setPreviewData] = useState([]); const compile = useCompile(); const [loading, setLoading] = useState(false); - const { getInterface, getCollectionFields } = useCollectionManager(); + const { getInterface, getCollectionFields } = useCollectionManager_deprecated(); const api = useAPIClient(); const { t } = useTranslation(); const form = useForm(); diff --git a/packages/core/client/src/collection-manager/templates/components/sql-collection/FieldsConfigure.tsx b/packages/core/client/src/collection-manager/templates/components/sql-collection/FieldsConfigure.tsx index 99e07f895..ff70e0677 100644 --- a/packages/core/client/src/collection-manager/templates/components/sql-collection/FieldsConfigure.tsx +++ b/packages/core/client/src/collection-manager/templates/components/sql-collection/FieldsConfigure.tsx @@ -4,9 +4,9 @@ import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; import { Alert, Cascader, Input, Select, Spin, Table, Tag } from 'antd'; import { observer, useField, useForm } from '@formily/react'; import { ArrayField } from '@formily/core'; -import { getOptions } from '../../../Configuration/interfaces'; +import { useFieldInterfaceOptions } from '../../../Configuration/interfaces'; import { useCompile } from '../../../../schema-component'; -import { useCollectionManager } from '../../../hooks'; +import { useCollectionManager_deprecated } from '../../../hooks'; import dayjs from 'dayjs'; import { FieldOptions } from '@nocobase/database'; import { ResourceActionContext } from '../../../ResourceActionProvider'; @@ -34,7 +34,7 @@ const useSourceFieldsOptions = () => { const form = useForm(); const { sources = [] } = form.values; const { t } = useTranslation(); - const { getCollection, getInheritCollections, getParentCollectionFields } = useCollectionManager(); + const { getCollection, getInheritCollections, getParentCollectionFields } = useCollectionManager_deprecated(); const data = []; sources.forEach((item: string) => { const collection = getCollection(item); @@ -81,10 +81,12 @@ export const FieldsConfigure = observer(() => { const field: ArrayField = useField(); const { data: curFields } = useContext(ResourceActionContext); const compile = useCompile(); - const { getInterface, getCollectionField } = useCollectionManager(); + const { getInterface, getCollectionField } = useCollectionManager_deprecated(); + const options = useFieldInterfaceOptions(); + const interfaceOptions = useMemo( () => - getOptions() + options .filter((v) => !['relation'].includes(v.key)) .map((options, index) => ({ ...options, diff --git a/packages/core/client/src/collection-manager/templates/expression.tsx b/packages/core/client/src/collection-manager/templates/expression.tsx index d94b764ee..e309f964f 100644 --- a/packages/core/client/src/collection-manager/templates/expression.tsx +++ b/packages/core/client/src/collection-manager/templates/expression.tsx @@ -1,13 +1,13 @@ import { getOptions } from '@nocobase/evaluators/client'; import { getConfigurableProperties } from './properties'; -import { ICollectionTemplate } from './types'; +import { CollectionTemplate } from '../../data-source/collection-template/CollectionTemplate'; -export const expression: ICollectionTemplate = { - name: 'expression', - title: '{{t("Expression collection")}}', - order: 4, - color: 'orange', - default: { +export class ExpressionCollectionTemplate extends CollectionTemplate { + name = 'expression'; + title = '{{t("Expression collection")}}'; + order = 4; + color = 'orange'; + default = { createdBy: true, updatedBy: true, createdAt: true, @@ -50,9 +50,16 @@ export const expression: ICollectionTemplate = { }, }, ], - }, - availableFieldInterfaces: { + }; + availableFieldInterfaces = { include: [], - }, - configurableProperties: getConfigurableProperties('title', 'name', 'inherits', 'category', 'description'), -}; + }; + configurableProperties = getConfigurableProperties( + 'title', + 'name', + 'inherits', + 'category', + 'description', + 'presetFields', + ); +} diff --git a/packages/core/client/src/collection-manager/templates/general.tsx b/packages/core/client/src/collection-manager/templates/general.tsx index c3078e3cc..9da90c7df 100644 --- a/packages/core/client/src/collection-manager/templates/general.tsx +++ b/packages/core/client/src/collection-manager/templates/general.tsx @@ -1,20 +1,20 @@ +import { CollectionTemplate } from '../../data-source/collection-template/CollectionTemplate'; import { getConfigurableProperties } from './properties'; -import { ICollectionTemplate } from './types'; -export const general: ICollectionTemplate = { - name: 'general', - title: '{{t("General collection")}}', - order: 1, - color: 'blue', - default: { +export class GeneralCollectionTemplate extends CollectionTemplate { + name = 'general'; + title = '{{t("General collection")}}'; + order = 1; + color = 'blue'; + default = { fields: [], - }, - configurableProperties: getConfigurableProperties( + }; + configurableProperties = getConfigurableProperties( 'title', 'name', 'inherits', 'category', 'description', - 'moreOptions', - ), -}; + 'presetFields', + ); +} diff --git a/packages/core/client/src/collection-manager/templates/properties/index.ts b/packages/core/client/src/collection-manager/templates/properties/index.ts index 8256e1ff3..ba17381e3 100644 --- a/packages/core/client/src/collection-manager/templates/properties/index.ts +++ b/packages/core/client/src/collection-manager/templates/properties/index.ts @@ -1,74 +1,5 @@ import { css } from '@emotion/css'; - -const moreOptions = { - autoGenId: { - type: 'boolean', - 'x-content': '{{t("Generate ID field automatically")}}', - default: true, - 'x-decorator': 'FormItem', - 'x-component': 'Checkbox', - 'x-disabled': '{{ !createOnly }}', - 'x-reactions': [ - { - target: 'sortable', - when: '{{createOnly&&!$self.value}}', - fulfill: { - state: { - value: false, - }, - schema: { - 'x-disabled': true, - }, - }, - otherwise: { - schema: { - 'x-disabled': '{{!createOnly}}', - }, - }, - }, - ], - }, - createdBy: { - type: 'boolean', - 'x-content': '{{t("Store the creation user of each record")}}', - default: true, - 'x-decorator': 'FormItem', - 'x-component': 'Checkbox', - 'x-disabled': '{{ !createOnly }}', - }, - updatedBy: { - type: 'boolean', - 'x-content': '{{t("Store the last update user of each record")}}', - default: true, - 'x-decorator': 'FormItem', - 'x-component': 'Checkbox', - 'x-disabled': '{{ !createOnly }}', - }, - createdAt: { - type: 'boolean', - 'x-content': '{{t("Store the creation time of each record")}}', - default: true, - 'x-decorator': 'FormItem', - 'x-component': 'Checkbox', - 'x-disabled': '{{ !createOnly }}', - }, - updatedAt: { - type: 'boolean', - 'x-content': '{{t("Store the last update time of each record")}}', - default: true, - 'x-decorator': 'FormItem', - 'x-component': 'Checkbox', - 'x-disabled': '{{ !createOnly }}', - }, - sortable: { - type: 'boolean', - 'x-content': '{{t("Records can be sorted")}}', - default: true, - 'x-decorator': 'FormItem', - 'x-component': 'Checkbox', - 'x-disabled': '{{ !createOnly }}', - }, -}; +import { PresetFields } from '../components/PresetFields'; export const defaultConfigurableProperties = { title: { @@ -120,11 +51,11 @@ export const defaultConfigurableProperties = { 'x-decorator': 'FormItem', 'x-component': 'Input.TextArea', }, - ...moreOptions, - moreOptions: { - title: '{{t("More options")}}', + presetFields: { + title: '{{t("Preset fields")}}', type: 'void', 'x-decorator': 'FormItem', + 'x-visible': '{{ createOnly }}', 'x-decorator-props': { className: css` .ant-formily-item { @@ -132,9 +63,7 @@ export const defaultConfigurableProperties = { } `, }, - properties: { - ...moreOptions, - }, + 'x-component': PresetFields, }, }; @@ -150,7 +79,7 @@ export type DefaultConfigurableKeys = | 'updatedAt' | 'sortable' | 'description' - | 'moreOptions'; + | 'presetFields'; export const getConfigurableProperties = (...keys: DefaultConfigurableKeys[]) => { const props = {} as Record; diff --git a/packages/core/client/src/collection-manager/templates/sql.tsx b/packages/core/client/src/collection-manager/templates/sql.tsx index c1e3d21ca..9fd7b11f4 100644 --- a/packages/core/client/src/collection-manager/templates/sql.tsx +++ b/packages/core/client/src/collection-manager/templates/sql.tsx @@ -1,19 +1,18 @@ import { Field } from '@formily/core'; -import { useAsyncData } from '../../async-data-provider'; import { SQLInput, PreviewTable, FieldsConfigure, SQLRequestProvider } from './components/sql-collection'; import { getConfigurableProperties } from './properties'; -import { ICollectionTemplate } from './types'; import { i18n } from '../../i18n'; +import { CollectionTemplate } from '../../data-source/collection-template/CollectionTemplate'; -export const sql: ICollectionTemplate = { - name: 'sql', - title: '{{t("SQL collection")}}', - order: 4, - color: 'yellow', - default: { +export class SqlCollectionTemplate extends CollectionTemplate { + name = 'sql'; + title = '{{t("SQL collection")}}'; + order = 4; + color = 'yellow'; + default = { fields: [], - }, - configurableProperties: { + }; + configurableProperties = { title: { type: 'string', title: '{{ t("Collection display name") }}', @@ -76,5 +75,5 @@ export const sql: ICollectionTemplate = { }, }, ...getConfigurableProperties('category'), - }, -}; + }; +} diff --git a/packages/core/client/src/collection-manager/templates/tree.tsx b/packages/core/client/src/collection-manager/templates/tree.tsx index e472df660..467f468d3 100644 --- a/packages/core/client/src/collection-manager/templates/tree.tsx +++ b/packages/core/client/src/collection-manager/templates/tree.tsx @@ -1,12 +1,12 @@ +import { CollectionTemplate } from '../../data-source/collection-template/CollectionTemplate'; import { getConfigurableProperties } from './properties'; -import { ICollectionTemplate } from './types'; -export const tree: ICollectionTemplate = { - name: 'tree', - title: '{{t("Tree collection")}}', - order: 3, - color: 'blue', - default: { +export class TreeCollectionTemplate extends CollectionTemplate { + name = 'tree'; + title = '{{t("Tree collection")}}'; + order = 3; + color = 'blue'; + default = { tree: 'adjacencyList', fields: [ { @@ -62,8 +62,8 @@ export const tree: ICollectionTemplate = { }, }, ], - }, - events: { + }; + events = { beforeSubmit(values) { if (Array.isArray(values?.fields)) { values?.fields.map((f) => { @@ -71,13 +71,13 @@ export const tree: ICollectionTemplate = { }); } }, - }, - configurableProperties: getConfigurableProperties( + }; + configurableProperties = getConfigurableProperties( 'title', 'name', 'inherits', 'category', 'description', - 'moreOptions', - ), -}; + 'presetFields', + ); +} diff --git a/packages/core/client/src/collection-manager/templates/types.ts b/packages/core/client/src/collection-manager/templates/types.ts index c57c8e0e9..b7937e9a0 100644 --- a/packages/core/client/src/collection-manager/templates/types.ts +++ b/packages/core/client/src/collection-manager/templates/types.ts @@ -1,14 +1,18 @@ -import { ISchema } from '@formily/react'; -import { FieldOptions } from '../../collection-manager/types'; +import type { ISchema } from '@formily/react'; +import type { FieldOptions } from '../types'; +import type { Application } from '../../application'; +import type { CollectionOptions, Collection } from '../../data-source/collection/Collection'; export interface ICollectionTemplate { name: string; + Collection?: typeof Collection; + transform?: (collection: CollectionOptions, app: Application) => CollectionOptions; title?: string; color?: string; /** 排序 */ order?: number; /** 默认配置 */ - default?: CollectionOptions; + default?: CollectionDefaultOptions; events?: any; /** UI 可配置的 CollectionOptions 参数(添加或编辑的 Collection 表单的字段) */ configurableProperties?: Record; @@ -32,7 +36,7 @@ interface AvailableFieldInterfacesExclude { exclude?: any[]; } -interface CollectionOptions { +interface CollectionDefaultOptions { /** * 自动生成 id * @default true diff --git a/packages/core/client/src/collection-manager/templates/view.tsx b/packages/core/client/src/collection-manager/templates/view.tsx index 12dc26289..3555910d7 100644 --- a/packages/core/client/src/collection-manager/templates/view.tsx +++ b/packages/core/client/src/collection-manager/templates/view.tsx @@ -1,18 +1,18 @@ +import { CollectionTemplate } from '../../data-source/collection-template/CollectionTemplate'; import { PreviewFields } from './components/PreviewFields'; import { PreviewTable } from './components/PreviewTable'; import { getConfigurableProperties } from './properties'; -import { ICollectionTemplate } from './types'; -export const view: ICollectionTemplate = { - name: 'view', - title: '{{t("Connect to database view")}}', - order: 4, - color: 'yellow', - default: { +export class ViewCollectionTemplate extends CollectionTemplate { + name = 'view'; + title = '{{t("Connect to database view")}}'; + order = 4; + color = 'yellow'; + default = { fields: [], - }, - divider: true, - configurableProperties: { + }; + divider = true; + configurableProperties = { title: { type: 'string', title: '{{ t("Collection display name") }}', @@ -137,5 +137,5 @@ export const view: ICollectionTemplate = { }, ...getConfigurableProperties('category', 'description'), - }, -}; + }; +} diff --git a/packages/core/client/src/collection-manager/types.ts b/packages/core/client/src/collection-manager/types.ts index b50daa2f9..00729d200 100644 --- a/packages/core/client/src/collection-manager/types.ts +++ b/packages/core/client/src/collection-manager/types.ts @@ -1,77 +1,12 @@ -import { ISchema } from '@formily/react'; -import { ReactNode } from 'react'; +import type { ISchema } from '@formily/react'; +import type { ReactNode } from 'react'; +import type { CollectionManager, CollectionOptions } from '../data-source'; -type dumpable = 'required' | 'optional' | 'skip'; -type CollectionSortable = string | boolean | { name?: string; scopeKey?: string }; - -type MetaDataType = 'meta'; -type ConfigDataType = 'config'; -type BusinessDataType = 'business'; - -type DumpDataType = MetaDataType | ConfigDataType | BusinessDataType; -type Dumpable = 'required' | 'optional' | 'skip'; - -type BaseDuplicatorObject = { - with?: string[] | string; - delayRestore?: any; -}; - -type Duplicator = - | Dumpable - | ({ - dumpable?: Dumpable; - } & BaseDuplicatorObject) - | ({ - dataType?: DumpDataType; - } & BaseDuplicatorObject); - -export interface CollectionOptions { - name: string; - title?: string; - namespace?: string; - /** - * Used for @nocobase/plugin-duplicator - * @see packages/core/database/src/collection-group-manager.tss - * - * @prop {'required' | 'optional' | 'skip'} dumpable - Determine whether the collection is dumped - * @prop {string[] | string} [with] - Collections dumped with this collection - * @prop {any} [delayRestore] - A function to execute after all collections are restored - */ - duplicator?: Duplicator; - tableName?: string; - inherits?: string[] | string; - viewName?: string; - writableView?: boolean; - - filterTargetKey?: string; - fields?: FieldOptions[]; - model?: any; - repository?: any; - sortable?: CollectionSortable; - /** - * @default true - */ - autoGenId?: boolean; - /** - * @default 'options' - */ - magicAttribute?: string; - - tree?: string; - - template?: string; - - [key: string]: any; -} +export type { CollectionOptions } from '../data-source'; export interface CollectionManagerOptions { - service?: any; - interfaces?: any; - collections?: CollectionOptions[]; - templates?: any; - refreshCM?: () => Promise; + instance?: CollectionManager; children?: ReactNode; - updateCollection?: (collection: any) => void; } export type FieldOptions = any; @@ -81,7 +16,7 @@ export interface ICollectionProviderProps { fields?: any; } -export interface CollectionFieldOptions { +export interface CollectionFieldOptions_deprecated { name?: any; collectionName?: string; sourceKey?: string; // association field diff --git a/packages/core/client/src/common/index.ts b/packages/core/client/src/common/index.ts index 394b92eb9..30ff4e593 100644 --- a/packages/core/client/src/common/index.ts +++ b/packages/core/client/src/common/index.ts @@ -1 +1,2 @@ export * from './SelectWithTitle'; +export * from './useFieldComponentName'; diff --git a/packages/core/client/src/common/useFieldComponentName.tsx b/packages/core/client/src/common/useFieldComponentName.tsx new file mode 100644 index 000000000..a20cbca9a --- /dev/null +++ b/packages/core/client/src/common/useFieldComponentName.tsx @@ -0,0 +1,38 @@ +import { Field } from '@formily/core'; +import { useField, useFieldSchema } from '@formily/react'; +import { useIsFileField } from '../schema-component/antd/form-item/FormItem.Settings'; +import { useColumnSchema } from '../schema-component/antd/table-v2/Table.Column.Decorator'; +import { useCollectionManager_deprecated, useCollection_deprecated } from '../collection-manager'; + +/** + * 获取字段相关的配置信息 + * @returns + */ +function useCollectionField() { + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); + const fieldSchema = useFieldSchema(); + const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + return collectionField; +} + +export function useFieldComponentName(): string { + const { fieldSchema: tableColumnSchema, collectionField: tableColumnField } = useColumnSchema(); + const field = useField(); + const isFileField = useIsFileField(); + const schema = useFieldSchema(); + const targetCollectionField = useCollectionField(); + const fieldSchema = tableColumnSchema || schema; + const collectionField = tableColumnField || targetCollectionField; + + const map = { + // AssociationField 的 mode 默认值是 Select + AssociationField: 'Select', + }; + const fieldComponentName = + fieldSchema?.['x-component-props']?.['mode'] || + field?.componentProps?.['mode'] || + (isFileField ? 'FileManager' : '') || + collectionField?.uiSchema?.['x-component']; + return map[fieldComponentName] || fieldComponentName; +} diff --git a/packages/core/client/src/common/useNiceDropdownHeight.ts b/packages/core/client/src/common/useNiceDropdownHeight.ts new file mode 100644 index 000000000..e71508a7d --- /dev/null +++ b/packages/core/client/src/common/useNiceDropdownHeight.ts @@ -0,0 +1,25 @@ +import { useEffect, useMemo, useRef } from 'react'; + +/** + * 通过鼠标的位置计算出最佳的 dropdown 的高度,以尽量避免出现滚动条 + * @param deps 类似于 useEffect 的第二个参数,如果不传则默认为 [] + */ +export const useNiceDropdownMaxHeight = (deps: any[] = []) => { + const heightRef = useRef(0); + + useEffect(() => { + const handler = (e: MouseEvent) => { + const { clientY } = e; + const h = Math.max(clientY, window.innerHeight - clientY); + heightRef.current = h; + }; + + window.addEventListener('mousemove', handler); + + return () => { + window.removeEventListener('mousemove', handler); + }; + }, []); + + return useMemo(() => heightRef.current - 40, deps); +}; diff --git a/packages/core/client/src/data-source/__tests__/collection-field-interface/CollectionFieldInterfaceManager.test.ts b/packages/core/client/src/data-source/__tests__/collection-field-interface/CollectionFieldInterfaceManager.test.ts new file mode 100644 index 000000000..75af02496 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/collection-field-interface/CollectionFieldInterfaceManager.test.ts @@ -0,0 +1,126 @@ +import { Application, CollectionFieldInterface, CollectionFieldInterfaceManager } from '@nocobase/client'; + +describe('CollectionFieldInterfaceManager', () => { + let collectionFieldInterfaceManager: CollectionFieldInterfaceManager; + + beforeEach(() => { + const app = new Application(); + collectionFieldInterfaceManager = app.dataSourceManager.collectionFieldInterfaceManager; + }); + + describe('addFieldInterfaces', () => { + it('should add field interfaces correctly', () => { + class A extends CollectionFieldInterface { + name = 'a'; + } + class B extends CollectionFieldInterface { + name = 'b'; + } + collectionFieldInterfaceManager.addFieldInterfaces([A, B]); + + const a = collectionFieldInterfaceManager.getFieldInterface('a'); + const b = collectionFieldInterfaceManager.getFieldInterface('b'); + expect(a).instanceOf(A); + expect(b).instanceOf(B); + }); + + it('should override duplicates', () => { + class A extends CollectionFieldInterface { + name = 'a'; + } + class B extends CollectionFieldInterface { + name = 'a'; + } + collectionFieldInterfaceManager.addFieldInterfaces([A, B]); + + const a = collectionFieldInterfaceManager.getFieldInterface('a'); + expect(a).instanceOf(B); + }); + }); + + describe('getFieldInterface', () => { + beforeEach(() => { + class A extends CollectionFieldInterface { + name = 'a'; + } + collectionFieldInterfaceManager.addFieldInterfaces([A]); + }); + + it('should return the correct interface by name', () => { + const fieldInterface = collectionFieldInterfaceManager.getFieldInterface('a'); + expect(fieldInterface.name).toBe('a'); + }); + + it('should return undefined for a non-existent interface', () => { + const fieldInterface = collectionFieldInterfaceManager.getFieldInterface('nonExistentInterface'); + expect(fieldInterface).toBeUndefined(); + }); + }); + + describe('getFieldInterfaces', () => { + beforeEach(() => { + class A extends CollectionFieldInterface { + name = 'a'; + supportDataSourceType = ['sourceA']; + } + class B extends CollectionFieldInterface { + name = 'b'; + notSupportDataSourceType = ['sourceA']; + } + + class C extends CollectionFieldInterface { + name = 'c'; + } + + collectionFieldInterfaceManager.addFieldInterfaces([A, B, C]); + }); + + it('should return all stored interfaces when no dataSourceType is provided', () => { + const interfaces = collectionFieldInterfaceManager.getFieldInterfaces(); + expect(interfaces.length).toBe(3); + }); + + it('should return supported interface when dataSourceType is provided', () => { + const interfaces = collectionFieldInterfaceManager.getFieldInterfaces('sourceA'); + expect(interfaces.length).toBe(2); + expect(interfaces[0].name).toBe('a'); + expect(interfaces[1].name).toBe('c'); + }); + + it('should return empty array when an unsupported dataSourceType is provided', () => { + const interfaces = collectionFieldInterfaceManager.getFieldInterfaces('sourceB'); + expect(interfaces.length).toBe(2); + expect(interfaces[0].name).toBe('b'); + expect(interfaces[1].name).toBe('c'); + }); + }); + + describe('addFieldInterfaceGroups', () => { + it('should add field interface groups', () => { + const groups = { group1: { label: 'Group 1', order: 1 } }; + collectionFieldInterfaceManager.addFieldInterfaceGroups(groups); + expect(collectionFieldInterfaceManager.getFieldInterfaceGroup('group1')).toEqual(groups.group1); + }); + }); + + describe('getFieldInterfaceGroups', () => { + it('should return all field interface groups', () => { + expect(collectionFieldInterfaceManager.getFieldInterfaceGroups()).toEqual({}); + const groups = { group1: { label: 'Group 1', order: 1 } }; + collectionFieldInterfaceManager.addFieldInterfaceGroups(groups); + expect(collectionFieldInterfaceManager.getFieldInterfaceGroups()).toEqual(groups); + }); + }); + + describe('getFieldInterfaceGroup', () => { + it('should return the correct group by name', () => { + const groups = { group1: { label: 'Group 1', order: 1 } }; + collectionFieldInterfaceManager.addFieldInterfaceGroups(groups); + expect(collectionFieldInterfaceManager.getFieldInterfaceGroup('group1')).toEqual(groups.group1); + }); + + it('should return undefined when the group does not exist', () => { + expect(collectionFieldInterfaceManager.getFieldInterfaceGroup('nonExistentGroup')).toBeUndefined(); + }); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/collection-field/CollectionField.test.tsx b/packages/core/client/src/data-source/__tests__/collection-field/CollectionField.test.tsx new file mode 100644 index 000000000..cc90cd1dc --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/collection-field/CollectionField.test.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { render, screen } from '@nocobase/test/client'; +import { + CollectionField, + CollectionProvider, + Application, + FormItem, + Input, + SchemaComponent, + SchemaComponentProvider, + DataSourceApplicationProvider, +} from '@nocobase/client'; +import collections from '../collections.json'; + +function renderApp(fieldName: string) { + const noUiSchema = { + key: 'no-ui-schema', + name: 'no-ui-schema', + type: 'string', + interface: 'select', + description: null, + collectionName: 't_vwpds9fs4xs', + parentKey: null, + reverseKey: null, + }; + + const usersCollection: any = collections[0]; + usersCollection.fields.push(noUiSchema); + + const app = new Application({ + dataSourceManager: { + collections: [usersCollection], + }, + }); + + const schema = { + name: 'root', + type: 'object', + properties: { + [fieldName]: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + }, + }, + }; + + return render( +
+ + + + + + + +
, + ); +} + +describe('CollectionField', () => { + it('works', () => { + renderApp('nickname'); + expect(screen.getByText('Nickname')).toBeInTheDocument(); + expect(screen.getByRole('textbox')).toHaveClass('ant-input'); + }); + + it('no schema', () => { + renderApp('no-ui-schema'); + expect(document.querySelector('.ant-formily-item-control-content-component')).toHaveTextContent(''); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/collection-field/CollectionFieldProvider.test.tsx b/packages/core/client/src/data-source/__tests__/collection-field/CollectionFieldProvider.test.tsx new file mode 100644 index 000000000..e0a6c7765 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/collection-field/CollectionFieldProvider.test.tsx @@ -0,0 +1,60 @@ +import React, { ComponentType } from 'react'; +import { render, screen } from '@nocobase/test/client'; +import { + CollectionFieldProvider, + CollectionProvider, + useCollectionField, + Application, + SchemaComponentProvider, + DataSourceApplicationProvider, +} from '@nocobase/client'; +import collections from '../collections.json'; + +function renderApp(Demo: ComponentType, name?: string) { + const app = new Application({ + dataSourceManager: { + collections: collections as any, + }, + }); + return render( +
+ + + + + + + + + +
, + ); +} + +describe('CollectionFieldProvider', () => { + test('useCollectionField() should get current field', () => { + const Demo = () => { + const field = useCollectionField(); + return ( + <> +
{field.name}
+ + ); + }; + + renderApp(Demo, 'nickname'); + + expect(screen.getByTestId('demo')).toHaveTextContent('nickname'); + }); + + test('field not exists, should render `CollectionDeletedPlaceholder`', () => { + const Demo = () => { + return
children
; + }; + + renderApp(Demo, 'not-exists'); + + expect(document.body.innerHTML).toContain('ant-typography'); + expect(document.body.innerHTML).not.toContain('children'); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/collection-record/CollectionRecord.test.tsx b/packages/core/client/src/data-source/__tests__/collection-record/CollectionRecord.test.tsx new file mode 100644 index 000000000..c747d900c --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/collection-record/CollectionRecord.test.tsx @@ -0,0 +1,14 @@ +import { CollectionRecord } from '../../collection-record'; + +describe('Record', () => { + test('should works', () => { + const record = new CollectionRecord<{ id: number }, { name: string }>({ data: { id: 1 } }); + expect(record.data.id).toBe(1); + + record.setData({ id: 2 }); + expect(record.data.id).toBe(2); + + record.setParentRecord(new CollectionRecord({ data: { name: 'a' } })); + expect(record.parentRecord.data.name).toBe('a'); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/collection-record/CollectionRecordProvider.test.tsx b/packages/core/client/src/data-source/__tests__/collection-record/CollectionRecordProvider.test.tsx new file mode 100644 index 000000000..67e00a944 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/collection-record/CollectionRecordProvider.test.tsx @@ -0,0 +1,200 @@ +import React from 'react'; +import { render, screen } from '@nocobase/test/client'; +import { + CollectionRecordProvider, + CollectionRecord, + useCollectionParentRecordData, + useCollectionParentRecord, + useCollectionRecordData, + useCollectionRecord, +} from '../../collection-record'; + +describe('CollectionRecordProvider', () => { + describe('record and parentRecord', () => { + test('record parameter is a `Record` instance', () => { + const Demo = () => { + const record = useCollectionRecord(); + return
{JSON.stringify(record)}
; + }; + const record = new CollectionRecord({ data: { id: 1, name: 'foo' } }); + + render( + + + , + ); + + expect(screen.getByTestId('content')).toHaveTextContent(JSON.stringify({ id: 1, name: 'foo' })); + }); + + test('record parameter is a `plain object`', () => { + const Demo = () => { + const record = useCollectionRecord(); + return
{JSON.stringify(record)}
; + }; + + render( + + + , + ); + + expect(screen.getByTestId('content')).toHaveTextContent(JSON.stringify({ id: 1, name: 'foo' })); + }); + + test('record parameter is a `Record` instance with parent record', () => { + const Demo = () => { + const record = useCollectionRecord(); + return
{JSON.stringify(record)}
; + }; + + const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } }); + const record = new CollectionRecord({ data: { id: 1, name: 'foo' }, parentRecord }); + + render( + + + , + ); + + expect(screen.getByTestId('content')).toHaveTextContent( + JSON.stringify({ + data: { + id: 1, + name: 'foo', + }, + parentRecord: { + data: { + id: 1, + role: 'admin', + }, + }, + }), + ); + }); + + test('record parameter is a `Record` instance, parent record is passed through parentRecord parameter', () => { + const Demo = () => { + const record = useCollectionRecord(); + return
{JSON.stringify(record)}
; + }; + + const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } }); + const record = new CollectionRecord({ data: { id: 1, name: 'foo' } }); + + render( + + + , + ); + + expect(screen.getByTestId('content')).toHaveTextContent( + JSON.stringify({ + data: { + id: 1, + name: 'foo', + }, + parentRecord: { + data: { + id: 1, + role: 'admin', + }, + }, + }), + ); + }); + + test('record parameter is a `plain object`, parent record is also a `plain object`', () => { + const Demo = () => { + const record = useCollectionRecord(); + return
{JSON.stringify(record)}
; + }; + + render( + + + , + ); + + expect(screen.getByTestId('content')).toHaveTextContent( + JSON.stringify({ + data: { + id: 1, + name: 'foo', + }, + parentRecord: { + data: { + id: 1, + role: 'admin', + }, + }, + }), + ); + }); + }); + + describe('hooks', () => { + test('useCollectionRecordData()', () => { + const Demo = () => { + const data = useCollectionRecordData(); + return
{JSON.stringify(data)}
; + }; + const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } }); + const record = new CollectionRecord({ data: { id: 1, name: 'foo' }, parentRecord }); + + render( + + + , + ); + + expect(screen.getByTestId('content')).toHaveTextContent(JSON.stringify({ id: 1, name: 'foo' })); + }); + + test('useCollectionParentRecord()', () => { + const Demo = () => { + const data = useCollectionParentRecord(); + return
{JSON.stringify(data)}
; + }; + const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } }); + const record = new CollectionRecord({ data: { id: 1, name: 'foo' }, parentRecord }); + + render( + + + , + ); + + expect(screen.getByTestId('content')).toHaveTextContent( + JSON.stringify({ + data: { + id: 1, + role: 'admin', + }, + }), + ); + }); + + test('useCollectionParentRecordData()', () => { + const Demo = () => { + const data = useCollectionParentRecordData(); + return
{JSON.stringify(data)}
; + }; + const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } }); + const record = new CollectionRecord({ data: { id: 1, name: 'foo' }, parentRecord }); + + render( + + + , + ); + + expect(screen.getByTestId('content')).toHaveTextContent( + JSON.stringify({ + id: 1, + role: 'admin', + }), + ); + }); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/collection-record/isNewRecord.test.ts b/packages/core/client/src/data-source/__tests__/collection-record/isNewRecord.test.ts new file mode 100644 index 000000000..ebb586c7e --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/collection-record/isNewRecord.test.ts @@ -0,0 +1,58 @@ +import { isNewRecord, markRecordAsNew } from '../../collection-record/isNewRecord'; + +describe('recordUtilities', () => { + let record: any; + + beforeEach(() => { + record = {}; + }); + + describe('isNewRecord', () => { + it('should return false for a record that has not been marked as new', () => { + expect(isNewRecord(record)).toBe(false); + }); + + it('should return true for a record that has been marked as new', () => { + markRecordAsNew(record); + expect(isNewRecord(record)).toBe(true); + }); + + it('should return false for non-object types', () => { + expect(isNewRecord(null)).toBe(false); + expect(isNewRecord(undefined)).toBe(false); + // @ts-ignore + expect(isNewRecord(42)).toBe(false); + // @ts-ignore + expect(isNewRecord('not an object')).toBe(false); + }); + + it('should not be fooled by properties that mimic the internal Symbol key', () => { + const fakeKey = 'Symbol(isNewRecord)'; + record[fakeKey] = true; + expect(isNewRecord(record)).toBe(false); + }); + }); + + describe('markRecordAsNew', () => { + it('should mark an object as new', () => { + const markedRecord = markRecordAsNew(record); + expect(markedRecord).toBe(record); + expect(isNewRecord(record)).toBe(true); + }); + + it('should not affect other properties of the object', () => { + record.someProperty = 'some value'; + markRecordAsNew(record); + expect(record.someProperty).toBe('some value'); + }); + + it('should throw an error when trying to mark non-object types as new', () => { + expect(() => markRecordAsNew(null)).toThrow(); + expect(() => markRecordAsNew(undefined)).toThrow(); + // @ts-ignore + expect(() => markRecordAsNew(42)).toThrow(); + // @ts-ignore + expect(() => markRecordAsNew('not an object')).toThrow(); + }); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/collection-template/CollectionTemplateManager.test.ts b/packages/core/client/src/data-source/__tests__/collection-template/CollectionTemplateManager.test.ts new file mode 100644 index 000000000..fa6875f95 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/collection-template/CollectionTemplateManager.test.ts @@ -0,0 +1,99 @@ +import { Application, CollectionTemplate, CollectionTemplateManager, Collection } from '@nocobase/client'; + +describe('CollectionTemplateManager', () => { + let collectionTemplateManager: CollectionTemplateManager; + + beforeEach(() => { + const app = new Application(); + collectionTemplateManager = app.dataSourceManager.collectionTemplateManager; + }); + + it('adds new templates', () => { + class A extends CollectionTemplate { + name = 'a'; + } + collectionTemplateManager.addCollectionTemplates([A]); + + expect(collectionTemplateManager.getCollectionTemplate('a')).instanceOf(A); + expect(collectionTemplateManager.getCollectionTemplates()).toHaveLength(1); + + class B extends CollectionTemplate { + name = 'b'; + } + collectionTemplateManager.addCollectionTemplates([B]); + + expect(collectionTemplateManager.getCollectionTemplate('b')).instanceOf(B); + expect(collectionTemplateManager.getCollectionTemplates()).toHaveLength(2); + }); + + it('overrides duplicates', () => { + class A extends CollectionTemplate { + name = 'a'; + } + class B extends CollectionTemplate { + name = 'a'; + } + collectionTemplateManager.addCollectionTemplates([A, B]); + + expect(collectionTemplateManager.getCollectionTemplate('a')).instanceOf(B); + expect(collectionTemplateManager.getCollectionTemplates()).toHaveLength(1); + }); + + it('should re-add collections', () => { + class CollectionTest extends Collection {} + + class A extends CollectionTemplate { + name = 'a'; + Collection = CollectionTest; + } + + const collectionA = { + name: 'collectionA', + template: 'a', + }; + collectionTemplateManager.dataSourceManager.getDataSource().collectionManager.addCollections([collectionA]); + expect( + collectionTemplateManager.dataSourceManager.getDataSource().collectionManager.getCollection('collectionA'), + ).instanceOf(Collection); + collectionTemplateManager.addCollectionTemplates([A]); + expect( + collectionTemplateManager.dataSourceManager.getDataSource().collectionManager.getCollection('collectionA'), + ).instanceOf(CollectionTest); + }); + + describe('getCollectionTemplates', () => { + beforeEach(() => { + class A extends CollectionTemplate { + name = 'a'; + supportDataSourceType = ['sourceA']; + } + class B extends CollectionTemplate { + name = 'b'; + notSupportDataSourceType = ['sourceA']; + } + class C extends CollectionTemplate { + name = 'c'; + } + collectionTemplateManager.addCollectionTemplates([A, B, C]); + }); + + it('should return all stored templates when no dataSourceType is provided', () => { + const templates = collectionTemplateManager.getCollectionTemplates(); + expect(templates.length).toBe(3); + }); + + it('should return supported templates when dataSourceType is provided', () => { + const templates = collectionTemplateManager.getCollectionTemplates('sourceA'); + expect(templates.length).toBe(2); + expect(templates[0].name).toBe('a'); + expect(templates[1].name).toBe('c'); + }); + + it('should return empty array when an unsupported dataSourceType is provided', () => { + const templates = collectionTemplateManager.getCollectionTemplates('sourceB'); + expect(templates.length).toBe(2); + expect(templates[0].name).toBe('b'); + expect(templates[1].name).toBe('c'); + }); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/collection/AssociationProvider.test.tsx b/packages/core/client/src/data-source/__tests__/collection/AssociationProvider.test.tsx new file mode 100644 index 000000000..afe2f1b18 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/collection/AssociationProvider.test.tsx @@ -0,0 +1,99 @@ +import React, { ComponentType } from 'react'; +import { render, screen } from '@nocobase/test/client'; +import { + AssociationProvider, + useCollectionField, + useCollection, + Application, + SchemaComponent, + SchemaComponentProvider, + DataSourceApplicationProvider, + useParentCollection, +} from '@nocobase/client'; +import collections from '../collections.json'; + +function renderApp(Demo: ComponentType, props: any = {}) { + const app = new Application({ + dataSourceManager: { + collections: collections as any, + dataSources: [ + { + key: 'a', + displayName: 'a', + collections: collections as any, + }, + ], + }, + }); + + const schema = { + name: 'root', + type: 'void', + 'x-component': 'Demo', + 'x-decorator': 'AssociationProvider', + 'x-decorator-props': props, + }; + + return render( +
+ + + + + +
, + ); +} + +describe('AssociationProvider', () => { + test('should render', () => { + const Demo = () => { + const collection = useCollection(); + const parentCollection = useParentCollection(); + const collectionFiled = useCollectionField(); + return ( + <> +
{collection.name}
+
{parentCollection.name}
+
{collectionFiled.name}
+ + ); + }; + + renderApp(Demo, { name: 'users.roles' }); + + expect(screen.getByTestId('collection')).toHaveTextContent('roles'); + expect(screen.getByTestId('parent-collection')).toHaveTextContent('users'); + expect(screen.getByTestId('field')).toHaveTextContent('roles'); + }); + + test('should render with dataSource', () => { + const Demo = () => { + const collection = useCollection(); + const parentCollection = useParentCollection(); + const collectionFiled = useCollectionField(); + return ( + <> +
{collection.name}
+
{parentCollection.name}
+
{collectionFiled.name}
+ + ); + }; + + renderApp(Demo, { name: 'users.roles', dataSource: 'a' }); + + expect(screen.getByTestId('collection')).toHaveTextContent('roles'); + expect(screen.getByTestId('parent-collection')).toHaveTextContent('users'); + expect(screen.getByTestId('field')).toHaveTextContent('roles'); + }); + + test('not exists, should render `CollectionDeletedPlaceholder`', () => { + const Demo = () => { + return
Demo
; + }; + renderApp(Demo, { name: 'users.not-exists' }); + + expect(document.body.innerHTML).toContain('ant-result'); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/collection/Collection.test.tsx b/packages/core/client/src/data-source/__tests__/collection/Collection.test.tsx new file mode 100644 index 000000000..b82eb8baf --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/collection/Collection.test.tsx @@ -0,0 +1,218 @@ +import { Application, CollectionOptions, DEFAULT_DATA_SOURCE_KEY } from '@nocobase/client'; +import collections from '../collections.json'; + +function getCollection(collection: CollectionOptions) { + const app = new Application({ + dataSourceManager: { + collections: [collection], + }, + }); + + return app.getCollectionManager().getCollection(collection.name); +} + +describe('Collection', () => { + describe('getPrimaryKey()', () => { + test('Return `targetKey` if targetKey property exists', () => { + const collection = getCollection({ name: 'test', targetKey: 'a' }); + expect(collection.getPrimaryKey()).toBe('a'); + }); + + test('If targetKey does not exist, return the name of the field with `primaryKey` set to true in the fields', () => { + const collection = getCollection({ + name: 'test', + fields: [{ name: 'a', primaryKey: true }, { name: 'b' }], + }); + expect(collection.getPrimaryKey()).toBe('a'); + }); + + test('cache the result', () => { + const collection = getCollection({ name: 'test', fields: [{ name: 'a', primaryKey: true }] }); + const spy = vitest.spyOn(collection, 'getFields'); + collection.getPrimaryKey(); + collection.getPrimaryKey(); + expect(spy).toHaveBeenCalledTimes(1); + }); + }); + + describe('titleField', () => { + test('return `titleField` if it exists', () => { + const collection = getCollection({ name: 'test', titleField: 'a', fields: [{ name: 'a' }] }); + expect(collection.titleField).toBe('a'); + }); + + test('if `titleField` does not exist in fields, return `primaryKey`', () => { + const collection = getCollection({ name: 'test', titleField: 'a', targetKey: 'b' }); + expect(collection.titleField).toBe('b'); + }); + + test('if `titleField` does not exist, return `primaryKey`', () => { + const collection = getCollection({ name: 'test', targetKey: 'a' }); + expect(collection.titleField).toBe('a'); + }); + }); + + describe('options', () => { + test('getOptions()', () => { + const collection = getCollection({ name: 'test', titleField: 'a', targetKey: 'b' }); + expect(collection.getOptions()).toMatchObject({ + name: 'test', + titleField: 'a', + targetKey: 'b', + dataSource: DEFAULT_DATA_SOURCE_KEY, + }); + }); + + test('getOption()', () => { + const collection = getCollection({ name: 'test', key: 'a', model: 'b' }); + expect(collection.getOption('name')).toBe('test'); + expect(collection.getOption('key')).toBe('a'); + expect(collection.getOption('model')).toBe('b'); + expect(collection.getOption('dataSource')).toBe(DEFAULT_DATA_SOURCE_KEY); + }); + }); + + test('dataSource', () => { + const app = new Application({ + dataSourceManager: { + dataSources: [ + { + key: 'a', + displayName: 'a', + collections: [{ name: 'user' }], + }, + ], + }, + }); + + const user = app.getCollectionManager('a').getCollection('user'); + expect(user.dataSource).toBe('a'); + }); + + describe('getFields()', () => { + test('return fields', () => { + const collection = getCollection({ name: 'test', fields: [{ name: 'a' }, { name: 'b' }] }); + expect(collection.getFields()).toMatchObject([{ name: 'a' }, { name: 'b' }]); + }); + + test('support predicate', () => { + const collection = getCollection({ + name: 'test', + fields: [ + { name: 'a', primaryKey: false }, + { name: 'b', primaryKey: true }, + ], + }); + + // { key: value } + const res1 = collection.getFields({ name: 'a' }); + expect(res1.length).toBe(1); + expect(res1[0].name).toBe('a'); + + // key === { key: true } + const res2 = collection.getFields('primaryKey'); + expect(res2.length).toBe(1); + expect(res2[0].name).toBe('b'); + + const res3 = collection.getFields({ primaryKey: true }); + expect(res3.length).toBe(1); + expect(res3[0].name).toBe('b'); + + // function + const res4 = collection.getFields((field) => field.name === 'a'); + expect(res4.length).toBe(1); + expect(res4[0].name).toBe('a'); + }); + }); + + describe('getField()', () => { + test('return field', () => { + const collection = getCollection({ name: 'test', fields: [{ name: 'a' }, { name: 'b' }] }); + expect(collection.getField('a')).toMatchObject({ name: 'a' }); + + expect(collection.getField('test.a')).toMatchObject({ name: 'a' }); + }); + + test('support dot notation', () => { + const app = new Application({ + dataSourceManager: { + collections: collections as any, + }, + }); + + const users = app.getCollectionManager().getCollection('users'); + expect(users.getField('roles.name')).toMatchObject({ name: 'name', collectionName: 'roles' }); + }); + + test('return undefined if field does not exist', () => { + const app = new Application({ + dataSourceManager: { + collections: collections as any, + }, + }); + + const users = app.getCollectionManager().getCollection('users'); + + expect(users.getField('no-exist')).toBeUndefined(); + expect(users.getField('no-exist.c')).toBeUndefined(); + expect(users.getField('id.no-exist')).toBeUndefined(); + expect(users.getField('roles.no-exist')).toBeUndefined(); + }); + }); + + describe('hasField()', () => { + test('return true if field exists', () => { + const collection = getCollection({ name: 'test', fields: [{ name: 'a' }, { name: 'b' }] }); + expect(collection.hasField('a')).toBe(true); + }); + + test('return false if field does not exist', () => { + const collection = getCollection({ name: 'test', fields: [{ name: 'a' }, { name: 'b' }] }); + expect(collection.hasField('c')).toBe(false); + }); + }); + + test('properties', () => { + const app = new Application({ + dataSourceManager: { + collections: collections as any, + }, + }); + + const users = app.getCollectionManager().getCollection('users'); + const usersOptions: any = collections.find((c) => c.name === 'users'); + + expect(users.app).toBe(app); + expect(users.template).toBe(usersOptions.template); + expect(users.sourceKey).toBe(usersOptions.sourceKey); + expect(users.name).toBe(usersOptions.name); + expect(users.key).toBe(usersOptions.key); + expect(users.title).toBe(usersOptions.title); + expect(users.inherit).toBe(usersOptions.inherit); + expect(users.hidden).toBe(usersOptions.hidden); + expect(users.description).toBe(usersOptions.description); + expect(users.duplicator).toBe(usersOptions.duplicator); + expect(users.category).toBe(usersOptions.category); + expect(users.targetKey).toBe(usersOptions.targetKey); + expect(users.model).toBe(usersOptions.model); + expect(users.createdBy).toBe(usersOptions.createdBy); + expect(users.updatedBy).toBe(usersOptions.updatedBy); + expect(users.logging).toBe(usersOptions.logging); + expect(users.from).toBe(usersOptions.from); + expect(users.rawTitle).toBe(usersOptions.rawTitle); + expect(users.inherits).toMatchObject([]); + expect(users.sources).toMatchObject([]); + expect(users.fields).toMatchObject(usersOptions.fields); + expect(users.tableName).toBe(usersOptions.tableName); + expect(users.viewName).toBe(usersOptions.viewName); + expect(users.writableView).toBe(usersOptions.writableView); + expect(users.filterTargetKey).toBe(usersOptions.filterTargetKey); + expect(users.sortable).toBe(usersOptions.sortable); + expect(users.autoGenId).toBe(usersOptions.autoGenId); + expect(users.magicAttribute).toBe(usersOptions.magicAttribute); + expect(users.tree).toBe(usersOptions.tree); + expect(users.isThrough).toBe(usersOptions.isThrough); + expect(users.autoCreate).toBe(usersOptions.autoCreate); + expect(users.resource).toBe(usersOptions.resource); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/collection/CollectionManager.test.tsx b/packages/core/client/src/data-source/__tests__/collection/CollectionManager.test.tsx new file mode 100644 index 000000000..ddfaf80c2 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/collection/CollectionManager.test.tsx @@ -0,0 +1,230 @@ +import { Application, CollectionManager, CollectionTemplate, Collection } from '@nocobase/client'; +import collections from '../collections.json'; +import { app } from '../../../application/demos/demo3'; + +describe('CollectionManager', () => { + let collectionManager: CollectionManager; + + beforeEach(() => { + const app = new Application({}); + collectionManager = app.getCollectionManager(); + }); + describe('constructor', () => { + it('should initialize with empty collections', () => { + expect(collectionManager.collectionInstancesMap).toEqual({}); + expect(collectionManager.collectionInstancesArr).toEqual([]); + }); + }); + + describe('addCollections', () => { + it('should add new collections', () => { + const collections = [{ name: 'test' }]; + collectionManager.addCollections(collections as any[]); + expect(collectionManager.collectionInstancesArr.length).toBe(1); + expect(collectionManager.collectionInstancesMap['test']).instanceOf(Collection); + }); + + it('should merge with existing collections', () => { + const initialCollections = [{ name: 'initial' }]; + const additionalCollections = [{ name: 'additional' }]; + collectionManager.addCollections(initialCollections as any[]); + collectionManager.addCollections(additionalCollections as any[]); + expect(collectionManager.collectionInstancesArr.length).toBe(2); + }); + + it('should not add duplicate collections', () => { + const collections = [{ name: 'test' }, { name: 'test' }]; + collectionManager.addCollections(collections as any[]); + expect(collectionManager.collectionInstancesArr.length).toBe(1); + }); + + it('custom Collection class', () => { + class CustomCollection extends Collection {} + + class CustomTemplate extends CollectionTemplate { + name = 'custom'; + Collection = CustomCollection; + } + + const collections = [{ name: 'test', template: 'custom' }]; + collectionManager.dataSourceManager.addCollectionTemplates([CustomTemplate]); + collectionManager.addCollections(collections as any[]); + const collection = collectionManager.getCollection('test'); + expect(collection).instanceOf(CustomCollection); + }); + + it('custom Template with transform', () => { + class CustomTemplate extends CollectionTemplate { + name = 'custom'; + transform(collection) { + collection.test = 'transformed'; + return collection; + } + } + + const collections = [{ name: 'test', template: 'custom' }]; + collectionManager.dataSourceManager.addCollectionTemplates([CustomTemplate]); + collectionManager.addCollections(collections as any[]); + const collection = collectionManager.getCollection<{ test: string }>('test'); + expect(collection.getOption('test')).toBe('transformed'); + }); + }); + + describe('setCollections', () => { + it('should replace existing collections', () => { + const initialCollections = [{ name: 'initial' }]; + const newCollections = [{ name: 'new' }]; + collectionManager.addCollections(initialCollections as any[]); + collectionManager.setCollections(newCollections as any[]); + expect(collectionManager.collectionInstancesArr.length).toBe(1); + expect(Object.keys(collectionManager.collectionInstancesMap)).toEqual(['new']); + }); + }); + + describe('reAddCollections', () => { + it('should re-add existing collections', () => { + const initialCollections = [ + { name: 'a', rawTitle: 'aa' }, + { name: 'b', rawTitle: 'bb' }, + ]; + + collectionManager.addCollections(initialCollections as any[]); + + collectionManager.getCollection('a'); + + const spy = vitest.spyOn(collectionManager, 'addCollections'); + collectionManager.reAddCollections(); + expect(collectionManager.collectionInstancesArr.length).toBe(2); + + expect(spy).toHaveBeenCalledWith([ + collectionManager.getCollection('a').getOptions(), + collectionManager.getCollection('b').getOptions(), + ]); + }); + + it('should re-add specified collections with filter', () => { + const initialCollections = [ + { name: 'a', rawTitle: 'aa' }, + { name: 'b', rawTitle: 'bb' }, + ]; + + collectionManager.addCollections(initialCollections as any[]); + + const spy = vitest.spyOn(collectionManager, 'addCollections'); + collectionManager.reAddCollections([collectionManager.getCollection('a')]); + expect(collectionManager.collectionInstancesArr.length).toBe(2); + + expect(spy).toHaveBeenCalledWith([collectionManager.getCollection('a').getOptions()]); + }); + }); + + describe('getCollection', () => { + it('should get a collection by name', () => { + const collections = [{ name: 'test' }]; + collectionManager.addCollections(collections as any[]); + const collection = collectionManager.getCollection('test'); + expect(collection).toBeDefined(); + }); + + it('should return undefined for non-existing collection', () => { + const collection = collectionManager.getCollection('nonExisting'); + expect(collection).toBeUndefined(); + }); + + it('should handle path with associations', () => { + collectionManager.addCollections(collections as any[]); + const collection = collectionManager.getCollection('users.roles'); + expect(collection).toBeDefined(); + }); + + it('if collection is object, should return the collection instance', () => { + const collection = collectionManager.getCollection({ name: 'test' }); + expect(collection).instanceOf(Collection); + expect(collection.name).toBe('test'); + }); + }); + + describe('getCollections', () => { + it('should return all collections', () => { + const collections = [{ name: 'test1' }, { name: 'test2' }]; + collectionManager.addCollections(collections as any[]); + const allCollections = collectionManager.getCollections(); + expect(allCollections.length).toBe(2); + }); + + it('should return filtered collections', () => { + const collections = [{ name: 'test1' }, { name: 'test2' }]; + collectionManager.addCollections(collections as any[]); + const filteredCollections = collectionManager.getCollections((c) => c.name === 'test1'); + expect(filteredCollections.length).toBe(1); + expect(filteredCollections[0].name).toBe('test1'); + }); + }); + + describe('getCollectionName', () => { + it('should return the name of a collection', () => { + const collections = [{ name: 'test' }]; + collectionManager.addCollections(collections as any[]); + const name = collectionManager.getCollectionName('test'); + expect(name).toBe('test'); + }); + }); + + describe('getCollectionField', () => { + beforeEach(() => { + collectionManager.addCollections(collections as any[]); + }); + + it('should return undefined for invalid path', () => { + const field = collectionManager.getCollectionField('invalid'); + expect(field).toBeUndefined(); + + const field2 = collectionManager.getCollectionField(undefined); + expect(field2).toBeUndefined(); + }); + + it('should return a collection field by path', () => { + const field = collectionManager.getCollectionField('users.nickname'); + expect(field).toBeDefined(); + expect(field.name).toBe('nickname'); + }); + + it('if collection not found, should return undefined', () => { + const field = collectionManager.getCollectionField('invalid.nickname'); + expect(field).toBeUndefined(); + }); + + it('if field is object, should return the field', () => { + const field = collectionManager.getCollectionField({ name: 'nickname' }); + expect(field).toEqual({ name: 'nickname' }); + }); + }); + + describe('getCollectionFields', () => { + beforeEach(() => { + collectionManager.addCollections(collections as any[]); + }); + + it('should return fields of a specified collection', () => { + const fields = collectionManager.getCollectionFields('users'); + expect(fields.length).greaterThan(1); + }); + + it('if collection not found, should return an empty array', () => { + const fields = collectionManager.getCollectionFields('invalid'); + expect(fields).toEqual([]); + }); + }); + + describe('clone', () => { + it('should create a clone of the collection manager', () => { + const collections = [{ name: 'test' }]; + collectionManager.addCollections(collections as any[]); + const clone = collectionManager.clone(); + expect(clone).not.toBe(collectionManager); + expect(clone.collectionInstancesArr).toEqual(collectionManager.collectionInstancesArr); + expect(clone.collectionInstancesMap).toEqual(collectionManager.collectionInstancesMap); + expect(clone.dataSource).toBe(collectionManager.dataSource); + }); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/collection/CollectionManagerProvider.test.tsx b/packages/core/client/src/data-source/__tests__/collection/CollectionManagerProvider.test.tsx new file mode 100644 index 000000000..abe01efb8 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/collection/CollectionManagerProvider.test.tsx @@ -0,0 +1,118 @@ +import React, { ComponentType, useEffect } from 'react'; +import { render, screen } from '@nocobase/test/client'; +import { + CollectionManager, + useCollectionManager, + useCollections, + Application, + DataSourceApplicationProvider, + CollectionManagerProvider, + ExtendCollectionsProvider, +} from '@nocobase/client'; +import collections from '../collections.json'; + +function renderApp(Demo: ComponentType, dataSource?: string) { + const app = new Application({ + dataSourceManager: { + collections: collections as any, + dataSources: [ + { + key: 'a', + displayName: 'a', + collections: [collections[0]] as any, + }, + ], + }, + }); + return render( + + + , + ); +} + +describe('CollectionManagerProvider', () => { + test('should render', () => { + const Demo = () => { + const cm = useCollectionManager(); + useEffect(() => { + expect(cm instanceof CollectionManager).toBeTruthy(); + }, [cm]); + const users = cm.getCollection('users'); + return
{users.name}
; + }; + renderApp(Demo); + + expect(screen.getByTestId('demo')).toHaveTextContent('users'); + }); + + test('useCollections()', () => { + const Demo = () => { + const collections = useCollections(); + return
{collections.length}
; + }; + renderApp(Demo); + + expect(screen.getByTestId('demo')).toHaveTextContent('2'); + }); + + test('useCollections({ predicate })', () => { + const Demo = () => { + const collections = useCollections((collection) => collection.name === 'users'); + return
{collections.length}
; + }; + renderApp(Demo); + + expect(screen.getByTestId('demo')).toHaveTextContent('1'); + }); + + test('useCollections() with dataSource', () => { + const Demo = () => { + const collections = useCollections(); + return
{collections.length}
; + }; + renderApp(Demo, 'a'); + + expect(screen.getByTestId('demo')).toHaveTextContent('1'); + }); + + test('extend collections by props', () => { + const Demo = () => { + const collections = useCollections(); + return
{collections.length}
; + }; + + const Wrapper = () => { + return ( + + + + ); + }; + + renderApp(Wrapper, 'a'); + + expect(screen.getByTestId('demo')).toHaveTextContent('2'); + }); + + test('extend collections by ExtendCollectionsProvider', () => { + const Demo = () => { + const collections = useCollections(); + return
{collections.length}
; + }; + + const Wrapper = () => { + return ( + + + + + + ); + }; + + renderApp(Wrapper, 'a'); + + expect(screen.getByTestId('demo')).toHaveTextContent('2'); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/collection/CollectionProvider.test.tsx b/packages/core/client/src/data-source/__tests__/collection/CollectionProvider.test.tsx new file mode 100644 index 000000000..639e336bb --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/collection/CollectionProvider.test.tsx @@ -0,0 +1,96 @@ +import React, { ComponentType } from 'react'; +import { render, screen } from '@nocobase/test/client'; +import { + CollectionProvider, + useCollectionFields, + useCollection, + Application, + SchemaComponent, + SchemaComponentProvider, + DataSourceApplicationProvider, +} from '@nocobase/client'; +import collections from '../collections.json'; + +function renderApp(Demo: ComponentType, props: any = {}) { + const app = new Application({ + dataSourceManager: { + collections: collections as any, + }, + }); + const schema = { + name: 'root', + type: 'void', + 'x-component': 'Demo', + 'x-decorator': 'CollectionProvider', + 'x-decorator-props': props, + }; + + return render( +
+ + + + + +
, + ); +} + +describe('CollectionProvider', () => { + test('should render', () => { + const Demo = () => { + const collection = useCollection(); + const collectionFields = useCollectionFields(); + return ( + <> +
{collection.name}
+
{collectionFields.length}
+ + ); + }; + + renderApp(Demo, { name: 'users' }); + + expect(screen.getByTestId('name')).toHaveTextContent('users'); + + const usersOptions = collections.find((item) => item.name === 'users'); + expect(screen.getByTestId('fields')).toHaveTextContent(String(usersOptions.fields.length)); + }); + + test('collection not exists and { allowNull: true }, should render children', () => { + const Demo = () => { + const collection = useCollection(); + expect(collection).toBeFalsy(); + + return
children
; + }; + + renderApp(Demo, { name: 'not-exists', allowNull: true }); + + expect(screen.getByTestId('children')).toHaveTextContent('children'); + }); + + test('collection not exists and { allowNull: false }, should render `CollectionDeletedPlaceholder` content', () => { + const Demo = () => { + const collection = useCollection(); + expect(collection).toBeFalsy(); + + return
children
; + }; + + renderApp(Demo, { name: 'not-exists', allowNull: false }); + + expect(document.body.innerHTML).toContain('ant-result'); + }); + + test('useCollectionFields() support predicate', () => { + const Demo = () => { + const fields = useCollectionFields({ name: 'id' }); + return
{fields.length}
; + }; + + renderApp(Demo, { name: 'users' }); + + expect(screen.getByTestId('fields')).toHaveTextContent('1'); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/collection/ExtendCollectionsProvider.test.tsx b/packages/core/client/src/data-source/__tests__/collection/ExtendCollectionsProvider.test.tsx new file mode 100644 index 000000000..21531f1de --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/collection/ExtendCollectionsProvider.test.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { + ExtendCollectionsProvider, + Application, + DataSourceManagerProvider, + CollectionManagerProvider, + useCollectionManager, +} from '@nocobase/client'; +import { useDataSourceKey } from '../../data-source/DataSourceProvider'; + +describe('ExtendCollectionsProvider', () => { + const app = new Application({ + dataSourceManager: { + dataSources: [ + { + key: 'test', + displayName: 'test', + }, + ], + }, + }); + + const TestComponent = () => { + const cm = useCollectionManager(); + const dataSourceKey = useDataSourceKey(); + const collections = cm.getCollections(); + return ( + <> +
{dataSourceKey}
+
+ {collections.map((collection) => ( +
{collection.name}
+ ))} +
+ + ); + }; + + const App = ({ children }) => { + return ( + + {children} + + ); + }; + + test('should provide extend collections', () => { + const collections = [{ name: 'collection1' }, { name: 'collection2' }]; + + render( + + + + + , + ); + + expect(screen.getByTestId('collections')).toHaveTextContent('collection1'); + expect(screen.getByTestId('collections')).toHaveTextContent('collection2'); + + expect(screen.getByTestId('data-source')).toHaveTextContent('test'); + }); + + it('extends parent collections with the given ones', () => { + render( + + + + + + + , + ); + + expect(screen.getByTestId('collections')).toHaveTextContent('collection1'); + expect(screen.getByTestId('collections')).toHaveTextContent('collection2'); + + expect(screen.getByTestId('data-source')).toHaveTextContent('test'); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/collections.json b/packages/core/client/src/data-source/__tests__/collections.json new file mode 100644 index 000000000..9d8fc5f6b --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/collections.json @@ -0,0 +1,223 @@ +[ + { + "key": "h7b9i8khc3q", + "name": "users", + "inherit": false, + "hidden": false, + "description": null, + "category": [], + "namespace": "users.users", + "duplicator": { + "dumpable": "optional", + "with": "rolesUsers" + }, + "sortable": "sort", + "model": "UserModel", + "createdBy": true, + "updatedBy": true, + "logging": true, + "from": "db2cm", + "title": "{{t(\"Users\")}}", + "rawTitle": "{{t(\"Users\")}}", + "fields": [ + { + "uiSchema": { + "type": "number", + "title": "{{t(\"ID\")}}", + "x-component": "InputNumber", + "x-read-pretty": true, + "rawTitle": "{{t(\"ID\")}}" + }, + "key": "ffp1f2sula0", + "name": "id", + "type": "bigInt", + "interface": "id", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "autoIncrement": true, + "primaryKey": true, + "allowNull": false + }, + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Nickname\")}}", + "x-component": "Input", + "rawTitle": "{{t(\"Nickname\")}}" + }, + "key": "vrv7yjue90g", + "name": "nickname", + "type": "string", + "interface": "input", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null + }, + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Username\")}}", + "x-component": "Input", + "x-validator": { + "username": true + }, + "required": true, + "rawTitle": "{{t(\"Username\")}}" + }, + "key": "2ccs6evyrub", + "name": "username", + "type": "string", + "interface": "input", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "unique": true + }, + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Email\")}}", + "x-component": "Input", + "x-validator": "email", + "required": true, + "rawTitle": "{{t(\"Email\")}}" + }, + "key": "rrskwjl5wt1", + "name": "email", + "type": "string", + "interface": "email", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "unique": true + }, + { + "key": "t09bauwm0wb", + "name": "roles", + "type": "belongsToMany", + "interface": "m2m", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "target": "roles", + "foreignKey": "userId", + "otherKey": "roleName", + "onDelete": "CASCADE", + "sourceKey": "id", + "targetKey": "name", + "through": "rolesUsers", + "uiSchema": { + "type": "array", + "title": "{{t(\"Roles\")}}", + "x-component": "AssociationField", + "x-component-props": { + "multiple": true, + "fieldNames": { + "label": "title", + "value": "name" + } + } + } + }, + { + "key": "1pz0art9mt7", + "name": "f_n2fu6hvprct", + "type": "string", + "interface": "select", + "description": null, + "collectionName": "t_vwpds9fs4xs", + "parentKey": null, + "reverseKey": null, + "uiSchema": { + "enum": [ + { + "value": "test1", + "label": "test1" + }, + { + "value": "test2", + "label": "test2" + } + ], + "type": "string", + "x-component": "Select", + "title": "test" + } + } + ] + }, + { + "key": "pqnenvqrzxr", + "name": "roles", + "inherit": false, + "hidden": false, + "description": null, + "category": [], + "namespace": "acl.acl", + "duplicator": { + "dumpable": "required", + "with": "uiSchemas" + }, + "autoGenId": false, + "model": "RoleModel", + "filterTargetKey": "name", + "sortable": true, + "from": "db2cm", + "title": "{{t(\"Roles\")}}", + "rawTitle": "{{t(\"Roles\")}}", + "fields": [ + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Role UID\")}}", + "x-component": "Input", + "rawTitle": "{{t(\"Role UID\")}}" + }, + "key": "jbz9m80bxmp", + "name": "name", + "type": "uid", + "interface": "input", + "description": null, + "collectionName": "roles", + "parentKey": null, + "reverseKey": null, + "prefix": "r_", + "primaryKey": true + }, + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Role name\")}}", + "x-component": "Input", + "rawTitle": "{{t(\"Role name\")}}" + }, + "key": "faywtz4sf3u", + "name": "title", + "type": "string", + "interface": "input", + "description": null, + "collectionName": "roles", + "parentKey": null, + "reverseKey": null, + "unique": true, + "translation": true + }, + { + "key": "1enkovm9sye", + "name": "description", + "type": "string", + "interface": null, + "description": null, + "collectionName": "roles", + "parentKey": null, + "reverseKey": null + } + ] + } +] diff --git a/packages/core/client/src/data-source/__tests__/components/CollectionDeletedPlaceholder.test.tsx b/packages/core/client/src/data-source/__tests__/components/CollectionDeletedPlaceholder.test.tsx new file mode 100644 index 000000000..eb304d888 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/components/CollectionDeletedPlaceholder.test.tsx @@ -0,0 +1,78 @@ +import React from 'react'; + +import { render, screen } from '@nocobase/test/client'; +import { CollectionDeletedPlaceholder, SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; + +function renderApp(name?: any, designable?: boolean) { + const schema = { + name: 'root', + type: 'void', + 'x-component': 'CollectionDeletedPlaceholder', + 'x-component-props': { + name, + type: 'Collection', + }, + }; + + render( +
+ + + +
, + ); +} + +describe('CollectionDeletedPlaceholder', () => { + test('name is undefined, render `Result` component', () => { + renderApp(undefined, true); + + expect(document.body.innerHTML).toContain('ant-result'); + }); + + describe('name exist', () => { + test("designable: true & process.env.NODE_ENV === 'development', render `Result` component", () => { + const NODE_ENV = process.env.NODE_ENV; + process.env.NODE_ENV = 'development'; + + renderApp('test', true); + + process.env.NODE_ENV = NODE_ENV; + + expect(document.body.innerHTML).toContain('ant-result'); + }); + + test("designable: false & process.env.NODE_ENV === 'development', render `Result` component", () => { + const NODE_ENV = process.env.NODE_ENV; + process.env.NODE_ENV = 'development'; + + renderApp('test', false); + + process.env.NODE_ENV = NODE_ENV; + + expect(document.body.innerHTML).toContain('ant-result'); + }); + + test("designable: true & process.env.NODE_ENV !== 'development', render `Result` component", () => { + const NODE_ENV = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + + renderApp('test', true); + + process.env.NODE_ENV = NODE_ENV; + + expect(document.body.innerHTML).toContain('ant-result'); + }); + + test("designable: false & process.env.NODE_ENV !== 'development', render nothing", () => { + const NODE_ENV = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + + renderApp('test', false); + + process.env.NODE_ENV = NODE_ENV; + + expect(screen.getByTestId('app').innerHTML.length).toBe(0); + }); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/data-block/DataBlockProvider.test.tsx b/packages/core/client/src/data-source/__tests__/data-block/DataBlockProvider.test.tsx new file mode 100644 index 000000000..fcf93e7c8 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/data-block/DataBlockProvider.test.tsx @@ -0,0 +1,141 @@ +import React from 'react'; +import { fireEvent, render, screen, waitFor } from '@nocobase/test/client'; +import CollectionTableListDemo from './data-block-demos/collection-table-list'; +import CollectionFormGetAndUpdateDemo from './data-block-demos/collection-form-get-and-update'; +import CollectionFormCreateDemo from './data-block-demos/collection-form-create'; +import CollectionFormRecordAndUpdateDemo from './data-block-demos/collection-form-record-and-update'; +import AssociationTableListAndSourceIdDemo from './data-block-demos/association-table-list-and-source-id'; +import AssociationTableListAndParentRecordDemo from './data-block-demos/association-table-list-and-parent-record'; + +describe('CollectionDataSourceProvider', () => { + describe('collection', () => { + test('Table list', async () => { + const { getByText, getByRole } = render(); + + // app loading + await waitFor(() => { + expect(getByRole('table')).toBeInTheDocument(); + }); + + expect(getByText('UserName')).toBeInTheDocument(); + expect(getByText('NickName')).toBeInTheDocument(); + expect(getByText('Email')).toBeInTheDocument(); + + // loading table data + await waitFor(() => { + const columns = screen.getByRole('table').querySelectorAll('tbody tr'); + expect(columns.length).toBe(3); + }); + + expect(getByText('jack')).toBeInTheDocument(); + expect(getByText('Jack Ma')).toBeInTheDocument(); + expect(getByText('test@gmail.com')).toBeInTheDocument(); + }); + + test('Form get & update', async () => { + render(); + + await waitFor(() => { + expect(screen.getByText('Username')).toBeInTheDocument(); + expect(screen.getByText('Age')).toBeInTheDocument(); + }); + + // load form data success + await waitFor(() => { + expect(document.getElementById('username')).toHaveValue('Bamboo'); + expect(document.getElementById('age')).toHaveValue('18'); + }); + + fireEvent.click(document.querySelector('button')); + + await waitFor(() => { + expect(screen.getByText('Save successfully!')).toBeInTheDocument(); + }); + }); + + test('Form create', async () => { + render(); + + await waitFor(() => { + expect(screen.getByText('Username')).toBeInTheDocument(); + expect(screen.getByText('Age')).toBeInTheDocument(); + }); + + fireEvent.change(document.getElementById('username'), { target: { value: 'Bamboo' } }); + fireEvent.change(document.getElementById('age'), { target: { value: '18' } }); + + fireEvent.click(document.querySelector('button')); + + await waitFor(() => { + expect(screen.getByText('Save successfully!')).toBeInTheDocument(); + }); + }); + + test('Form record & update', async () => { + render(); + + await waitFor(() => { + expect(screen.getByText('Username')).toBeInTheDocument(); + expect(screen.getByText('Age')).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(document.getElementById('username')).toHaveValue('Bamboo'); + expect(document.getElementById('age')).toHaveValue('18'); + }); + + fireEvent.click(document.querySelector('button')); + + await waitFor(() => { + expect(screen.getByText('Save successfully!')).toBeInTheDocument(); + }); + }); + }); + + describe('association', () => { + test('Table list & sourceId', async () => { + const { getByText, getByRole } = render(); + + // app loading + await waitFor(() => { + expect(getByRole('table')).toBeInTheDocument(); + }); + + expect(getByText('Name')).toBeInTheDocument(); + expect(getByText('Title')).toBeInTheDocument(); + expect(getByText('Description')).toBeInTheDocument(); + + // loading table data + await waitFor(() => { + const columns = screen.getByRole('table').querySelectorAll('tbody tr'); + expect(columns.length).toBe(2); + }); + + expect(getByText('admin')).toBeInTheDocument(); + expect(getByText('Admin')).toBeInTheDocument(); + expect(getByText('Admin description')).toBeInTheDocument(); + }); + test('Table list & parentRecord', async () => { + const { getByText, getByRole } = render(); + + // app loading + await waitFor(() => { + expect(getByRole('table')).toBeInTheDocument(); + }); + + expect(getByText('Name')).toBeInTheDocument(); + expect(getByText('Title')).toBeInTheDocument(); + expect(getByText('Description')).toBeInTheDocument(); + + // loading table data + await waitFor(() => { + const columns = screen.getByRole('table').querySelectorAll('tbody tr'); + expect(columns.length).toBe(2); + }); + + expect(getByText('admin')).toBeInTheDocument(); + expect(getByText('Admin')).toBeInTheDocument(); + expect(getByText('Admin description')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/association-table-list-and-parent-record.tsx b/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/association-table-list-and-parent-record.tsx new file mode 100644 index 000000000..0d30c69b9 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/association-table-list-and-parent-record.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { Table, TableProps } from 'antd'; +import { SchemaComponent, UseDataBlockProps, useDataBlockRequest, withDynamicSchemaProps } from '@nocobase/client'; +import { ISchema } from '@formily/json-schema'; +import { createApp } from './createApp'; + +const collection = 'users'; +const associationField = 'roles'; + +const association = `${collection}.${associationField}`; +const action = 'list'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-use-decorator-props': 'useBlockDecoratorProps', + 'x-decorator-props': { + association, + action, + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + }, + }, +}; + +const MyTable = withDynamicSchemaProps(Table); + +function useTableProps(): TableProps { + const { data, loading } = useDataBlockRequest(); + return { + loading, + dataSource: data?.data || [], + columns: [ + { + title: 'Name', + dataIndex: 'name', + }, + { + title: 'Title', + dataIndex: 'title', + }, + { + title: 'Description', + dataIndex: 'description', + }, + ], + }; +} + +const useBlockDecoratorProps: UseDataBlockProps<'CollectionList'> = () => { + const parentRecord = { + id: 1, + username: 'Tom', + }; + return { + parentRecord, + }; +}; + +const Demo = () => { + return ; +}; + +const mocks = { + [`${collection}/1/${associationField}:${action}`]: { + data: [ + { + name: 'admin', + title: 'Admin', + description: 'Admin description', + }, + { + name: 'developer', + title: 'Developer', + description: 'Developer description', + }, + ], + }, +}; + +const Root = createApp( + Demo, + { + components: { MyTable }, + scopes: { useTableProps, useBlockDecoratorProps }, + }, + mocks, +); + +export default Root; diff --git a/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/association-table-list-and-source-id.tsx b/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/association-table-list-and-source-id.tsx new file mode 100644 index 000000000..68d4293bb --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/association-table-list-and-source-id.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { Select, Table, TableProps } from 'antd'; +import { SchemaComponent, UseDataBlockProps, useDataBlockRequest, withDynamicSchemaProps } from '@nocobase/client'; +import { ISchema } from '@formily/json-schema'; + +import useUrlState from '@ahooksjs/use-url-state'; +import { createApp } from './createApp'; + +const collection = 'users'; +const associationField = 'roles'; + +const association = `${collection}.${associationField}`; +const action = 'list'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-use-decorator-props': 'useBlockDecoratorProps', + 'x-decorator-props': { + association, + action, + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', + }, + }, +}; + +const MyTable = withDynamicSchemaProps(Table); + +function useTableProps(): TableProps { + const { data, loading } = useDataBlockRequest(); + return { + loading, + dataSource: data?.data || [], + columns: [ + { + title: 'Name', + dataIndex: 'name', + }, + { + title: 'Title', + dataIndex: 'title', + }, + { + title: 'Description', + dataIndex: 'description', + }, + ], + }; +} + +const useBlockDecoratorProps: UseDataBlockProps<'CollectionList'> = () => { + const [state] = useUrlState({ userId: '1' }); + return { + sourceId: state.userId, + }; +}; + +const Demo = () => { + const [state, setState] = useUrlState({ userId: '1' }); + return ( + <> + + + + ); +}; + +const mocks = { + [`${collection}/1/${associationField}:${action}`]: { + data: [ + { + name: 'admin', + title: 'Admin', + description: 'Admin description', + }, + { + name: 'developer', + title: 'Developer', + description: 'Developer description', + }, + ], + }, + [`${collection}/2/${associationField}:${action}`]: { + data: [ + { + name: 'developer', + title: 'Developer', + description: 'Developer description', + }, + { + name: 'tester', + title: 'Tester', + description: 'Tester description', + }, + ], + }, + [`${collection}:get/1`]: { + id: 1, + username: 'Tom', + }, + [`${collection}:get/2`]: { + id: 1, + username: 'Jack', + }, +}; + +const Root = createApp( + Demo, + { + components: { MyTable }, + scopes: { useTableProps, useBlockDecoratorProps }, + }, + mocks, +); + +export default Root; diff --git a/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/collection-form-create.tsx b/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/collection-form-create.tsx new file mode 100644 index 000000000..b678a48be --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/collection-form-create.tsx @@ -0,0 +1,94 @@ +import React, { FC } from 'react'; +import { Button, Form, FormProps, Input, InputNumber, notification } from 'antd'; +import { SchemaComponent, useDataBlockResource, withDynamicSchemaProps } from '@nocobase/client'; +import { ISchema } from '@formily/json-schema'; +import { createApp } from './createApp'; + +interface DemoFormFieldType { + id: number; + username: string; + age: number; +} +type DemoFormProps = FormProps; +const DemoForm: FC = withDynamicSchemaProps((props) => { + return ( + + + label="Username" + name="username" + rules={[{ required: true, message: 'Please input your username!' }]} + > + + + + label="Age" + name="age" + rules={[{ required: true, message: 'Please input your age!' }]} + > + + + + + + + ); +}); + +function useDemoFormProps(): DemoFormProps { + const resource = useDataBlockResource(); + const onFinish = async (values: DemoFormFieldType) => { + console.log('values', values); + await resource.create({ + values, + }); + notification.success({ + message: 'Save successfully!', + }); + }; + + return { + onFinish, + }; +} + +const collection = 'users'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: collection, + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'object', + 'x-component': 'DemoForm', + 'x-use-component-props': 'useDemoFormProps', + }, + }, +}; + +const Demo = () => { + return ; +}; + +const mocks = { + [`${collection}:create`]: (config) => { + console.log('config.data', config.data); + return [200, { msg: 'ok' }]; + }, +}; +const Root = createApp( + Demo, + { + components: { DemoForm }, + scopes: { useDemoFormProps }, + }, + mocks, +); + +export default Root; diff --git a/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/collection-form-get-and-update.tsx b/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/collection-form-get-and-update.tsx new file mode 100644 index 000000000..7ed2daa87 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/collection-form-get-and-update.tsx @@ -0,0 +1,158 @@ +import React, { FC, useEffect } from 'react'; +import { Button, Form, FormProps, Input, InputNumber, Select, notification } from 'antd'; +import { + SchemaComponent, + UseDataBlockProps, + useDataBlockResource, + useCollectionRecordData, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { ISchema } from '@formily/json-schema'; +import useUrlState from '@ahooksjs/use-url-state'; + +import { createApp } from './createApp'; + +interface DemoFormFieldType { + id: number; + username: string; + age: number; +} +type DemoFormProps = FormProps; +const DemoForm: FC = withDynamicSchemaProps((props) => { + return ( +
+ + label="Username" + name="username" + rules={[{ required: true, message: 'Please input your username!' }]} + > + + + + label="Age" + name="age" + rules={[{ required: true, message: 'Please input your age!' }]} + > + + + + + + + ); +}); + +function useDemoFormProps(): DemoFormProps { + const data = useCollectionRecordData(); + const resource = useDataBlockResource(); + + const [form] = Form.useForm(); + + useEffect(() => { + form.setFieldsValue(data); + }, [data, form]); + + const onFinish = async (values: DemoFormFieldType) => { + console.log('values', values); + await resource.update({ + filterByTk: data.id, + values, + }); + notification.success({ + message: 'Save successfully!', + }); + }; + + return { + initialValues: data, + preserve: true, + onFinish, + form, + }; +} +const useBlockDecoratorProps: UseDataBlockProps<'CollectionGet'> = () => { + const [state] = useUrlState({ id: '1' }); + return { + filterByTk: state.id, + }; +}; + +const collection = 'users'; +const action = 'get'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-use-decorator-props': 'useBlockDecoratorProps', + 'x-decorator-props': { + collection: collection, + action: action, + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'object', + 'x-component': 'DemoForm', + 'x-use-component-props': 'useDemoFormProps', + }, + }, +}; + +const Demo = () => { + const [state, setState] = useUrlState({ id: '1' }); + + return ( + <> + + + + ); +}; + +const mocks = { + [`${collection}:${action}`]: function (config) { + const { filterByTk } = config.params; + return { + data: + Number(filterByTk) === 1 + ? { + id: 1, + username: 'Bamboo', + age: 18, + } + : { + id: 2, + username: 'Mary', + age: 25, + }, + }; + }, + [`${collection}:update`]: function (config) { + console.log('config.data', config.data); + return { + data: 'ok', + }; + }, +}; +const Root = createApp( + Demo, + { + components: { DemoForm }, + scopes: { useDemoFormProps, useBlockDecoratorProps }, + }, + mocks, +); + +export default Root; diff --git a/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/collection-form-record-and-update.tsx b/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/collection-form-record-and-update.tsx new file mode 100644 index 000000000..423cc88d3 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/collection-form-record-and-update.tsx @@ -0,0 +1,136 @@ +import React, { FC, useEffect } from 'react'; +import { Button, Form, FormProps, Input, InputNumber, notification } from 'antd'; +import { + CollectionRecordProvider, + SchemaComponent, + UseDataBlockProps, + useDataBlockResource, + useCollectionRecordData, + withDynamicSchemaProps, +} from '@nocobase/client'; +import { ISchema } from '@formily/json-schema'; +import { createApp } from './createApp'; + +interface DemoFormFieldType { + id: number; + username: string; + age: number; +} +type DemoFormProps = FormProps; +const DemoForm: FC = withDynamicSchemaProps((props) => { + return ( +
+ + label="Username" + name="username" + rules={[{ required: true, message: 'Please input your username!' }]} + > + + + + label="Age" + name="age" + rules={[{ required: true, message: 'Please input your age!' }]} + > + + + + + + + ); +}); + +function useDemoFormProps(): DemoFormProps { + const data = useCollectionRecordData(); + const resource = useDataBlockResource(); + + const [form] = Form.useForm(); + + useEffect(() => { + form.setFieldsValue(data); + }, [data, form]); + + const onFinish = async (values: DemoFormFieldType) => { + console.log('values', values); + await resource.update({ + filterByTk: data.id, + values, + }); + notification.success({ + message: 'Save successfully!', + }); + }; + + return { + initialValues: data, + preserve: true, + onFinish, + form, + }; +} +const useFormBlockDecoratorProps: UseDataBlockProps<'CollectionRecord'> = () => { + const record = useCollectionRecordData(); + return { + record, + }; +}; + +const collection = 'users'; +const action = 'get'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-use-decorator-props': 'useFormBlockDecoratorProps', + 'x-decorator-props': { + collection: collection, + action: action, + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'object', + 'x-component': 'DemoForm', + 'x-use-component-props': 'useDemoFormProps', + }, + }, +}; + +const recordData = { + id: 1, + username: 'Bamboo', + age: 18, +}; + +const Demo = () => { + return ( + <> + + + + + ); +}; + +const mocks = { + [`${collection}:update`]: function (config) { + console.log('config.data', config.data); + return { + data: 'ok', + }; + }, +}; +const Root = createApp( + Demo, + { + components: { DemoForm }, + scopes: { useDemoFormProps, useFormBlockDecoratorProps }, + }, + mocks, +); + +export default Root; diff --git a/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/collection-table-list.tsx b/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/collection-table-list.tsx new file mode 100644 index 000000000..0e0240bab --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/collection-table-list.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import { SchemaComponent, useDataBlockRequest, withDynamicSchemaProps } from '@nocobase/client'; +import { Table, TableProps } from 'antd'; +import { ISchema } from '@formily/json-schema'; +import { createApp } from './createApp'; + +const schema: ISchema = { + type: 'void', + name: 'root', + 'x-decorator': 'DataBlockProvider', + 'x-decorator-props': { + collection: 'users', + action: 'list', + }, + 'x-component': 'CardItem', + properties: { + demo: { + type: 'array', + 'x-component': 'MyTable', + 'x-use-component-props': 'useTableProps', // 动态 table 属性 + }, + }, +}; + +const MyTable = withDynamicSchemaProps(Table); + +function useTableProps(): TableProps { + const { data, loading } = useDataBlockRequest(); + return { + loading, + dataSource: data?.data || [], + columns: [ + { + title: 'UserName', + dataIndex: 'username', + }, + { + title: 'NickName', + dataIndex: 'nickname', + }, + { + title: 'Email', + dataIndex: 'email', + }, + ], + }; +} + +const Demo = () => { + return ; +}; + +const mocks = { + 'users:list': { + data: [ + { + id: '1', + username: 'jack', + nickname: 'Jack Ma', + email: 'test@gmail.com', + }, + { + id: '2', + username: 'jim', + nickname: 'Jim Green', + }, + { + id: '3', + username: 'tom', + nickname: 'Tom Cat', + email: 'tom@gmail.com', + }, + ], + }, + 'roles:list': { + data: [ + { + name: 'root', + title: 'Root', + description: 'Root', + }, + { + name: 'admin', + title: 'Admin', + description: 'Admin description', + }, + ], + }, +}; + +const App = createApp( + Demo, + { + components: { MyTable }, + scopes: { useTableProps }, + }, + mocks, +); + +export default App; diff --git a/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/collections.json b/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/collections.json new file mode 100644 index 000000000..4d3aa45f8 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/collections.json @@ -0,0 +1,198 @@ +[ + { + "key": "h7b9i8khc3q", + "name": "users", + "inherit": false, + "hidden": false, + "description": null, + "category": [], + "namespace": "users.users", + "duplicator": { + "dumpable": "optional", + "with": "rolesUsers" + }, + "sortable": "sort", + "model": "UserModel", + "createdBy": true, + "updatedBy": true, + "logging": true, + "from": "db2cm", + "title": "{{t(\"Users\")}}", + "rawTitle": "{{t(\"Users\")}}", + "fields": [ + { + "uiSchema": { + "type": "number", + "title": "{{t(\"ID\")}}", + "x-component": "InputNumber", + "x-read-pretty": true, + "rawTitle": "{{t(\"ID\")}}" + }, + "key": "ffp1f2sula0", + "name": "id", + "type": "bigInt", + "interface": "id", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "autoIncrement": true, + "primaryKey": true, + "allowNull": false + }, + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Nickname\")}}", + "x-component": "Input", + "rawTitle": "{{t(\"Nickname\")}}" + }, + "key": "vrv7yjue90g", + "name": "nickname", + "type": "string", + "interface": "input", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null + }, + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Username\")}}", + "x-component": "Input", + "x-validator": { + "username": true + }, + "required": true, + "rawTitle": "{{t(\"Username\")}}" + }, + "key": "2ccs6evyrub", + "name": "username", + "type": "string", + "interface": "input", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "unique": true + }, + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Email\")}}", + "x-component": "Input", + "x-validator": "email", + "required": true, + "rawTitle": "{{t(\"Email\")}}" + }, + "key": "rrskwjl5wt1", + "name": "email", + "type": "string", + "interface": "email", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "unique": true + }, + { + "key": "t09bauwm0wb", + "name": "roles", + "type": "belongsToMany", + "interface": "m2m", + "description": null, + "collectionName": "users", + "parentKey": null, + "reverseKey": null, + "target": "roles", + "foreignKey": "userId", + "otherKey": "roleName", + "onDelete": "CASCADE", + "sourceKey": "id", + "targetKey": "name", + "through": "rolesUsers", + "uiSchema": { + "type": "array", + "title": "{{t(\"Roles\")}}", + "x-component": "AssociationField", + "x-component-props": { + "multiple": true, + "fieldNames": { + "label": "title", + "value": "name" + } + } + } + } + ] + }, + { + "key": "pqnenvqrzxr", + "name": "roles", + "inherit": false, + "hidden": false, + "description": null, + "category": [], + "namespace": "acl.acl", + "duplicator": { + "dumpable": "required", + "with": "uiSchemas" + }, + "autoGenId": false, + "model": "RoleModel", + "filterTargetKey": "name", + "sortable": true, + "from": "db2cm", + "title": "{{t(\"Roles\")}}", + "rawTitle": "{{t(\"Roles\")}}", + "fields": [ + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Role UID\")}}", + "x-component": "Input", + "rawTitle": "{{t(\"Role UID\")}}" + }, + "key": "jbz9m80bxmp", + "name": "name", + "type": "uid", + "interface": "input", + "description": null, + "collectionName": "roles", + "parentKey": null, + "reverseKey": null, + "prefix": "r_", + "primaryKey": true + }, + { + "uiSchema": { + "type": "string", + "title": "{{t(\"Role name\")}}", + "x-component": "Input", + "rawTitle": "{{t(\"Role name\")}}" + }, + "key": "faywtz4sf3u", + "name": "title", + "type": "string", + "interface": "input", + "description": null, + "collectionName": "roles", + "parentKey": null, + "reverseKey": null, + "unique": true, + "translation": true + }, + { + "key": "1enkovm9sye", + "name": "description", + "type": "string", + "interface": null, + "description": null, + "collectionName": "roles", + "parentKey": null, + "reverseKey": null + } + ] + } +] diff --git a/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/createApp.tsx b/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/createApp.tsx new file mode 100644 index 000000000..5d5cb91c9 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/data-block/data-block-demos/createApp.tsx @@ -0,0 +1,56 @@ +import { + ApplicationOptions, + Application, + CardItem, + Plugin, + CollectionPlugin, + DataBlockProvider, + LocalDataSource, + DEFAULT_DATA_SOURCE_KEY, + DEFAULT_DATA_SOURCE_TITLE, +} from '@nocobase/client'; +import MockAdapter from 'axios-mock-adapter'; +import { ComponentType } from 'react'; +import collections from '../../collections.json'; + +export function createApp(Demo: ComponentType, options: ApplicationOptions = {}, mocks: Record) { + class MyPlugin extends Plugin { + async load() { + this.app.dataSourceManager.addDataSource(LocalDataSource, { + key: DEFAULT_DATA_SOURCE_KEY, + displayName: DEFAULT_DATA_SOURCE_TITLE, + collections: collections as any, + }); + } + } + const app = new Application({ + apiClient: { + baseURL: 'http://localhost:8000', + }, + providers: [Demo], + ...options, + components: { + ...options.components, + DataBlockProvider, + CardItem, + }, + plugins: [CollectionPlugin, MyPlugin], + designable: true, + }); + + const mock = new MockAdapter(app.apiClient.axios); + + Object.entries(mocks).forEach(([url, data]) => { + mock.onGet(url).reply(async (config) => { + const res = typeof data === 'function' ? data(config) : data; + return [200, res]; + }); + mock.onPost(url).reply(async (config) => { + const res = typeof data === 'function' ? data(config) : data; + return [200, res]; + }); + }); + + const Root = app.getRootComponent(); + return Root; +} diff --git a/packages/core/client/src/data-source/__tests__/data-source/CollectionDataSourceProvider.test.tsx b/packages/core/client/src/data-source/__tests__/data-source/CollectionDataSourceProvider.test.tsx new file mode 100644 index 000000000..dc35b9769 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/data-source/CollectionDataSourceProvider.test.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { render, screen } from '@nocobase/test/client'; + +import { Application } from '@nocobase/client'; +import { DataSourceApplicationProvider } from '../../components/DataSourceApplicationProvider'; +import { DataSourceProvider, useDataSourceKey } from '../../data-source/DataSourceProvider'; + +describe('CollectionDataSourceProvider', () => { + test('should work', () => { + const app = new Application({ + dataSourceManager: { + dataSources: [ + { + key: 'a', + displayName: 'a', + }, + ], + }, + }); + const Demo = () => { + const name = useDataSourceKey(); + return
{name}
; + }; + + render( + + + + + , + ); + + expect(screen.getByTestId('content')).toHaveTextContent('a'); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/data-source/DataSource.test.ts b/packages/core/client/src/data-source/__tests__/data-source/DataSource.test.ts new file mode 100644 index 000000000..8770dce2c --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/data-source/DataSource.test.ts @@ -0,0 +1,78 @@ +import { Application, DataSourceOptions, DataSource } from '@nocobase/client'; + +describe('DataSource', () => { + let dataSource: DataSource; + let app: Application; + const collections = [{ name: 'a' }]; + const newDisplayName = 'new display name'; + const dataSourceOptions: DataSourceOptions = { + key: 'test', + displayName: 'Test Data Source', + status: 'loading', + }; + + beforeEach(() => { + app = new Application({}); + class TestDataSource extends DataSource { + async getDataSource() { + return { + displayName: newDisplayName, + collections: collections, + }; + } + } + app.dataSourceManager.addDataSource(TestDataSource, dataSourceOptions); + dataSource = app.dataSourceManager.getDataSource('test'); + }); + + it('initializes with the correct options', () => { + expect(dataSource.getOptions()).toEqual(dataSourceOptions); + expect(dataSource.app).toBe(app); + expect(dataSource.key).toBe(dataSourceOptions.key); + expect(dataSource.displayName).toBe(dataSourceOptions.displayName); + expect(dataSource.key).toBe(dataSourceOptions.key); + expect(dataSource.status).toBe(dataSourceOptions.status); + expect(dataSource.errorMessage).toBe(dataSourceOptions.errorMessage); + expect(dataSource.collections).toEqual([]); + }); + + it('getOption() returns the correct value', () => { + expect(dataSource.getOption('displayName')).toBe(dataSourceOptions.displayName); + }); + + it('sets new options correctly', () => { + dataSource.setOptions({ displayName: 'New Display Name' }); + expect(dataSource.displayName).toBe('New Display Name'); + }); + + it('can add and call reload callbacks', async () => { + const callback = vitest.fn(); + dataSource.addReloadCallback(callback); + await dataSource.reload(); + expect(callback).toHaveBeenCalled(); + }); + + it('does not add the same reload callback twice', () => { + const callback = vitest.fn(); + dataSource.addReloadCallback(callback); + dataSource.addReloadCallback(callback); + expect(dataSource['reloadCallbacks'].length).toBe(1); + }); + + it('can remove reload callbacks', () => { + const callback = vitest.fn(); + dataSource.addReloadCallback(callback); + dataSource.removeReloadCallback(callback); + expect(dataSource['reloadCallbacks'].length).toBe(0); + }); + + it('updates collections and calls callbacks on reload', async () => { + const callback = vitest.fn(); + dataSource.addReloadCallback(callback); + await dataSource.reload(); + expect(dataSource.collections.length).toBe(collections.length); + expect(dataSource.displayName).toBe(newDisplayName); + expect(callback).toHaveBeenCalledWith(collections); + expect(dataSource.collectionManager.getCollection('a')).toBeTruthy(); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/data-source/DataSourceManager.test.ts b/packages/core/client/src/data-source/__tests__/data-source/DataSourceManager.test.ts new file mode 100644 index 000000000..72aaeace1 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/data-source/DataSourceManager.test.ts @@ -0,0 +1,188 @@ +import { Application, Collection, DEFAULT_DATA_SOURCE_KEY, LocalDataSource, Plugin } from '@nocobase/client'; +import collections from '../collections.json'; + +describe('DataSourceManager', () => { + describe('mixins', () => { + test('init should work', () => { + class DemoCollectionMixin extends Collection { + a() { + return 'test-' + this.name; + } + } + + const app = new Application({ + dataSourceManager: { + collections: collections as any, + collectionMixins: [DemoCollectionMixin], + }, + }); + + const user = app.getCollectionManager().getCollection('users'); + expect(user.a()).toBe('test-users'); + }); + + test('plugin should work', async () => { + class DemoCollectionMixin extends Collection { + b() { + return 'test-' + this.name; + } + } + + class MyPlugin extends Plugin { + async load() { + this.app.getCollectionManager().addCollections(collections as any); + this.app.dataSourceManager.addCollectionMixins([DemoCollectionMixin]); + } + } + + const app = new Application({ + plugins: [MyPlugin], + }); + + await app.load(); + + const user = app.getCollectionManager().getCollection('users'); + expect(user.b()).toBe('test-users'); + }); + + test('multiple mixins should work', () => { + class DemoCollectionMixin1 extends Collection { + c() { + return 'test1-' + this.name; + } + } + + class DemoCollectionMixin2 extends Collection { + d() { + return 'test2-' + this.name; + } + } + + const app = new Application({ + dataSourceManager: { + collections: collections as any, + collectionMixins: [DemoCollectionMixin1, DemoCollectionMixin2], + }, + }); + + const user = app.getCollectionManager().getCollection('users'); + expect(user.c()).toBe('test1-users'); + expect(user.d()).toBe('test2-users'); + }); + + test('after add mixins, collection should be re-instantiated', () => { + class DemoCollectionMixin extends Collection { + e() { + return 'test-' + this.name; + } + } + + const app = new Application({ + dataSourceManager: { + collections: collections as any, + }, + }); + + const user = app.getCollectionManager().getCollection('users'); + expect(user.e).toBeUndefined(); + + app.dataSourceManager.addCollectionMixins([DemoCollectionMixin]); + + const user2 = app.getCollectionManager().getCollection('users'); + expect(user2.e()).toBe('test-users'); + }); + }); + + describe('getAllCollections', () => { + test('getAllCollections() should work', () => { + const app = new Application({ + dataSourceManager: { + dataSources: [ + { + key: 'a', + displayName: 'a', + }, + ], + }, + }); + app.getCollectionManager().addCollections(collections as any); + app.getCollectionManager('a').addCollections([collections[0]] as any); + + const allCollections = app.dataSourceManager.getAllCollections(); + + expect(allCollections.length).toBe(2); + expect(allCollections[0].key).toBe(DEFAULT_DATA_SOURCE_KEY); + expect(allCollections[1].key).toBe('a'); + + expect(allCollections[0].collections.length).toBe(2); + expect(allCollections[1].collections.length).toBe(1); + }); + }); + + describe('addDataSource', () => { + test('should add a data source', () => { + const app = new Application(); + const dataSourceManager = app.dataSourceManager; + + expect(dataSourceManager.getDataSources()).toHaveLength(1); + + const dataSource = dataSourceManager.addDataSource(LocalDataSource, { + key: 'test', + displayName: 'Test', + }); + + expect(dataSourceManager.getDataSources()).toHaveLength(2); + expect(dataSourceManager.getDataSource('test')).toBe(dataSource); + }); + }); + + describe('removeDataSources', () => { + test('should remove data sources', () => { + const app = new Application(); + const dataSourceManager = app.dataSourceManager; + + dataSourceManager.addDataSource(LocalDataSource, { + key: 'test1', + displayName: 'Test 1', + }); + + dataSourceManager.addDataSource(LocalDataSource, { + key: 'test2', + displayName: 'Test 2', + }); + + expect(dataSourceManager.getDataSources()).toHaveLength(3); + + dataSourceManager.removeDataSources(['test1']); + + expect(dataSourceManager.getDataSources()).toHaveLength(2); + expect(dataSourceManager.getDataSource('test1')).toBeUndefined(); + expect(dataSourceManager.getDataSource('test2')).toBeDefined(); + }); + }); + + describe('reload', () => { + test('should reload data sources', async () => { + const app = new Application(); + const dataSourceManager = app.dataSourceManager; + + const dataSource1 = dataSourceManager.addDataSource(LocalDataSource, { + key: 'test1', + displayName: 'Test 1', + }); + + const dataSource2 = dataSourceManager.addDataSource(LocalDataSource, { + key: 'test2', + displayName: 'Test 2', + }); + + const reloadSpy1 = vitest.spyOn(dataSource1, 'reload'); + const reloadSpy2 = vitest.spyOn(dataSource2, 'reload'); + + await dataSourceManager.reload(); + + expect(reloadSpy1).toHaveBeenCalledTimes(1); + expect(reloadSpy2).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/data-source/DataSourceManagerProvider.test.tsx b/packages/core/client/src/data-source/__tests__/data-source/DataSourceManagerProvider.test.tsx new file mode 100644 index 000000000..2f5649add --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/data-source/DataSourceManagerProvider.test.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { DataSourceManagerProvider, useDataSourceManager } from '@nocobase/client'; + +describe('DataSourceManagerProvider', () => { + test('should render children', () => { + const dataSourceManager = {} as any; // Replace with your actual data source manager object + const { getByText } = render( + +
Test Children
+
, + ); + expect(getByText('Test Children')).toBeInTheDocument(); + }); +}); + +describe('useDataSourceManager', () => { + test('should return the data source manager', () => { + const dataSourceManager = {} as any; // Replace with your actual data source manager object + const TestComponent = () => { + const manager = useDataSourceManager(); + expect(manager).toBe(dataSourceManager); + return null; + }; + render( + + + , + ); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/data-source/DataSourceProvider.test.tsx b/packages/core/client/src/data-source/__tests__/data-source/DataSourceProvider.test.tsx new file mode 100644 index 000000000..8088514c3 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/data-source/DataSourceProvider.test.tsx @@ -0,0 +1,82 @@ +import { + Application, + DEFAULT_DATA_SOURCE_KEY, + DataSourceManagerProvider, + DataSourceOptions, + DataSource, + SchemaComponent, + SchemaComponentProvider, +} from '@nocobase/client'; +import { DataSourceProvider, useDataSourceKey } from '../../data-source/DataSourceProvider'; +import { render, screen } from '@nocobase/test/client'; +import React from 'react'; +import { AppSchemaComponentProvider } from '../../../application/AppSchemaComponentProvider'; + +describe('DataSourceProvider', () => { + function renderComponent(dataSource?: string, status?: DataSourceOptions['status']) { + const app = new Application({ + dataSourceManager: { + collections: [{ name: 'a' }], + }, + }); + + const Demo = () => { + const dataSourceKey = useDataSourceKey(); + + return
{dataSourceKey}
; + }; + const dataSourceOptions: DataSourceOptions = { + key: 'test', + displayName: 'Test', + status: status, + }; + class TestDataSource extends DataSource { + async getDataSource() { + return dataSourceOptions; + } + } + app.dataSourceManager.addDataSource(TestDataSource, dataSourceOptions); + + const schema = { + name: 'root', + type: 'void', + 'x-decorator': 'DataSourceProvider', + 'x-decorator-props': { + dataSource, + }, + 'x-component': 'Demo', + }; + + render( + + + + + , + ); + } + it('should render default dataSource', () => { + renderComponent(); + expect(screen.getByTestId('demo')).toHaveTextContent(DEFAULT_DATA_SOURCE_KEY); + }); + + it('should render children when data source is available', () => { + renderComponent('test'); + expect(screen.getByTestId('demo')).toHaveTextContent('test'); + }); + + it('should render error state when data source is not found', () => { + renderComponent('non-existent'); + expect(document.querySelector('.ant-result')).toBeInTheDocument(); + }); + + it('should render loading state when data source is loading', () => { + renderComponent('test', 'loading'); + expect(screen.getByText('Test data source loading...')).toBeInTheDocument(); + }); + + it('should render error state when data source loading fails', () => { + renderComponent('test', 'loading-failed'); + expect(screen.getByText('DataSource "Test" loading failed')).toBeInTheDocument(); + }); +}); diff --git a/packages/core/client/src/data-source/__tests__/utils.test.ts b/packages/core/client/src/data-source/__tests__/utils.test.ts new file mode 100644 index 000000000..d097966c2 --- /dev/null +++ b/packages/core/client/src/data-source/__tests__/utils.test.ts @@ -0,0 +1,45 @@ +import { CollectionFieldInterface, isTitleField, Application } from '@nocobase/client'; +import collections from './collections.json'; + +describe('utils', () => { + describe('isTitleField', () => { + class Demo1FieldInterface extends CollectionFieldInterface { + name = 'demo1'; + titleUsable = false; + } + class Demo2FieldInterface extends CollectionFieldInterface { + name = 'demo2'; + titleUsable = true; + } + + const dm = new Application({ + dataSourceManager: { + collections: collections as any, + fieldInterfaces: [Demo1FieldInterface, Demo2FieldInterface], + }, + }).dataSourceManager; + + it('should return false when field is foreign key', () => { + const field = { + isForeignKey: true, + }; + expect(isTitleField(dm, field)).toBeFalsy(); + }); + + it('should return false when field interface is not title usable', () => { + const field = { + isForeignKey: false, + interface: 'demo1', + }; + expect(isTitleField(dm, field)).toBeFalsy(); + }); + + it('should return true when field is not foreign key and field interface is title usable', () => { + const field = { + isForeignKey: false, + interface: 'demo2', + }; + expect(isTitleField(dm, field)).toBeTruthy(); + }); + }); +}); diff --git a/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts new file mode 100644 index 000000000..51e281204 --- /dev/null +++ b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts @@ -0,0 +1,47 @@ +import type { ISchema } from '@formily/react'; +import type { CollectionFieldOptions } from '../collection'; +import { CollectionFieldInterfaceManager } from './CollectionFieldInterfaceManager'; + +export type CollectionFieldInterfaceFactory = new ( + collectionFieldInterfaceManager: CollectionFieldInterfaceManager, +) => CollectionFieldInterface; + +export abstract class CollectionFieldInterface { + constructor(public collectionFieldInterfaceManager: CollectionFieldInterfaceManager) {} + name: string; + group: string; + title?: string; + description?: string; + order?: number; + default?: { + type: string; + uiSchema?: ISchema; + [key: string]: any; + }; + sortable?: boolean; + availableTypes?: string[]; + supportDataSourceType?: string[]; + notSupportDataSourceType?: string[]; + hasDefaultValue?: boolean; + isAssociation?: boolean; + operators?: any[]; + /** + * - 如果该值为空,则在 Filter 组件中该字段会被过滤掉 + * - 如果该值为空,则不会在变量列表中看到该字段 + */ + filterable?: { + /** + * 字段所支持的操作符,会在 Filter 组件中显示,比如设置 `数据范围` 的时候可以看见 + */ + operators?: any[]; + /** + * 为当前字段添加子选项,这个子选项会在 Filter 组件中显示,比如设置 `数据范围` 的时候可以看见 + */ + children?: any[]; + [key: string]: any; + }; + titleUsable?: boolean; + validateSchema?(fieldSchema: ISchema): Record; + usePathOptions?(field: CollectionFieldOptions): any; + schemaInitialize?(schema: ISchema, data: any): void; +} diff --git a/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterfaceManager.ts b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterfaceManager.ts new file mode 100644 index 000000000..10089c449 --- /dev/null +++ b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterfaceManager.ts @@ -0,0 +1,53 @@ +import type { DataSourceManager } from '../data-source'; +import type { CollectionFieldInterface, CollectionFieldInterfaceFactory } from './CollectionFieldInterface'; + +export class CollectionFieldInterfaceManager { + protected collectionFieldInterfaceInstances: Record = {}; + protected collectionFieldGroups: Record = {}; + + constructor( + fieldInterfaceClasses: CollectionFieldInterfaceFactory[], + fieldInterfaceGroups: Record, + public dataSourceManager: DataSourceManager, + ) { + this.addFieldInterfaces(fieldInterfaceClasses); + this.addFieldInterfaceGroups(fieldInterfaceGroups); + } + + addFieldInterfaces(fieldInterfaceClasses: CollectionFieldInterfaceFactory[] = []) { + const newCollectionFieldInterfaces = fieldInterfaceClasses.reduce((acc, Interface) => { + const instance = new Interface(this); + acc[instance.name] = instance; + return acc; + }, {}); + + Object.assign(this.collectionFieldInterfaceInstances, newCollectionFieldInterfaces); + } + getFieldInterface(name: string) { + return this.collectionFieldInterfaceInstances[name] as T; + } + getFieldInterfaces(dataSourceType?: string) { + return Object.values(this.collectionFieldInterfaceInstances).filter((item) => { + if (!dataSourceType) return true; + if (!item.supportDataSourceType && !item.notSupportDataSourceType) return true; + + if (item.supportDataSourceType) { + return item.supportDataSourceType?.includes(dataSourceType); + } + if (item.notSupportDataSourceType) { + return !item.notSupportDataSourceType?.includes(dataSourceType); + } + return true; + }); + } + + addFieldInterfaceGroups(groups: Record) { + Object.assign(this.collectionFieldGroups, groups); + } + getFieldInterfaceGroups() { + return this.collectionFieldGroups; + } + getFieldInterfaceGroup(name: string) { + return this.collectionFieldGroups[name]; + } +} diff --git a/packages/core/client/src/data-source/collection-field-interface/index.ts b/packages/core/client/src/data-source/collection-field-interface/index.ts new file mode 100644 index 000000000..ab0254e7a --- /dev/null +++ b/packages/core/client/src/data-source/collection-field-interface/index.ts @@ -0,0 +1,2 @@ +export * from './CollectionFieldInterface'; +export * from './CollectionFieldInterfaceManager'; diff --git a/packages/core/client/src/collection-manager/CollectionField.tsx b/packages/core/client/src/data-source/collection-field/CollectionField.tsx similarity index 58% rename from packages/core/client/src/collection-manager/CollectionField.tsx rename to packages/core/client/src/data-source/collection-field/CollectionField.tsx index 16108e70f..2896d3563 100644 --- a/packages/core/client/src/collection-manager/CollectionField.tsx +++ b/packages/core/client/src/data-source/collection-field/CollectionField.tsx @@ -2,12 +2,11 @@ import { Field } from '@formily/core'; import { connect, useField, useFieldSchema } from '@formily/react'; import { merge } from '@formily/shared'; import { concat } from 'lodash'; -import React, { useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useActionContext, useCompile, useComponent, useFormBlockContext, useRecord } from '..'; -import useIsAllowToSetDefaultValue from '../schema-settings/hooks/useIsAllowToSetDefaultValue'; -import { CollectionFieldProvider } from './CollectionFieldProvider'; -import { useCollectionField } from './hooks'; +import React, { useEffect, useMemo } from 'react'; +import { useFormBlockContext } from '../../block-provider/FormBlockProvider'; +import { useCompile, useComponent } from '../../schema-component'; +import { useIsAllowToSetDefaultValue } from '../../schema-settings/hooks/useIsAllowToSetDefaultValue'; +import { CollectionFieldProvider, useCollectionField } from './CollectionFieldProvider'; type Props = { component: any; @@ -15,14 +14,15 @@ type Props = { }; // TODO: 初步适配 -const InternalField: React.FC = (props: Props) => { +export const CollectionFieldInternalField: React.FC = (props: Props) => { const { component } = props; + const compile = useCompile(); const field = useField(); const fieldSchema = useFieldSchema(); - const { uiSchema, defaultValue } = useCollectionField(); + const { uiSchema: uiSchemaOrigin, defaultValue } = useCollectionField(); const { isAllowToSetDefaultValue } = useIsAllowToSetDefaultValue(); + const uiSchema = useMemo(() => compile(uiSchemaOrigin), [JSON.stringify(uiSchemaOrigin)]); const Component = useComponent(component || uiSchema?.['x-component'] || 'Input'); - const compile = useCompile(); const setFieldProps = (key, value) => { field[key] = typeof field[key] === 'undefined' ? value : field[key]; }; @@ -51,6 +51,7 @@ const InternalField: React.FC = (props: Props) => { const defaultVal = isAllowToSetDefaultValue() ? fieldSchema.default || defaultValue : undefined; defaultVal !== null && defaultVal !== undefined && setFieldProps('initialValue', defaultVal); } + if (!field.validator && (uiSchema['x-validator'] || fieldSchema['x-validator'])) { const concatSchema = concat([], uiSchema['x-validator'] || [], fieldSchema['x-validator'] || []); field.validator = concatSchema; @@ -74,51 +75,13 @@ const InternalField: React.FC = (props: Props) => { return ; }; -export const InternalFallbackField = () => { - const { uiSchema } = useCollectionField(); - const field = useField(); - const fieldSchema = useFieldSchema(); - const record = useRecord(); - - const displayKey = fieldSchema['x-component-props']?.fieldNames?.label ?? 'id'; - - const value = record[fieldSchema.name]; - - useEffect(() => { - field.title = fieldSchema.title ?? fieldSchema.name; - }, [uiSchema?.title]); - - let displayText = value; - - if (Array.isArray(value) || typeof value === 'object') { - displayText = [] - .concat(value) - .map((i) => i[displayKey]) - .join(', '); - } - - return
{displayText}
; -}; - -// 当字段被删除时,显示一个提示占位符 -const DeletedField = () => { - const { t } = useTranslation(); - return
{t('The field has been deleted')}
; -}; - export const CollectionField = connect((props) => { const fieldSchema = useFieldSchema(); - const field = fieldSchema?.['x-component-props']?.['field']; - const { snapshot } = useActionContext(); return ( - : } - > - + + ); }); -export default CollectionField; +CollectionField.displayName = 'CollectionField'; diff --git a/packages/core/client/src/data-source/collection-field/CollectionFieldProvider.tsx b/packages/core/client/src/data-source/collection-field/CollectionFieldProvider.tsx new file mode 100644 index 000000000..22724c1db --- /dev/null +++ b/packages/core/client/src/data-source/collection-field/CollectionFieldProvider.tsx @@ -0,0 +1,52 @@ +import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react'; + +import { useFieldSchema, type SchemaKey } from '@formily/react'; +import type { CollectionFieldOptions } from '../collection'; + +import { useCollection, useCollectionManager } from '../collection'; +import { CollectionDeletedPlaceholder } from '../components/CollectionDeletedPlaceholder'; + +export const CollectionFieldContext = createContext(null); +CollectionFieldContext.displayName = 'CollectionFieldContext'; + +export type CollectionFieldProviderProps = { + name?: SchemaKey; + children?: ReactNode; + allowNull?: boolean; +}; + +export const CollectionFieldProvider: FC = (props) => { + const { name, children, allowNull } = props; + const fieldSchema = useFieldSchema(); + const collection = useCollection(); + const collectionManager = useCollectionManager(); + + const value = useMemo(() => { + if (!collection || allowNull) return null; + const field = fieldSchema?.['x-component-props']?.['field']; + return ( + collectionManager.getCollectionField(fieldSchema?.['x-collection-field']) || + field || + collection.getField(field?.name || name) + ); + }, [collection, fieldSchema, name, collectionManager]); + + if (!collection || allowNull) { + return <>{children}; + } + + if (!value) { + return ; + } + + return {children}; +}; + +export const useCollectionField = () => { + const context = useContext(CollectionFieldContext); + // if (!context) { + // throw new Error('useCollectionField() must be used within a CollectionFieldProvider'); + // } + + return context; +}; diff --git a/packages/core/client/src/data-source/collection-field/index.ts b/packages/core/client/src/data-source/collection-field/index.ts new file mode 100644 index 000000000..b0f92e02a --- /dev/null +++ b/packages/core/client/src/data-source/collection-field/index.ts @@ -0,0 +1,2 @@ +export * from './CollectionField'; +export * from './CollectionFieldProvider'; diff --git a/packages/core/client/src/data-source/collection-record/CollectionRecord.ts b/packages/core/client/src/data-source/collection-record/CollectionRecord.ts new file mode 100644 index 000000000..05a13c0f9 --- /dev/null +++ b/packages/core/client/src/data-source/collection-record/CollectionRecord.ts @@ -0,0 +1,25 @@ +export interface CollectionRecordOptions { + isNew?: boolean; + data?: DataType; + parentRecord?: CollectionRecord; +} + +export class CollectionRecord { + public data?: DataType; + public parentRecord?: CollectionRecord; + public isNew?: boolean; + constructor(options: CollectionRecordOptions) { + const { data, parentRecord, isNew } = options; + this.isNew = isNew; + this.data = data; + this.parentRecord = parentRecord; + } + + setData(data: DataType) { + this.data = data; + } + + setParentRecord(parentRecord: CollectionRecord) { + this.parentRecord = parentRecord; + } +} diff --git a/packages/core/client/src/data-source/collection-record/CollectionRecordProvider.tsx b/packages/core/client/src/data-source/collection-record/CollectionRecordProvider.tsx new file mode 100644 index 000000000..d2090d650 --- /dev/null +++ b/packages/core/client/src/data-source/collection-record/CollectionRecordProvider.tsx @@ -0,0 +1,65 @@ +import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react'; +import { CollectionRecord } from './CollectionRecord'; + +export const CollectionRecordContext = createContext>(null); +CollectionRecordContext.displayName = 'CollectionRecordContext'; + +export interface CollectionRecordProviderProps { + children?: ReactNode; + isNew?: boolean; + record?: CollectionRecord | DataType; + parentRecord?: CollectionRecord | DataType; +} + +export const CollectionRecordProvider: FC = ({ + isNew, + record, + parentRecord, + children, +}) => { + const parentRecordValue = useMemo(() => { + if (parentRecord) { + if (parentRecord instanceof CollectionRecord) return parentRecord; + return new CollectionRecord({ data: parentRecord }); + } + if (record instanceof CollectionRecord) return record.parentRecord; + }, [parentRecord, record]); + + const currentRecordValue = useMemo(() => { + let res: CollectionRecord; + if (record) { + if (record instanceof CollectionRecord) { + res = record; + res.isNew = record.isNew || isNew; + } else { + res = new CollectionRecord({ data: record, isNew }); + } + } else { + res = new CollectionRecord({ isNew }); + } + res.setParentRecord(parentRecordValue); + return res; + }, [record, parentRecordValue, isNew]); + + return {children}; +}; + +export function useCollectionRecord(): CollectionRecord { + const context = useContext>(CollectionRecordContext); + return context; +} + +export function useCollectionRecordData(): DataType { + const record = useCollectionRecord(); + return record?.data; +} + +export function useCollectionParentRecord(): CollectionRecord { + const record = useCollectionRecord(); + return record?.parentRecord; +} + +export function useCollectionParentRecordData(): ParentDataType { + const record = useCollectionRecord(); + return record?.parentRecord?.data; +} diff --git a/packages/core/client/src/data-source/collection-record/index.ts b/packages/core/client/src/data-source/collection-record/index.ts new file mode 100644 index 000000000..09eb42e65 --- /dev/null +++ b/packages/core/client/src/data-source/collection-record/index.ts @@ -0,0 +1,2 @@ +export * from './CollectionRecord'; +export * from './CollectionRecordProvider'; diff --git a/packages/core/client/src/data-source/collection-record/isNewRecord.ts b/packages/core/client/src/data-source/collection-record/isNewRecord.ts new file mode 100644 index 000000000..cc9517c27 --- /dev/null +++ b/packages/core/client/src/data-source/collection-record/isNewRecord.ts @@ -0,0 +1,25 @@ +import { isObject } from 'lodash'; + +// 使用 Symbol 作为 key,避免与其他属性冲突,同时也可以防止通过表单提交到后端 +const key = Symbol('isNewRecord'); + +/** + * 判断一个记录对象是否是新记录,可通过 markRecordAsNew 标记 + * @param record + * @returns + */ +export const isNewRecord = (record: object) => { + return !!record?.[key]; +}; + +/** + * 将一个记录对象标记为新记录,可通过 isNewRecord 判断 + * @param record + * @returns + */ +export const markRecordAsNew = (record: object) => { + if (!isObject(record)) throw new Error('markRecordAsNew: record must be an object'); + + record[key] = true; + return record; +}; diff --git a/packages/core/client/src/data-source/collection-template/CollectionTemplate.ts b/packages/core/client/src/data-source/collection-template/CollectionTemplate.ts new file mode 100644 index 000000000..af6bfc823 --- /dev/null +++ b/packages/core/client/src/data-source/collection-template/CollectionTemplate.ts @@ -0,0 +1,72 @@ +import type { CollectionOptions, Collection } from '../collection'; +import type { ISchema } from '@formily/react'; +import type { CollectionTemplateManager } from './CollectionTemplateManager'; +import type { Application } from '../../application/Application'; + +interface AvailableFieldInterfacesInclude { + include?: any[]; +} + +interface AvailableFieldInterfacesExclude { + exclude?: any[]; +} + +interface CollectionTemplateDefaultOptions { + /** + * 自动生成 id + * @default true + * */ + autoGenId?: boolean; + /** 创建人 */ + createdBy?: boolean; + /** 最后更新人 */ + updatedBy?: boolean; + /** 创建日期 */ + createdAt?: boolean; + /** 更新日期 */ + updatedAt?: boolean; + /** 可排序 */ + sortable?: boolean; + /* 树结构 */ + tree?: string; + /* 日志 */ + logging?: boolean; + /** 继承 */ + inherits?: string | string[]; + /* 字段列表 */ + fields?: CollectionOptions['fields']; + [key: string]: any; +} + +export type CollectionTemplateFactory = new ( + collectionTemplateManager: CollectionTemplateManager, +) => CollectionTemplate; +export abstract class CollectionTemplate { + constructor(public collectionTemplateManager: CollectionTemplateManager) {} + name: string; + Collection?: typeof Collection; + title?: string; + color?: string; + /** 排序 */ + order?: number; + /** 默认配置 */ + default?: CollectionTemplateDefaultOptions; + events?: any; + /** UI 可配置的 CollectionOptions 参数(添加或编辑的 Collection 表单的字段) */ + configurableProperties?: Record; + /** 当前模板可用的字段类型 */ + availableFieldInterfaces?: AvailableFieldInterfacesInclude | AvailableFieldInterfacesExclude; + /** 是否分割线 */ + divider?: boolean; + /** 模板描述 */ + description?: string; + /**配置字段中的操作按钮 */ + configureActions?: Record; + //是否禁止删除字段 + forbidDeletion?: boolean; + + supportDataSourceType?: string[]; + notSupportDataSourceType?: string[]; + + transform?(collection: CollectionOptions, app: Application): CollectionOptions; +} diff --git a/packages/core/client/src/data-source/collection-template/CollectionTemplateManager.ts b/packages/core/client/src/data-source/collection-template/CollectionTemplateManager.ts new file mode 100644 index 000000000..7a52ece17 --- /dev/null +++ b/packages/core/client/src/data-source/collection-template/CollectionTemplateManager.ts @@ -0,0 +1,53 @@ +import type { DataSourceManager } from '../data-source'; +import type { CollectionTemplate, CollectionTemplateFactory } from './CollectionTemplate'; + +export class CollectionTemplateManager { + protected collectionTemplateInstances: Record = {}; + + constructor( + templateClasses: CollectionTemplateFactory[], + public dataSourceManager: DataSourceManager, + ) { + this.addCollectionTemplates(templateClasses); + } + + addCollectionTemplates(templateClasses: CollectionTemplateFactory[] = []) { + const newCollectionTemplateInstances = templateClasses.reduce((acc, Template) => { + const instance = new Template(this); + acc[instance.name] = instance; + return acc; + }, {}); + Object.assign(this.collectionTemplateInstances, newCollectionTemplateInstances); + + // Re-add collections + this.dataSourceManager.getDataSources().forEach((dataSource) => { + const reAddCollections = dataSource.collectionManager.getCollections((collection) => { + return newCollectionTemplateInstances[collection.template]; + }); + dataSource.collectionManager.reAddCollections(reAddCollections); + }); + + return newCollectionTemplateInstances; + } + + getCollectionTemplate(name: string): T { + return this.collectionTemplateInstances[name] as T; + } + + getCollectionTemplates(dataSourceType?: string) { + return Object.values(this.collectionTemplateInstances) + .filter((item) => { + if (!dataSourceType) return true; + if (!item.supportDataSourceType && !item.notSupportDataSourceType) return true; + + if (item.supportDataSourceType) { + return item.supportDataSourceType?.includes(dataSourceType); + } + if (item.notSupportDataSourceType) { + return !item.notSupportDataSourceType?.includes(dataSourceType); + } + return true; + }) + .sort((a, b) => (a.order || 0) - (b.order || 0)); + } +} diff --git a/packages/core/client/src/data-source/collection-template/index.ts b/packages/core/client/src/data-source/collection-template/index.ts new file mode 100644 index 000000000..cd3c81c61 --- /dev/null +++ b/packages/core/client/src/data-source/collection-template/index.ts @@ -0,0 +1,2 @@ +export * from './CollectionTemplate'; +export * from './CollectionTemplateManager'; diff --git a/packages/core/client/src/data-source/collection/AssociationProvider.tsx b/packages/core/client/src/data-source/collection/AssociationProvider.tsx new file mode 100644 index 000000000..ec9acf606 --- /dev/null +++ b/packages/core/client/src/data-source/collection/AssociationProvider.tsx @@ -0,0 +1,42 @@ +import React, { FC, ReactNode, createContext, useContext } from 'react'; +import { CollectionFieldProvider } from '../collection-field'; +import { CollectionDeletedPlaceholder } from '../components/CollectionDeletedPlaceholder'; +import { Collection } from './Collection'; +import { useCollectionManager } from './CollectionManagerProvider'; +import { CollectionProvider, useCollection } from './CollectionProvider'; + +export interface AssociationProviderProps { + dataSource?: string; + name: string; + children?: ReactNode; +} + +const ParentCollectionContext = createContext(null); + +const ParentCollectionProvider = (props) => { + const collection = useCollection(); + return {props.children}; +}; + +export const useParentCollection = () => { + return useContext(ParentCollectionContext); +}; + +export const AssociationProvider: FC = (props) => { + const { name, children } = props; + + const collectionManager = useCollectionManager(); + const collectionName = collectionManager.getCollectionName(name); + + if (!collectionName) return ; + + return ( + + + + {children} + + + + ); +}; diff --git a/packages/core/client/src/data-source/collection/Collection.ts b/packages/core/client/src/data-source/collection/Collection.ts new file mode 100644 index 000000000..5f83bcaea --- /dev/null +++ b/packages/core/client/src/data-source/collection/Collection.ts @@ -0,0 +1,280 @@ +import { SchemaKey } from '@formily/react'; +import { filter } from 'lodash'; + +import type { CollectionManager } from './CollectionManager'; + +type dumpable = 'required' | 'optional' | 'skip'; +type CollectionSortable = string | boolean | { name?: string; scopeKey?: string }; + +export interface CollectionFieldOptions { + name?: any; + collectionName?: string; + sourceKey?: string; // association field + uiSchema?: any; + target?: string; + + [key: string]: any; +} + +export interface CollectionOptions { + name: string; + title?: string; + dataSource?: string; + /** + * Used for @nocobase/plugin-duplicator + * @see packages/core/database/src/collection-group-manager.tss + * + * @prop {'required' | 'optional' | 'skip'} dumpable - Determine whether the collection is dumped + * @prop {string[] | string} [with] - Collections dumped with this collection + * @prop {any} [delayRestore] - A function to execute after all collections are restored + */ + duplicator?: + | dumpable + | { + dumpable: dumpable; + with?: string[] | string; + delayRestore?: any; + }; + + tableName?: string; + inherits?: string[] | string; + inherit?: string; + key?: string; + viewName?: string; + writableView?: boolean; + + filterTargetKey?: string; + fields?: CollectionFieldOptions[]; + model?: any; + repository?: any; + sortable?: CollectionSortable; + /** + * @default true + */ + autoGenId?: boolean; + /** + * @default 'options' + */ + magicAttribute?: string; + + tree?: string; + + template?: string; + + isThrough?: boolean; + autoCreate?: boolean; + resource?: string; + collectionName?: string; + sourceKey?: string; + uiSchema?: any; + [key: string]: any; +} + +export type GetCollectionFieldPredicate = + | ((collection: CollectionFieldOptions) => boolean) + | CollectionFieldOptions + | keyof CollectionFieldOptions; + +export class Collection { + protected fieldsMap: Record; + protected primaryKey: string; + constructor( + protected options: CollectionOptions, + public collectionManager: CollectionManager, + ) {} + get fields() { + return this.options.fields || []; + } + + get app() { + return this.collectionManager.app; + } + + get dataSource() { + return this.options.dataSource; + } + + get sourceKey() { + return this.options.sourceKey; + } + + get name() { + return this.options.name; + } + get key() { + return this.options.key; + } + get title() { + return this.options.title; + } + get inherit() { + return this.options.inherit; + } + get hidden() { + return this.options.hidden; + } + get description() { + return this.options.description; + } + get duplicator() { + return this.options.duplicator; + } + get category() { + return this.options.category; + } + get targetKey() { + return this.options.targetKey; + } + get model() { + return this.options.model; + } + get createdBy() { + return this.options.createdBy; + } + get updatedBy() { + return this.options.updatedBy; + } + get logging() { + return this.options.logging; + } + get from() { + return this.options.from; + } + get rawTitle() { + return this.options.rawTitle; + } + getPrimaryKey(): string { + if (this.primaryKey) { + return this.primaryKey; + } + if (this.options.targetKey) { + return this.options.targetKey; + } + const field = this.getFields({ primaryKey: true })[0]; + this.primaryKey = field?.name; + + return this.primaryKey; + } + + get inherits() { + return this.options.inherits || []; + } + + get titleField() { + return this.hasField(this.options.titleField) ? this.options.titleField : this.getPrimaryKey(); + } + + get sources() { + return this.options.sources || []; + } + + get template() { + return this.options.template; + } + + get tableName() { + return this.options.tableName; + } + + get viewName() { + return this.options.viewName; + } + + get writableView() { + return this.options.writableView; + } + + get filterTargetKey() { + return this.options.filterTargetKey; + } + + get sortable() { + return this.options.sortable; + } + + get autoGenId() { + return this.options.autoGenId; + } + + get magicAttribute() { + return this.options.magicAttribute; + } + + get tree() { + return this.options.tree; + } + + get isThrough() { + return this.options.isThrough; + } + + get autoCreate() { + return this.options.autoCreate; + } + + get resource() { + return this.options.resource; + } + + getOptions() { + return this.options; + } + getOption(key: K): CollectionOptions[K] { + return this.options[key]; + } + /** + * Get fields + * @param predicate https://www.lodashjs.com/docs/lodash.filter + * @example + * getFields() // get all fields + * getFields({ name: 'nickname' }) // Get the field with `name: 'nickname'` + * getFields('primaryKey') // Get the field with `primaryKey: true` + * getFields((field) => field.name === 'nickname') // Get the field with `name: 'nickname`' + */ + getFields(predicate?: GetCollectionFieldPredicate) { + return predicate ? filter(this.fields, predicate) : this.fields; + } + getFieldsMap() { + if (!this.fieldsMap) { + this.fieldsMap = this.getFields().reduce((memo, field) => { + memo[field.name] = field; + return memo; + }, {}); + } + return this.fieldsMap; + } + + private getFieldByAssociationName(name: SchemaKey) { + const fieldsMap = this.getFieldsMap(); + const [fieldName, ...others] = String(name).split('.'); + const field = fieldsMap[fieldName]; + if (!field) return undefined; + + const collectionName = field?.target; + if (!collectionName) return undefined; + + const collection = this.collectionManager.getCollection(collectionName); + if (!collection) return undefined; + + return collection.getField(others.join('.')); + } + + getField(name: SchemaKey) { + if (!name) return undefined; + const fieldsMap = this.getFieldsMap(); + if (String(name).split('.').length > 1) { + const associationRes = this.getFieldByAssociationName(name); + if (associationRes) return associationRes; + + if (typeof name === 'string' && name.startsWith(`${this.name}.`)) { + name = name.replace(`${this.name}.`, ''); + return this.getField(name); + } + return undefined; + } + return fieldsMap[name]; + } + hasField(name: SchemaKey) { + return !!this.getField(name); + } +} diff --git a/packages/core/client/src/data-source/collection/CollectionManager.ts b/packages/core/client/src/data-source/collection/CollectionManager.ts new file mode 100644 index 000000000..95bdf8fa1 --- /dev/null +++ b/packages/core/client/src/data-source/collection/CollectionManager.ts @@ -0,0 +1,143 @@ +import type { SchemaKey } from '@formily/json-schema'; +import type { DataSource } from '../data-source'; +import type { CollectionFieldOptions, CollectionOptions, GetCollectionFieldPredicate } from './Collection'; + +import { Collection } from './Collection'; +import { applyMixins, collectionTransform } from './utils'; + +export class CollectionManager { + public collectionInstancesMap: Record = {}; + public collectionInstancesArr: Collection[] = []; + + constructor( + collections: CollectionOptions[], + public dataSource: DataSource, + ) { + this.addCollections(collections); + } + + get dataSourceManager() { + return this.dataSource.dataSourceManager; + } + + get app() { + return this.dataSourceManager.app; + } + + protected getCollectionInstance(collection: CollectionOptions) { + const collectionTemplateInstance = + this.dataSource.dataSourceManager.collectionTemplateManager.getCollectionTemplate(collection.template); + const Cls = collectionTemplateInstance?.Collection || Collection; + const transform = collectionTemplateInstance?.transform; + const transformedCollection = transform + ? transform(collectionTransform(collection, this.app), this.app) + : collectionTransform(collection, this.dataSource.dataSourceManager.app); + const options = { ...transformedCollection, dataSource: this.dataSource.key }; + const instance = applyMixins(Cls, this.dataSourceManager.collectionMixins, options, this); + return instance; + } + + private getInstancesMap(collections: CollectionOptions[] = []): Record { + const instances = collections.map((collection) => this.getCollectionInstance(collection)); + return instances.reduce>((acc, collectionInstance) => { + acc[collectionInstance.name] = collectionInstance; + return acc; + }, {}); + } + + addCollections(collections: CollectionOptions[] = []) { + const newInstancesMap = this.getInstancesMap(collections); + + this.collectionInstancesMap = { + ...this.collectionInstancesMap, + ...newInstancesMap, + }; + + this.collectionInstancesArr = Object.values(this.collectionInstancesMap); + } + + setCollections(collections: CollectionOptions[]) { + this.collectionInstancesMap = this.getInstancesMap(collections); + this.collectionInstancesArr = Object.values(this.collectionInstancesMap); + } + + reAddCollections(collectionInstances: Collection[] = this.collectionInstancesArr) { + const collectionOptions = collectionInstances.map((collection) => collection.getOptions()); + this.addCollections(collectionOptions); + } + + /** + * Get a collection + * @example + * getCollection('users'); // Get the 'users' collection + * getCollection('users.profile'); // Get the associated collection of the 'profile' field in the 'users' collection + * getCollection('a.b.c'); // Get the associated collection of the 'c' field in the 'a' collection, which is associated with the 'b' field in the 'a' collection + */ + getCollection(path: SchemaKey | CollectionOptions): (Mixins & Collection) | undefined { + if (!path) return undefined; + + if (typeof path === 'object') { + return this.getCollectionInstance(path) as Mixins & Collection; + } + + if (String(path).split('.').length > 1) { + const associationField = this.getCollectionField(path); + if (!associationField) return undefined; + return this.getCollection(associationField.target); + } + + return this.collectionInstancesMap[path] as Mixins & Collection; + } + + getCollections(predicate?: (collection: Collection) => boolean) { + if (predicate) { + return this.collectionInstancesArr.filter(predicate); + } + return this.collectionInstancesArr; + } + + getCollectionName(path: SchemaKey | CollectionOptions): string | undefined { + const res = this.getCollection(path); + return res?.name; + } + + /** + * Get collection field + * @example + * getField('users.username'); // Get the 'username' field of the 'users' collection + * getField('a.b.c'); // Get the associated collection of the 'c' field in the 'a' collection, which is associated with the 'b' field in the 'a' collection + */ + getCollectionField(path: SchemaKey | CollectionFieldOptions) { + if (!path) return; + + if (typeof path === 'object') { + return path; + } + + if (String(path).split('.').length < 2) { + console.error(`[@nocobase/client]: CollectionManager.getField() path "${path}" is invalid`); + return; + } + + const [collectionName, ...fieldNames] = String(path).split('.'); + const collection = this.getCollection(collectionName); + if (!collection) { + return; + } + return collection.getField(fieldNames.join('.')); + } + + getCollectionFields(collectionName: string, predicate?: GetCollectionFieldPredicate) { + return this.getCollection(collectionName)?.getFields(predicate) || []; + } + + clone(collections: CollectionOptions[] = []) { + const collectionManager = new CollectionManager([], this.dataSource); + + collectionManager.collectionInstancesArr = this.collectionInstancesArr; + collectionManager.collectionInstancesMap = this.collectionInstancesMap; + + collectionManager.addCollections(collections); + return collectionManager; + } +} diff --git a/packages/core/client/src/data-source/collection/CollectionManagerProvider.tsx b/packages/core/client/src/data-source/collection/CollectionManagerProvider.tsx new file mode 100644 index 000000000..c3e7669cc --- /dev/null +++ b/packages/core/client/src/data-source/collection/CollectionManagerProvider.tsx @@ -0,0 +1,47 @@ +import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react'; +import type { CollectionManager } from './CollectionManager'; +import type { Collection } from './Collection'; +import { DataSourceProvider, useDataSource } from '../data-source/DataSourceProvider'; +import { useExtendCollections } from './ExtendCollectionsProvider'; + +export const CollectionManagerContext = createContext(null); +CollectionManagerContext.displayName = 'CollectionManagerContext'; + +export interface CollectionManagerProviderProps { + instance?: CollectionManager; + dataSource?: string; + children?: ReactNode; +} + +const CollectionManagerProviderInner: FC = ({ instance, children }) => { + const dataSource = useDataSource(); + const extendCollections = useExtendCollections(); + + const cm = useMemo(() => { + const res = instance || dataSource?.collectionManager; + if (extendCollections?.length) { + return res.clone(extendCollections); + } + return res; + }, [instance, extendCollections, dataSource]); + return {children}; +}; + +export const CollectionManagerProvider: FC = ({ instance, dataSource, children }) => { + return ( + + {children} + + ); +}; + +export function useCollectionManager() { + const context = useContext(CollectionManagerContext); + return context; +} + +export const useCollections = (predicate?: (collection: Collection) => boolean) => { + const collectionManager = useCollectionManager(); + const collections = useMemo(() => collectionManager.getCollections(predicate), [collectionManager, predicate]); + return collections; +}; diff --git a/packages/core/client/src/data-source/collection/CollectionProvider.tsx b/packages/core/client/src/data-source/collection/CollectionProvider.tsx new file mode 100644 index 000000000..249ffdf1c --- /dev/null +++ b/packages/core/client/src/data-source/collection/CollectionProvider.tsx @@ -0,0 +1,35 @@ +import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react'; + +import { useCollectionManager } from './CollectionManagerProvider'; +import { CollectionDeletedPlaceholder } from '../components/CollectionDeletedPlaceholder'; +import type { CollectionOptions, Collection, GetCollectionFieldPredicate } from './Collection'; + +export const CollectionContext = createContext(null); +CollectionContext.displayName = 'CollectionContext'; + +export interface CollectionProviderProps { + name: string | CollectionOptions; + children?: ReactNode; + allowNull?: boolean; +} + +export const CollectionProvider: FC = (props) => { + const { name, children, allowNull } = props; + const collectionManager = useCollectionManager(); + const collection = useMemo(() => collectionManager?.getCollection(name), [collectionManager, name]); + if (!collection && allowNull) return <>{props.children}; + if (!collection && !allowNull) return ; + return {children}; +}; + +export function useCollection(): (Mixins & Collection) | undefined { + const context = useContext(CollectionContext); + + return context as (Mixins & Collection) | undefined; +} + +export const useCollectionFields = (predicate?: GetCollectionFieldPredicate) => { + const collection = useCollection(); + const fields = useMemo(() => collection.getFields(predicate), [collection, predicate]); + return fields; +}; diff --git a/packages/core/client/src/data-source/collection/ExtendCollectionsProvider.tsx b/packages/core/client/src/data-source/collection/ExtendCollectionsProvider.tsx new file mode 100644 index 000000000..1acfffb8e --- /dev/null +++ b/packages/core/client/src/data-source/collection/ExtendCollectionsProvider.tsx @@ -0,0 +1,31 @@ +import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react'; +import { CollectionOptions } from './Collection'; +import { useDataSourceKey } from '../data-source/DataSourceProvider'; +import { CollectionManagerProvider } from './CollectionManagerProvider'; + +export const ExtendCollectionsContext = createContext(null); +ExtendCollectionsContext.displayName = 'ExtendCollectionsContext'; + +export interface ExtendCollectionsProviderProps { + collections: CollectionOptions[]; + children?: ReactNode; +} + +export const ExtendCollectionsProvider: FC = ({ children, collections }) => { + const parentCollections = useExtendCollections(); + const extendCollections = useMemo(() => { + return parentCollections ? [...parentCollections, ...collections] : collections; + }, [parentCollections, collections]); + const dataSource = useDataSourceKey(); + + return ( + + {children} + + ); +}; + +export function useExtendCollections() { + const context = useContext(ExtendCollectionsContext); + return context; +} diff --git a/packages/core/client/src/data-source/collection/index.ts b/packages/core/client/src/data-source/collection/index.ts new file mode 100644 index 000000000..9803f7605 --- /dev/null +++ b/packages/core/client/src/data-source/collection/index.ts @@ -0,0 +1,7 @@ +export * from './Collection'; +export * from './CollectionProvider'; +export * from './AssociationProvider'; + +export * from './CollectionManager'; +export * from './CollectionManagerProvider'; +export * from './ExtendCollectionsProvider'; diff --git a/packages/core/client/src/data-source/collection/utils.ts b/packages/core/client/src/data-source/collection/utils.ts new file mode 100644 index 000000000..84a2fd013 --- /dev/null +++ b/packages/core/client/src/data-source/collection/utils.ts @@ -0,0 +1,52 @@ +import type { Application } from '../../application/Application'; +import type { CollectionOptions } from './Collection'; + +export const collectionTransform = (collection: CollectionOptions, app: Application) => { + const { rawTitle, title, fields = [], ...rest } = collection; + return { + ...rest, + title: rawTitle ? title : app.i18n.t(title), + rawTitle: rawTitle || title, + fields: fields?.map(({ uiSchema, ...field }) => { + if (uiSchema?.title) { + const title = uiSchema.title; + uiSchema.title = uiSchema.rawTitle ? title : app.i18n.t(title, { ns: 'lm-collections' }); + uiSchema.rawTitle = uiSchema.rawTitle || title; + } + if (Array.isArray(uiSchema?.enum)) { + uiSchema.enum = uiSchema.enum.map((item) => ({ + ...item, + value: item?.value === undefined ? item : item.value, + label: item.rawLabel ? item.label : app.i18n.t(item.label, { ns: 'lm-collections' }), + rawLabel: item.rawLabel || item.label, + })); + } + return { uiSchema, ...field }; + }), + }; +}; + +export function applyMixins(Cls: any, mixins: any[], options?: any, collectionManager?: any) { + const instance = new Cls(options, collectionManager); + mixins.forEach((MixinClass) => { + const mixin = new MixinClass(options, collectionManager); + // 设置实例属性,将 mixin 的属性复制到 instance 上 + Object.getOwnPropertyNames(mixin).forEach((key) => { + const descriptor = Object.getOwnPropertyDescriptor(mixin, key); + if (descriptor) { + Object.defineProperty(instance, key, descriptor); + } else { + instance[key] = mixin[key]; + } + }); + + // 将 mixin 的原型属方法复制到 instance 的原型上 + Object.getOwnPropertyNames(MixinClass.prototype).forEach((key) => { + if (key !== 'constructor') { + instance.__proto__[key] = MixinClass.prototype[key]; + } + }); + }); + + return instance; +} diff --git a/packages/core/client/src/data-source/components/CollectionDeletedPlaceholder.tsx b/packages/core/client/src/data-source/components/CollectionDeletedPlaceholder.tsx new file mode 100644 index 000000000..e96e3244c --- /dev/null +++ b/packages/core/client/src/data-source/components/CollectionDeletedPlaceholder.tsx @@ -0,0 +1,97 @@ +import { App, Button, Result, Typography } from 'antd'; +import React, { FC, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { CardItem, useCompile, useDesignable } from '../../schema-component'; +import { useDataSource } from '../data-source/DataSourceProvider'; +import { useDataSourceManager } from '../data-source'; +import { DEFAULT_DATA_SOURCE_KEY } from '../../data-source/data-source/DataSourceManager'; +import { useCollection } from '../collection'; + +export interface CollectionDeletedPlaceholderProps { + type: 'Collection' | 'Field' | 'DataSource'; + name?: string | number; + message?: string; +} + +const { Text } = Typography; + +export const CollectionDeletedPlaceholder: FC = ({ type, name, message }) => { + const { designable, dn } = useDesignable(); + const { modal } = App.useApp(); + const { t } = useTranslation(); + const dataSource = useDataSource(); + const compile = useCompile(); + const collection = useCollection(); + const dataSourceManager = useDataSourceManager(); + const nameValue = useMemo(() => { + if (type === 'DataSource') { + return name; + } + const dataSourcePrefix = + dataSourceManager?.getDataSources().length >= 1 && dataSource && dataSource.key !== DEFAULT_DATA_SOURCE_KEY + ? `${compile(dataSource.displayName || dataSource.key)} > ` + : ''; + if (type === 'Collection') { + return `${dataSourcePrefix}${name}`; + } + const collectionPrefix = collection + ? `${compile(collection.title) || collection.name || collection.tableName} > ` + : ''; + return `${dataSourcePrefix}${collectionPrefix}${name}`; + }, []); + + const blockType = useMemo(() => { + if (type === 'Field') { + return 'Field'; + } + return 'Block'; + }, [type]); + + const messageValue = useMemo(() => { + if (!name) { + return `${t(type)} ${'name is required'}`; + } + if (message) { + return `${t(type)} "${nameValue}" ${message}`; + } + return t(`The {{type}} "{{name}}" may have been deleted. Please remove this {{blockType}}.`, { + type: t(type).toLocaleLowerCase(), + name: nameValue, + blockType: t(blockType), + }).replaceAll('>', '>'); + }, [message, nameValue, type, t, blockType]); + + if (designable || process.env.NODE_ENV === 'development') { + if (type === 'Field') { + return {messageValue}; + } + + return ( + + + modal.confirm({ + title: t('Delete block'), + content: t('Are you sure you want to delete it?'), + ...confirm, + onOk() { + dn.remove(null, { removeParentsIfNoChildren: true, breakRemoveOn: { 'x-component': 'Grid' } }); + }, + }) + } + > + {t('Delete')} + + } + /> + + ); + } + + return null; +}; diff --git a/packages/core/client/src/data-source/components/DataSourceApplicationProvider.tsx b/packages/core/client/src/data-source/components/DataSourceApplicationProvider.tsx new file mode 100644 index 000000000..c2b754691 --- /dev/null +++ b/packages/core/client/src/data-source/components/DataSourceApplicationProvider.tsx @@ -0,0 +1,23 @@ +import React, { FC } from 'react'; +import type { DataSourceManager } from '../data-source/DataSourceManager'; +import { DataSourceManagerProvider } from '../data-source/DataSourceManagerProvider'; +import { + type CollectionManagerProviderProps, + CollectionManagerProvider, +} from '../collection/CollectionManagerProvider'; + +interface DataSourceApplicationProviderProps extends CollectionManagerProviderProps { + dataSourceManager: DataSourceManager; +} + +export const DataSourceApplicationProvider: FC = ({ + children, + dataSourceManager, + ...otherProps +}) => { + return ( + + {children} + + ); +}; diff --git a/packages/core/client/src/data-source/components/index.ts b/packages/core/client/src/data-source/components/index.ts new file mode 100644 index 000000000..0746410b7 --- /dev/null +++ b/packages/core/client/src/data-source/components/index.ts @@ -0,0 +1,2 @@ +export * from './CollectionDeletedPlaceholder'; +export * from './DataSourceApplicationProvider'; diff --git a/packages/core/client/src/data-source/data-block/DataBlockProvider.tsx b/packages/core/client/src/data-source/data-block/DataBlockProvider.tsx new file mode 100644 index 000000000..cb1ea1170 --- /dev/null +++ b/packages/core/client/src/data-source/data-block/DataBlockProvider.tsx @@ -0,0 +1,171 @@ +import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react'; + +import { ACLCollectionProvider } from '../../acl/ACLProvider'; +import { UseRequestOptions, UseRequestService } from '../../api-client'; +import { withDynamicSchemaProps } from '../../application/hoc'; +import { Designable, useDesignable } from '../../schema-component'; +import { AssociationProvider, CollectionManagerProvider, CollectionOptions, CollectionProvider } from '../collection'; +import { CollectionRecord } from '../collection-record'; +import { BlockRequestProvider } from './DataBlockRequestProvider'; +import { DataBlockResourceProvider } from './DataBlockResourceProvider'; + +export interface AllDataBlockProps { + collection: string | CollectionOptions; + association: string; + dataSource?: string; + sourceId?: string | number; + filterByTk: string | number; + record: CollectionRecord; + action?: 'list' | 'get'; + params?: { + filterByTk?: string | number; + [index: string]: any; + }; + parentRecord?: CollectionRecord; + requestService?: UseRequestService; + requestOptions?: UseRequestOptions; + [index: string]: any; +} + +type CollectionCreateProps = Pick; + +interface CollectionGetProps + extends Pick< + AllDataBlockProps, + 'collection' | 'dataSource' | 'filterByTk' | 'params' | 'requestService' | 'requestOptions' + > { + action: 'get'; +} + +interface CollectionListProps + extends Pick { + action: 'list'; +} + +type CollectionRecordProps = Pick< + AllDataBlockProps, + 'collection' | 'dataSource' | 'record' | 'requestService' | 'requestOptions' +>; + +type AssociationCreateProps = Pick; + +interface AssociationGetProps + extends Pick< + AllDataBlockProps, + | 'association' + | 'dataSource' + | 'sourceId' + | 'parentRecord' + | 'filterByTk' + | 'params' + | 'requestService' + | 'requestOptions' + > { + action: 'get'; +} + +interface AssociationListProps + extends Pick< + AllDataBlockProps, + 'association' | 'dataSource' | 'sourceId' | 'parentRecord' | 'params' | 'requestService' | 'requestOptions' + > { + action: 'list'; +} + +type AssociationRecordProps = Pick< + AllDataBlockProps, + 'association' | 'dataSource' | 'record' | 'parentRecord' | 'requestService' | 'requestOptions' +>; + +type AllDataBlockType = { + CollectionCreate: CollectionCreateProps; + CollectionGet: CollectionGetProps; + CollectionList: CollectionListProps; + CollectionRecord: CollectionRecordProps; + AssociationCreate: AssociationCreateProps; + AssociationGet: AssociationGetProps; + AssociationList: AssociationListProps; + AssociationRecord: AssociationRecordProps; +}; + +export type DataBlockProviderProps = AllDataBlockType[keyof AllDataBlockType]; + +export type UseDataBlockProps = ( + props: DataBlockProviderProps & { [index: string]: any }, +) => Omit & { [index: string]: any }; + +export interface DataBlockContextValue { + props: AllDataBlockProps & T; + dn: Designable; +} + +export const DataBlockContext = createContext>({} as any); +DataBlockContext.displayName = 'DataBlockContext'; + +export const AssociationOrCollectionProvider = (props: { + collection: string | CollectionOptions; + association: string; + children: ReactNode; + allowNull?: boolean; +}) => { + const { collection, association, allowNull = false } = props; + const AssociationOrCollection = useMemo(() => { + if (association) { + return { + Component: AssociationProvider, + name: association, + }; + } + return { + Component: CollectionProvider, + name: collection, + }; + }, [collection, association]); + + return ( + + {props.children} + + ); +}; + +export const DataBlockProvider: FC = withDynamicSchemaProps( + (props) => { + const { collection, association, dataSource, children, ...resets } = props as Partial; + const { dn } = useDesignable(); + + return ( + + + + + + {children} + + + + + + ); + }, + { displayName: 'DataBlockProvider' }, +); + +export const useDataBlock = () => { + const context = useContext>(DataBlockContext); + if (!context) { + throw new Error('useDataBlock() must be used within a DataBlockProvider'); + } + + return context; +}; + +export const useDataBlockProps = (): DataBlockContextValue['props'] => { + const context = useDataBlock(); + return context.props; +}; diff --git a/packages/core/client/src/data-source/data-block/DataBlockRequestProvider.tsx b/packages/core/client/src/data-source/data-block/DataBlockRequestProvider.tsx new file mode 100644 index 000000000..38e0c8dfe --- /dev/null +++ b/packages/core/client/src/data-source/data-block/DataBlockRequestProvider.tsx @@ -0,0 +1,136 @@ +import { useDeepCompareEffect, useUpdateEffect } from 'ahooks'; +import React, { FC, createContext, useContext, useMemo } from 'react'; + +import { UseRequestResult, useAPIClient, useRequest } from '../../api-client'; +import { CollectionRecordProvider, CollectionRecord } from '../collection-record'; +import { AllDataBlockProps, useDataBlockProps } from './DataBlockProvider'; +import { useDataBlockResource } from './DataBlockResourceProvider'; + +export const BlockRequestContext = createContext>(null); +BlockRequestContext.displayName = 'BlockRequestContext'; + +function useCurrentRequest(options: Omit) { + const resource = useDataBlockResource(); + const { action, params = {}, record, requestService, requestOptions } = options; + if (params.filterByTk === undefined) { + delete params.filterByTk; + } + const request = useRequest( + requestService + ? requestService + : (customParams) => { + if (record) return Promise.resolve({ data: record }); + if (!action) { + throw new Error( + `[nocobase]: The 'action' parameter is missing in the 'DataBlockRequestProvider' component`, + ); + } + return resource[action]({ ...params, ...customParams }).then((res) => res.data); + }, + { + ...requestOptions, + manual: true, + }, + ); + + // 因为修改 Schema 会导致 params 对象发生变化,所以这里使用 `DeepCompare` + useDeepCompareEffect(() => { + if (action) { + request.run(); + } + }, [params, action, record]); + + useUpdateEffect(() => { + if (action) { + request.run(); + } + }, [resource]); + + return request; +} + +function useParentRequest(options: Omit) { + const { sourceId, association, parentRecord } = options; + const api = useAPIClient(); + return useRequest( + async () => { + if (parentRecord) return Promise.resolve({ data: parentRecord }); + if (!association) return Promise.resolve({ data: undefined }); + // "association": "Collection.Field" + const arr = association.split('.'); + // :get/ + const url = `${arr[0]}:get/${sourceId}`; + const res = await api.request({ url }); + return res.data; + }, + { + refreshDeps: [association, parentRecord, sourceId], + }, + ); +} + +export const BlockRequestProvider: FC = ({ children }) => { + const props = useDataBlockProps(); + const { + action, + filterByTk, + sourceId, + params = {}, + association, + collection, + record, + parentRecord, + requestOptions, + requestService, + } = props; + const currentRequest = useCurrentRequest<{ data: any }>({ + action, + sourceId, + record, + association, + collection, + requestOptions, + requestService, + params: { + ...params, + filterByTk: filterByTk || params.filterByTk, + }, + }); + + const parentRequest = useParentRequest<{ data: any }>({ + sourceId, + association, + parentRecord, + }); + + const memoizedParentRecord = useMemo(() => { + return parentRequest.data?.data && new CollectionRecord({ isNew: false, data: parentRequest.data?.data }); + }, [parentRequest.data?.data]); + + return ( + + {action !== 'list' ? ( + + {children} + + ) : ( + + {children} + + )} + + ); +}; + +export const useDataBlockRequest = (): UseRequestResult<{ data: T }> => { + const context = useContext(BlockRequestContext); + if (!context) { + throw new Error('useDataBlockRequest() must be used within a DataBlockRequestProvider'); + } + + return context; +}; diff --git a/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx b/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx new file mode 100644 index 000000000..32e3e59a8 --- /dev/null +++ b/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx @@ -0,0 +1,51 @@ +import { IResource } from '@nocobase/sdk'; +import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react'; + +import { useCollectionManager } from '../collection'; +import { useDataBlockProps } from './DataBlockProvider'; +import { useAPIClient } from '../../api-client'; +import { CollectionRecord } from '../collection-record'; +import { useDataSourceHeaders } from '../utils'; + +export const DataBlockResourceContext = createContext(null); +DataBlockResourceContext.displayName = 'DataBlockResourceContext'; + +export const DataBlockResourceProvider: FC<{ children?: ReactNode }> = ({ children }) => { + const dataBlockProps = useDataBlockProps(); + const cm = useCollectionManager(); + const { association, collection, dataSource, sourceId, parentRecord } = dataBlockProps; + const api = useAPIClient(); + const headers = useDataSourceHeaders(dataSource); + const collectionName = useMemo(() => (typeof collection === 'string' ? collection : collection?.name), [collection]); + + const sourceIdValue = useMemo(() => { + if (sourceId) { + return sourceId; + } + if (association && parentRecord) { + const associationCollection = cm.getCollection(association); + if (associationCollection) { + const parentRecordData = parentRecord instanceof CollectionRecord ? parentRecord.data : parentRecord; + return parentRecordData[associationCollection.sourceKey || 'id']; + } + } + }, [sourceId, parentRecord]); + + const resource = useMemo(() => { + if (association) { + return api.resource(association, sourceIdValue, headers); + } + return api.resource(collectionName, undefined, headers); + }, [api, association, collection, sourceIdValue, headers]); + return {children}; +}; + +export function useDataBlockResource() { + const context = useContext(DataBlockResourceContext); + + if (!context) { + throw new Error('useDataBlockResource() must be used within a DataBlockResourceProvider'); + } + + return context; +} diff --git a/packages/core/client/src/data-source/data-block/index.ts b/packages/core/client/src/data-source/data-block/index.ts new file mode 100644 index 000000000..ae2ee6dd4 --- /dev/null +++ b/packages/core/client/src/data-source/data-block/index.ts @@ -0,0 +1,3 @@ +export * from './DataBlockResourceProvider'; +export * from './DataBlockRequestProvider'; +export * from './DataBlockProvider'; diff --git a/packages/core/client/src/data-source/data-source/DataSource.ts b/packages/core/client/src/data-source/data-source/DataSource.ts new file mode 100644 index 000000000..d1c5cfa7a --- /dev/null +++ b/packages/core/client/src/data-source/data-source/DataSource.ts @@ -0,0 +1,91 @@ +import type { CollectionOptions } from '../collection'; +import type { DataSourceManager } from './DataSourceManager'; + +import { CollectionManager } from '../collection'; + +type LoadCallback = (collections: CollectionOptions[]) => void; + +export interface DataSourceOptions { + key: string; + displayName: string; + collections?: CollectionOptions[]; + errorMessage?: string; + status?: 'loaded' | 'loading-failed' | 'loading' | 'reloading'; +} + +export type DataSourceFactory = new (options: DataSourceOptions, dataSourceManager: DataSourceManager) => DataSource; + +export abstract class DataSource { + collectionManager: CollectionManager; + protected reloadCallbacks: LoadCallback[] = []; + + constructor( + protected options: DataSourceOptions, + public dataSourceManager: DataSourceManager, + ) { + this.collectionManager = new CollectionManager(options.collections, this); + } + + get app() { + return this.dataSourceManager.app; + } + + get key() { + return this.options.key; + } + + get displayName() { + return this.options.displayName; + } + + get status() { + return this.options.status; + } + + get errorMessage() { + return this.options.errorMessage; + } + + get collections() { + return this.collectionManager.getCollections() || []; + } + + getOptions() { + return this.options; + } + + setOptions(options: Partial) { + Object.assign(this.options, options); + } + + getOption(key: Key): DataSourceOptions[Key] { + return this.options[key]; + } + + addReloadCallback(callback: LoadCallback) { + if (this.reloadCallbacks.includes(callback)) return; + this.reloadCallbacks.push(callback); + } + + removeReloadCallback(callback: LoadCallback) { + this.reloadCallbacks = this.reloadCallbacks.filter((cb) => cb !== callback); + } + + abstract getDataSource(): Promise, 'key'>> | Omit, 'key'>; + + async reload() { + const dataSource = await this.getDataSource(); + this.setOptions(dataSource); + this.collectionManager.setCollections(dataSource.collections || []); + this.reloadCallbacks.forEach((callback) => callback(dataSource.collections)); + return this.options; + } +} + +export class LocalDataSource extends DataSource { + getDataSource() { + return { + collections: this.collections, + }; + } +} diff --git a/packages/core/client/src/data-source/data-source/DataSourceManager.ts b/packages/core/client/src/data-source/data-source/DataSourceManager.ts new file mode 100644 index 000000000..a84cc0beb --- /dev/null +++ b/packages/core/client/src/data-source/data-source/DataSourceManager.ts @@ -0,0 +1,117 @@ +import type { CollectionOptions, Collection } from '../collection'; +import type { Application } from '../../application/Application'; + +import { type DataSourceOptions, DataSource, LocalDataSource, DataSourceFactory } from './DataSource'; +import { type CollectionTemplateFactory, CollectionTemplateManager } from '../collection-template'; +import { type CollectionFieldInterfaceFactory, CollectionFieldInterfaceManager } from '../collection-field-interface'; + +export const DEFAULT_DATA_SOURCE_KEY = 'main'; +export const DEFAULT_DATA_SOURCE_TITLE = '{{t("Main")}}'; + +export interface DataSourceManagerOptions { + collectionTemplates?: CollectionTemplateFactory[]; + fieldInterfaces?: CollectionFieldInterfaceFactory[]; + fieldInterfaceGroups?: Record; + collectionMixins?: (typeof Collection)[]; + dataSources?: DataSourceOptions[]; + collections?: CollectionOptions[]; +} + +export class DataSourceManager { + protected dataSourceInstancesMap: Record = {}; + protected multiDataSources: [() => Promise, DataSourceFactory][] = []; + public collectionMixins: (typeof Collection)[] = []; + public collectionTemplateManager: CollectionTemplateManager; + public collectionFieldInterfaceManager: CollectionFieldInterfaceManager; + + constructor( + protected options: DataSourceManagerOptions = {}, + public app: Application, + ) { + this.collectionTemplateManager = new CollectionTemplateManager(options.collectionTemplates, this); + this.collectionFieldInterfaceManager = new CollectionFieldInterfaceManager( + options.fieldInterfaces, + options.fieldInterfaceGroups, + this, + ); + this.collectionMixins.push(...(options.collectionMixins || [])); + + this.addDataSource(LocalDataSource, { + key: DEFAULT_DATA_SOURCE_KEY, + displayName: DEFAULT_DATA_SOURCE_TITLE, + collections: options.collections || [], + }); + (options.dataSources || []).forEach((dataSourceOptions) => { + this.addDataSource(LocalDataSource, dataSourceOptions); + }); + } + + addCollectionMixins(mixins: (typeof Collection)[] = []) { + const newMixins = mixins.filter((mixin) => !this.collectionMixins.includes(mixin)); + if (!newMixins.length) return; + this.collectionMixins.push(...newMixins); + + // Re-add tables + this.getDataSources().forEach((dataSource) => dataSource.collectionManager.reAddCollections()); + } + + getDataSources() { + return Object.values(this.dataSourceInstancesMap); + } + + getDataSource(key?: string) { + return key ? this.dataSourceInstancesMap[key] : this.dataSourceInstancesMap[DEFAULT_DATA_SOURCE_KEY]; + } + + removeDataSources(keys: string[]) { + keys.forEach((key) => { + delete this.dataSourceInstancesMap[key]; + }); + } + + addDataSource(DataSource: DataSourceFactory, options: DataSourceOptions) { + const dataSourceInstance = new DataSource(options, this); + this.dataSourceInstancesMap[dataSourceInstance.key] = dataSourceInstance; + return dataSourceInstance; + } + + async addDataSources(request: () => Promise, DataSource: DataSourceFactory) { + if (this.multiDataSources.some(([req, DS]) => req === request && DS === DataSource)) return; + this.multiDataSources.push([request, DataSource]); + } + + getAllCollections( + predicate?: (collection: Collection) => boolean, + ): (DataSourceOptions & { collections: Collection[] })[] { + return this.getDataSources().reduce<(DataSourceOptions & { collections: Collection[] })[]>((acc, dataSource) => { + acc.push({ + ...dataSource.getOptions(), + collections: dataSource.collectionManager.getCollections(predicate), + }); + return acc; + }, []); + } + + addFieldInterfaceGroups(options: Record) { + this.collectionFieldInterfaceManager.addFieldInterfaceGroups(options); + } + + addCollectionTemplates(templateClasses: CollectionTemplateFactory[] = []) { + this.collectionTemplateManager.addCollectionTemplates(templateClasses); + } + + addFieldInterfaces(fieldInterfaceClasses: CollectionFieldInterfaceFactory[] = []) { + this.collectionFieldInterfaceManager.addFieldInterfaces(fieldInterfaceClasses); + } + + async reload() { + await Promise.all( + this.multiDataSources.map(async ([request, DataSource]) => { + const list = await request(); + list.map((options) => this.addDataSource(DataSource, options)); + }), + ); + + return Promise.all(this.getDataSources().map((dataSource) => dataSource.reload())); + } +} diff --git a/packages/core/client/src/data-source/data-source/DataSourceManagerProvider.tsx b/packages/core/client/src/data-source/data-source/DataSourceManagerProvider.tsx new file mode 100644 index 000000000..d46714084 --- /dev/null +++ b/packages/core/client/src/data-source/data-source/DataSourceManagerProvider.tsx @@ -0,0 +1,19 @@ +import React, { FC, ReactNode, createContext, useContext } from 'react'; +import type { DataSourceManager } from './DataSourceManager'; + +export const DataSourceManagerContext = createContext(null); +DataSourceManagerContext.displayName = 'DataSourceManagerContext'; + +export interface DataSourceManagerProviderProps { + dataSourceManager: DataSourceManager; + children?: ReactNode; +} + +export const DataSourceManagerProvider: FC = ({ children, dataSourceManager }) => { + return {children}; +}; + +export function useDataSourceManager() { + const context = useContext(DataSourceManagerContext); + return context; +} diff --git a/packages/core/client/src/data-source/data-source/DataSourceProvider.tsx b/packages/core/client/src/data-source/data-source/DataSourceProvider.tsx new file mode 100644 index 000000000..273886b76 --- /dev/null +++ b/packages/core/client/src/data-source/data-source/DataSourceProvider.tsx @@ -0,0 +1,74 @@ +import React, { FC, ReactNode, createContext, useContext } from 'react'; +import type { DataSource } from './DataSource'; +import { useDataSourceManager } from './DataSourceManagerProvider'; +import { CollectionDeletedPlaceholder } from '../components/CollectionDeletedPlaceholder'; +import { CardItem, useSchemaComponentContext } from '../../schema-component'; +import { Button, Result } from 'antd'; +import { LoadingOutlined } from '@ant-design/icons'; +import { useTranslation } from 'react-i18next'; + +export const DataSourceContext = createContext(null); +DataSourceContext.displayName = 'DataSourceContext'; + +export interface DataSourceProviderProps { + dataSource?: string; + children?: ReactNode; +} + +export const DataSourceProvider: FC = ({ children, dataSource }) => { + const dataSourceManager = useDataSourceManager(); + const { t } = useTranslation(); + const { refresh } = useSchemaComponentContext(); + const [_, setRandom] = React.useState(0); + const dataSourceValue = dataSourceManager.getDataSource(dataSource); + + if (!dataSourceValue) { + return ; + } + + if (dataSourceValue.status === 'loading-failed') { + return ( + + ); + } + + if (dataSourceValue.status === 'loading' || dataSourceValue.status === 'reloading') { + return ( + + } + title={`${dataSourceValue.displayName || dataSource} ${t('data source')} ${t('loading')}...`} + extra={ + + } + /> + + ); + } + + return {children}; +}; + +export function useDataSource() { + const context = useContext(DataSourceContext); + return context; +} + +export function useDataSourceKey() { + const context = useContext(DataSourceContext); + return context?.key; +} diff --git a/packages/core/client/src/data-source/data-source/index.ts b/packages/core/client/src/data-source/data-source/index.ts new file mode 100644 index 000000000..a7a651568 --- /dev/null +++ b/packages/core/client/src/data-source/data-source/index.ts @@ -0,0 +1,4 @@ +export * from './DataSource'; +export * from './DataSourceManager'; +export * from './DataSourceProvider'; +export * from './DataSourceManagerProvider'; diff --git a/packages/core/client/src/data-source/index.ts b/packages/core/client/src/data-source/index.ts new file mode 100644 index 000000000..61fc10035 --- /dev/null +++ b/packages/core/client/src/data-source/index.ts @@ -0,0 +1,9 @@ +export * from './collection'; +export * from './collection-field'; +export * from './collection-field-interface'; +export * from './collection-template'; +export * from './components'; +export * from './data-block'; +export * from './data-source'; +export * from './collection-record'; +export * from './utils'; diff --git a/packages/core/client/src/data-source/utils.ts b/packages/core/client/src/data-source/utils.ts new file mode 100644 index 000000000..c0b2e401c --- /dev/null +++ b/packages/core/client/src/data-source/utils.ts @@ -0,0 +1,23 @@ +// import { CascaderProps } from 'antd'; +import _ from 'lodash'; +import { useMemo } from 'react'; +import { CollectionFieldOptions } from './collection'; +import { DEFAULT_DATA_SOURCE_KEY, DataSourceManager } from './data-source/DataSourceManager'; + +// 等把老的去掉后,再把这个函数的实现从那边移动过来 +// export function getCollectionFieldsOptions(){} + +export const isTitleField = (dm: DataSourceManager, field: CollectionFieldOptions) => { + return !field.isForeignKey && dm.collectionFieldInterfaceManager.getFieldInterface(field.interface)?.titleUsable; +}; + +export const useDataSourceHeaders = (dataSource?: string) => { + const headers = useMemo(() => { + if (dataSource && dataSource !== DEFAULT_DATA_SOURCE_KEY) { + return { 'x-data-source': dataSource }; + // return { 'x-connection': dataSource }; + } + }, [dataSource]); + + return headers; +}; diff --git a/packages/core/client/src/filter-provider/FilterProvider.tsx b/packages/core/client/src/filter-provider/FilterProvider.tsx index 57fdc2aa1..20c218a0a 100644 --- a/packages/core/client/src/filter-provider/FilterProvider.tsx +++ b/packages/core/client/src/filter-provider/FilterProvider.tsx @@ -2,7 +2,7 @@ import { useField, useFieldSchema } from '@formily/react'; import { uniqBy } from 'lodash'; import React, { createContext, useEffect, useRef } from 'react'; import { useBlockRequestContext } from '../block-provider/BlockProvider'; -import { CollectionFieldOptions, useCollection } from '../collection-manager'; +import { CollectionFieldOptions_deprecated, useCollection_deprecated } from '../collection-manager'; import { removeNullCondition } from '../schema-component'; import { mergeFilter, useAssociatedFields } from './utils'; @@ -27,7 +27,7 @@ export interface ForeignKeyField { [key: string]: any; } -type Collection = ReturnType; +type Collection = ReturnType; export interface DataBlock { /** 唯一标识符,schema 中的 name 值 */ @@ -41,7 +41,7 @@ export interface DataBlock { /** 清除筛选区块设置的筛选参数 */ clearFilter: (uid: string) => void; /** 数据区块表中所有的关系字段 */ - associatedFields?: CollectionFieldOptions[]; + associatedFields?: CollectionFieldOptions_deprecated[]; /** 数据区块表中所有的外键字段 */ foreignKeyFields?: ForeignKeyField[]; /** 数据区块已经存在的过滤条件(通过 `设置数据范围` 或者其它能设置筛选条件的功能) */ @@ -70,18 +70,18 @@ export const FilterBlockProvider: React.FC = ({ children }) => { }; /** - * 用于收集记录当前页面中的数据区块的信息 + * 用于收集当前页面中的数据区块的信息,用于在过滤区块中使用 * @param param0 * @returns */ -export const FilterBlockRecord = ({ +export const DataBlockCollector = ({ children, params, }: { children: React.ReactNode; params?: { filter: FilterParam }; }) => { - const collection = useCollection(); + const collection = useCollection_deprecated(); const { recordDataBlocks, removeDataBlock } = useFilterBlock(); const { service } = useBlockRequestContext(); const field = useField(); diff --git a/packages/core/client/src/filter-provider/utils.ts b/packages/core/client/src/filter-provider/utils.ts index 829cd6ffe..65d3f7b75 100644 --- a/packages/core/client/src/filter-provider/utils.ts +++ b/packages/core/client/src/filter-provider/utils.ts @@ -4,11 +4,11 @@ import _ from 'lodash'; import { useCallback, useEffect, useState } from 'react'; import { FilterTarget, findFilterTargets } from '../block-provider/hooks'; import { - Collection, - CollectionFieldOptions, + Collection_deprecated, + CollectionFieldOptions_deprecated, FieldOptions, - useCollection, - useCollectionManager, + useCollection_deprecated, + useCollectionManager_deprecated, } from '../collection-manager'; import { removeNullCondition } from '../schema-component'; import { findFilterOperators } from '../schema-component/antd/form-item/SchemaSettingOptions'; @@ -37,13 +37,13 @@ export const mergeFilter = (filters: any[], op = '$and') => { }; export const getSupportFieldsByAssociation = (inheritCollectionsChain: string[], block: DataBlock) => { - return block.associatedFields?.filter((field) => - inheritCollectionsChain.some((collectionName) => collectionName === field.target), + return block.associatedFields?.filter( + (field) => inheritCollectionsChain?.some((collectionName) => collectionName === field.target), ); }; export const getSupportFieldsByForeignKey = ( - filterBlockCollection: ReturnType, + filterBlockCollection: ReturnType, block: DataBlock, ) => { return block.foreignKeyFields?.filter((foreignKeyField) => { @@ -61,8 +61,8 @@ export const getSupportFieldsByForeignKey = ( export const useSupportedBlocks = (filterBlockType: FilterBlockType) => { const { getDataBlocks } = useFilterBlock(); const fieldSchema = useFieldSchema(); - const collection = useCollection(); - const { getAllCollectionsInheritChain } = useCollectionManager(); + const collection = useCollection_deprecated(); + const { getAllCollectionsInheritChain } = useCollectionManager_deprecated(); // Form 和 Collapse 仅支持同表的数据区块 if (filterBlockType === FilterBlockType.FORM || filterBlockType === FilterBlockType.COLLAPSE) { @@ -80,7 +80,8 @@ export const useSupportedBlocks = (filterBlockType: FilterBlockType) => { return ( fieldSchema['x-uid'] !== block.uid && (isSameCollection(block.collection, collection) || - getSupportFieldsByAssociation(getAllCollectionsInheritChain(collection.name), block)?.length || + getSupportFieldsByAssociation(getAllCollectionsInheritChain(collection.name, collection.dataSource), block) + ?.length || getSupportFieldsByForeignKey(collection, block)?.length) ); }); @@ -90,7 +91,7 @@ export const useSupportedBlocks = (filterBlockType: FilterBlockType) => { export const transformToFilter = ( values: Record, fieldSchema: Schema, - getCollectionJoinField: (name: string) => CollectionFieldOptions, + getCollectionJoinField: (name: string) => CollectionFieldOptions_deprecated, collectionName: string, ) => { const { operators } = findFilterOperators(fieldSchema); @@ -156,7 +157,7 @@ export const transformToFilter = ( }; export const useAssociatedFields = () => { - const { fields } = useCollection(); + const { fields } = useCollection_deprecated(); return fields.filter((field) => isAssocField(field)) || []; }; @@ -167,8 +168,8 @@ export const isAssocField = (field?: FieldOptions) => { ); }; -export const isSameCollection = (c1: Collection, c2: Collection) => { - return c1.name === c2.name; +export const isSameCollection = (c1: Collection_deprecated, c2: Collection_deprecated) => { + return c1.name === c2.name && c1.dataSource === c2.dataSource; }; export const useFilterAPI = () => { diff --git a/packages/core/client/src/formula/Result.tsx b/packages/core/client/src/formula/Result.tsx index e5cd17f1c..f64687ab3 100644 --- a/packages/core/client/src/formula/Result.tsx +++ b/packages/core/client/src/formula/Result.tsx @@ -5,7 +5,7 @@ import cloneDeep from 'lodash/cloneDeep'; import * as math from 'mathjs'; import { isNumber } from 'mathjs'; import React, { useState } from 'react'; -import { useCollection } from '../collection-manager'; +import { useCollection_deprecated } from '../collection-manager'; const ReadPretty = (props) => { if (props?.options?.dataType !== 'string') { @@ -44,7 +44,7 @@ const Input = (props) => { export const Result = (props: any) => { const field = useField(); - const { getField } = useCollection(); + const { getField } = useCollection_deprecated(); const fieldSchema = useFieldSchema(); const options = getField(fieldSchema.name as string); return field.readPretty ? : ; diff --git a/packages/core/client/src/index.ts b/packages/core/client/src/index.ts index 3b9acfb3c..64bcbcdff 100644 --- a/packages/core/client/src/index.ts +++ b/packages/core/client/src/index.ts @@ -22,6 +22,7 @@ export * from './async-data-provider'; export * from './block-provider'; export * from './china-region'; export * from './collection-manager'; + export * from './common'; export * from './css-variable'; export * from './document-title'; @@ -49,3 +50,11 @@ export * from './system-settings'; export * from './testUtils'; export * from './user'; export * from './variables'; +export * from './data-source'; + +export { withDynamicSchemaProps } from './application/hoc/withDynamicSchemaProps'; + +export * from './modules/blocks/BlockSchemaToolbar'; +export * from './modules/blocks/data-blocks/table'; +export * from './modules/blocks/data-blocks/form'; +export * from './modules/blocks/data-blocks/table-selector'; diff --git a/packages/core/client/src/locale/en_US.json b/packages/core/client/src/locale/en_US.json index fc5780851..f57f11588 100644 --- a/packages/core/client/src/locale/en_US.json +++ b/packages/core/client/src/locale/en_US.json @@ -200,12 +200,12 @@ "Before change": "Before change", "After change": "After change", "Delete record": "Delete record", - "Delete collection":"Delete collection", + "Delete collection": "Delete collection", "Create collection": "Create collection", "Collection display name": "Collection display name", "Collection name": "Collection name", "Inherits": "Inherits", - "Generate ID field automatically": "Generate ID field automatically", + "Primary key, unique identifier, self growth": "Primary key, unique identifier, self growth", "Store the creation user of each record": "Store the creation user of each record", "Store the last update user of each record": "Store the last update user of each record", "Store the creation time of each record": "Store the creation time of each record", @@ -215,7 +215,7 @@ "Calendar collection": "Calendar collection", "General collection": "General collection", "Connect to database view": "Connect to database view", - "Sync from database":"Sync from database", + "Sync from database": "Sync from database", "Source collections": "Source collections", "Field source": "Field source", "Preview": "Preview", @@ -363,6 +363,8 @@ "Edit description": "Edit description", "Required": "Required", "Unique": "Unique", + "Primary": "Primary", + "Auto increment": "Auto increment", "Label field": "Label field", "Default is the ID field": "Default is the ID field", "Set default sorting rules": "Set default sorting rules", @@ -733,10 +735,10 @@ "Plugin name": "Plugin name", "Plugin tab name": "Plugin tab name", "AutoGenId": "Auto-generated ID field", - "CreatedBy": "Recording a row's created user", - "UpdatedBy": "Recording a row's last updated user", - "CreatedAt": "Recording a row's created time ", - "UpdatedAt": "Recording a row's last updated user", + "CreatedBy": "CreatedBy", + "UpdatedBy": "UpdatedBy", + "CreatedAt": "CreatedAt", + "UpdatedAt": "UpdatedAt", "Column width": "Column width", "Sortable": "Sortable", "Enable link": "Enable link", @@ -783,7 +785,23 @@ "Sign in with another account": "Sign in with another account", "Return to the main application": "Return to the main application", "Permission deined": "Permission denied", + "loading": "loading", + "name is required": "name is required", + "data source": "data source", + "DataSource": "DataSource", + "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.", + "Preset fields": "Preset fields", "Home page": "Home page", "User manual": "User manual", - "License": "License" + "License": "License", + "Generic properties": "Generic properties", + "Specific properties": "Specific properties", + "Used for drag and drop sorting scenarios, supporting grouping sorting": "Used for drag and drop sorting scenarios, supporting grouping sorting", + "Grouped sorting": "Grouped sorting", + "When a field is selected for grouping, it will be grouped first before sorting.": "When a field is selected for grouping, it will be grouped first before sorting.", + "Departments": "Departments", + "Main department": "Main department", + "Department name": "Department name", + "Superior department": "Superior department", + "Owners": "Owners" } diff --git a/packages/core/client/src/locale/es_ES.json b/packages/core/client/src/locale/es_ES.json index 5a768525e..f3f9be7db 100644 --- a/packages/core/client/src/locale/es_ES.json +++ b/packages/core/client/src/locale/es_ES.json @@ -184,7 +184,7 @@ "Collection display name": "Nombre para mostrar colección", "Collection name": "Nombre de la colección", "Inherits": "Hereda", - "Generate ID field automatically": "Generar campo ID automáticamente", + "Primary key, unique identifier, self growth": "Primary key, unique identifier, self growth", "Store the creation user of each record": "Almacenar el usuario de creación de cada registro", "Store the last update user of each record": "Almacenar el usuario de última actualización de cada registro", "Store the creation time of each record": "Almacenar la hora de creación de cada registro", @@ -732,6 +732,11 @@ "Data template": "Plantilla de datos", "Not found": "No encontrado", "Add": "Añadir", + "loading": "cargando", + "name is required": "el nombre es obligatorio", + "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "El {{type}} \"{{name}}\" puede haber sido eliminado. Por favor, elimine este {{blockType}}.", + "data source": "fuente de datos", + "DataSource": "Fuente de datos", "Home page": "Página de inicio", "User manual": "Manual de usuario", "License": "Licencia" diff --git a/packages/core/client/src/locale/fr_FR.json b/packages/core/client/src/locale/fr_FR.json index 7c7e87d73..bfda5b47e 100644 --- a/packages/core/client/src/locale/fr_FR.json +++ b/packages/core/client/src/locale/fr_FR.json @@ -195,7 +195,7 @@ "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", + "Primary key, unique identifier, self growth": "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", @@ -751,6 +751,11 @@ "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", "Allow add new":"Autoriser les ajouts", + "loading": "chargement", + "name is required": "le nom est requis", + "data source": "source de données", + "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "Le {{type}} \"{{name}}\" a peut-être été supprimé. Veuillez supprimer ce {{blockType}}.", + "DataSource": "Source de données", "Allow selection of existing records":"Permet de sélectionner des données existantes", "Home page": "Page d'accueil", "User manual": "Manuel de l'utilisateur", diff --git a/packages/core/client/src/locale/ja_JP.json b/packages/core/client/src/locale/ja_JP.json index 21a2087ed..6f4f65b5c 100644 --- a/packages/core/client/src/locale/ja_JP.json +++ b/packages/core/client/src/locale/ja_JP.json @@ -670,7 +670,12 @@ "Plugin loading failed. Please check the server logs.": "プラグインのロードに失敗しました。サーバーログを確認してください。", "Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects": "テーブルに依存するオブジェクト、およびそれらに依存するオブジェクトを自動的に削除する", "Allow add new":"新規作成を許可", - "Allow selection of existing records":"既存のデータの選択を許可", + "Allow selection of existing records": "既存のデータの選択を許可", + "loading": "ロード中", + "name is required": "名前は必須です", + "data source": "データソース", + "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" は削除されている可能性があります。この {{blockType}} を削除してください。", + "DataSource": "データソース", "Home page": "ホームページ", "User manual": "ユーザーマニュアル", "License": "ライセンス" diff --git a/packages/core/client/src/locale/ko_KR.json b/packages/core/client/src/locale/ko_KR.json index cee480711..0bef99df7 100644 --- a/packages/core/client/src/locale/ko_KR.json +++ b/packages/core/client/src/locale/ko_KR.json @@ -214,7 +214,7 @@ "Collection display name": "데이터 테이블 표시 이름", "Collection name": "데이터 테이블 이름", "Inherits": "상속", - "Generate ID field automatically": "ID 필드 자동 생성", + "Primary key, unique identifier, self growth": "기본 키, 고유 ID, 증가", "Store the creation user of each record": "각 레코드의 생성 사용자 저장", "Store the last update user of each record": "각 레코드의 마지막 업데이트 사용자 저장", "Store the creation time of each record": "각 레코드의 생성 시간 저장", @@ -843,6 +843,11 @@ "Permission denied": "허가 거부", "Allow add new": "신규 추가 허용", "Allow selection of existing records": "기존 레코드 선택 허용", + "loading": "로드 중", + "name is required": "이름은 필수입니다", + "data source": "데이터 소스", + "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\"이(가) 삭제되었을 수 있습니다. 이 {{blockType}}을(를) 제거하십시오.", + "DataSource": "데이터 소스", "Home page": "홈페이지", "User manual": "사용자 매뉴얼", "License": "라이선스" diff --git a/packages/core/client/src/locale/pt_BR.json b/packages/core/client/src/locale/pt_BR.json index 1187469a7..d50970b03 100644 --- a/packages/core/client/src/locale/pt_BR.json +++ b/packages/core/client/src/locale/pt_BR.json @@ -147,7 +147,7 @@ "Collection display name": "Nome para exibição da coleção", "Collection name": "Nome da coleção", "Inherits": "Herdado", - "Generate ID field automatically": "Gerar campo de ID automaticamente", + "Primary key, unique identifier, self growth": "Chave primária, identificador único, crescimento próprio", "Store the creation user of each record": "Armazenar o usuário criador de cada registro", "Store the last update user of each record": "Armazenar o último usuário que atualizou cada registro", "Store the creation time of each record": "Armazenar a hora de criação de cada registro", @@ -711,6 +711,11 @@ "Add": "Adicionar", "Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects": "Excluir automaticamente objetos que dependem desta tabela, bem como objetos que dependem desses objetos", "Allow add new":"Permitir novas adições", + "loading": "carregando", + "name is required": "nome é obrigatório", + "data source": "fonte de dados", + "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "O {{type}} \"{{name}}\" pode ter sido excluído. Por favor, remova este {{blockType}}.", + "DataSource": "Fonte de dados", "Allow selection of existing records":"Permitir a selecção dos registos existentes", "Home page": "Página inicial", "User manual": "Manual do usuário", diff --git a/packages/core/client/src/locale/ru_RU.json b/packages/core/client/src/locale/ru_RU.json index f7d6ccc03..eb77cd291 100644 --- a/packages/core/client/src/locale/ru_RU.json +++ b/packages/core/client/src/locale/ru_RU.json @@ -547,7 +547,12 @@ "Search plugin": "Поиск плагина", "Author": "Автор", "Plugin loading failed. Please check the server logs.": "Не удалось загрузить плагин. Пожалуйста, проверьте журналы сервера.", + "loading": "загрузка", + "name is required": "имя обязательно", + "data source": "источник данных", + "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" возможно был удален. Пожалуйста, удалите этот {{blockType}}.", + "DataSource": "Источник данных", "Home page": "Домашняя страница", "User manual": "Руководство пользователя", "License": "Лицензия" -} +} \ No newline at end of file diff --git a/packages/core/client/src/locale/tr_TR.json b/packages/core/client/src/locale/tr_TR.json index 45c6fc277..80805d458 100644 --- a/packages/core/client/src/locale/tr_TR.json +++ b/packages/core/client/src/locale/tr_TR.json @@ -544,7 +544,12 @@ "Search plugin": "Eklenti ara", "Author": "Yazar", "Plugin loading failed. Please check the server logs.": "Eklenti yüklenemedi. Lütfen sunucu günlüklerini kontrol edin.", + "loading": "yükleniyor", + "name is required": "ad gereklidir", + "data source": "veri kaynağı", + "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" silinmiş olabilir. Lütfen bu {{blockType}}'yi kaldırın.", + "DataSource": "Veri Kaynağı", "Home page": "Anasayfa", "User manual": "Kullanıcı kılavuzu", "License": "Lisans" -} +} \ No newline at end of file diff --git a/packages/core/client/src/locale/uk_UA.json b/packages/core/client/src/locale/uk_UA.json index 5c6cdc192..79b37428f 100644 --- a/packages/core/client/src/locale/uk_UA.json +++ b/packages/core/client/src/locale/uk_UA.json @@ -195,7 +195,7 @@ "Collection display name": "Відображуване ім'я колекції", "Collection name": "Ім'я колекції", "Inherits": "Наслідує", - "Generate ID field automatically": "Автоматично генерувати поле ID", + "Primary key, unique identifier, self growth": "Основний ключ, унікальний ідентифікатор, самовирост", "Store the creation user of each record": "Зберігати користувача, який створив кожний запис", "Store the last update user of each record": "Зберігати користувача останнього оновлення кожного запису", "Store the creation time of each record": "Зберігати час створення кожного запису", @@ -752,6 +752,11 @@ "Try again": "Спробуйте ще раз", "Data template": "Шаблон даних", "Template fields have been removed and need to be reconfigured": "Поля шаблону були видалені і потребують повторної конфігурації", + "loading": "завантаження", + "name is required": "ім'я обов'язкове", + "data source": "джерело даних", + "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" може бути видалено. Будь ласка, видаліть цей {{blockType}}.", + "DataSource": "Джерело даних", "Home page": "Домашня сторінка", "User manual": "Посібник користувача", "License": "Ліцензія" diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json index 09c8965af..3d6e324be 100644 --- a/packages/core/client/src/locale/zh-CN.json +++ b/packages/core/client/src/locale/zh-CN.json @@ -215,7 +215,7 @@ "Collection display name": "数据表名称", "Collection name": "数据表标识", "Inherits": "继承", - "Generate ID field automatically": "自动生成 ID 字段", + "Primary key, unique identifier, self growth": "主键、唯一标识、自增长", "Store the creation user of each record": "记录创建人", "Store the last update user of each record": "记录最后更新人", "Store the creation time of each record": "记录创建时间", @@ -384,6 +384,8 @@ "Edit description": "编辑描述", "Required": "必填", "Unique": "不允许重复", + "Primary": "主键", + "Auto increment": "自动增长", "Label field": "标签字段", "Default is the ID field": "默认为 ID 字段", "Set default sorting rules": "设置排序规则", @@ -597,6 +599,7 @@ "Global action permissions": "全局操作权限", "General action permissions": "通用操作权限", "Plugin settings permissions": "插件配置权限", + "Data source permissions": "数据源权限", "Allow to desgin pages": "允许界面配置", "Allow to manage plugins": "允许管理插件", "Allow to configure plugins": "允许管理配置中心", @@ -673,6 +676,7 @@ "Done": "完成", "Sign up successfully, and automatically jump to the sign in page": "注册成功,即将跳转到登录页面", "ACL": "访问控制", + "Access control": "访问控制", "Collection manager": "数据表管理", "Plugin manager": "插件管理器", "Local": "本地", @@ -844,11 +848,22 @@ "Return to the main application": "返回主应用", "Permission denied": "没有权限", "Allow add new": "允许新增", + "loading": "加载中", + "name is required": "名称不能为空", + "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" 可能已被删除。请删除当前{{blockType}}.", + "data source": "数据源", + "DataSource": "数据源", + "Data model": "数据模型", + "Security": "认证与安全", + "Action": "操作", + "System": "系统管理", + "Other": "其他", "Allow selection of existing records": "允许选择已有数据", "Data Model": "数据模型", "Blocks": "区块", "Users & permissions": "用户和权限", "System management": "系统管理", + "Preset fields": "预置字段", "System & security": "系统和安全", "Workflow": "工作流", "Third party services": "第三方服务", @@ -860,5 +875,18 @@ "Logging and monitoring": "日志与监控", "Home page": "主页", "User manual": "用户手册", - "License": "许可证" + "License": "许可证", + "Generic properties": "通用属性", + "Specific properties": "特有属性", + "Used for drag and drop sorting scenarios, supporting grouping sorting": "用于拖拽排序场景,支持分组排序", + "Grouped sorting": "分组排序", + "When a field is selected for grouping, it will be grouped first before sorting.": "当选了某个字段作为分组时,将先分组再排序", + "Main": "主数据源", + "Index": "索引", + "Field values must be unique.": "字段值必须具备唯一性", + "Departments": "部门", + "Main department": "主属部门", + "Department name": "部门名称", + "Superior department": "上级部门", + "Owners": "负责人" } diff --git a/packages/core/client/src/locale/zh-TW.json b/packages/core/client/src/locale/zh-TW.json index 256fad0f2..ab479d9ff 100644 --- a/packages/core/client/src/locale/zh-TW.json +++ b/packages/core/client/src/locale/zh-TW.json @@ -214,7 +214,7 @@ "Collection display name": "資料表名稱", "Collection name": "資料表標識", "Inherits": "繼承", - "Generate ID field automatically": "自動生成 ID 欄位", + "Primary key, unique identifier, self growth": "主键、唯一标识、自增长", "Store the creation user of each record": "記錄建立者", "Store the last update user of each record": "記錄最後更新者", "Store the creation time of each record": "記錄建立時間", @@ -842,6 +842,7 @@ "Return to the main application": "返回主App", "Permission denied": "沒有權限", "Allow add new":"允許新增", + "The {{type}} \"{{name}}\" may have been deleted. Please remove this {{blockType}}.": "{{type}} \"{{name}}\" 可能已被刪除,請移除這個 {{blockType}}。", "Allow selection of existing records":"允許選擇已有資料", "Home page": "主頁", "User manual": "使用手冊", diff --git a/packages/core/client/src/modules/actions/ActionSchemaToolbar.tsx b/packages/core/client/src/modules/actions/ActionSchemaToolbar.tsx new file mode 100644 index 000000000..fa57d2b84 --- /dev/null +++ b/packages/core/client/src/modules/actions/ActionSchemaToolbar.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { SchemaToolbar } from '../../schema-settings/GeneralSchemaDesigner'; + +export const ActionSchemaToolbar = (props) => { + return ; +}; diff --git a/packages/core/client/src/schema-initializer/items/CreateChildInitializer.tsx b/packages/core/client/src/modules/actions/add-child/CreateChildInitializer.tsx similarity index 89% rename from packages/core/client/src/schema-initializer/items/CreateChildInitializer.tsx rename to packages/core/client/src/modules/actions/add-child/CreateChildInitializer.tsx index 718e90b7a..58a237a6f 100644 --- a/packages/core/client/src/schema-initializer/items/CreateChildInitializer.tsx +++ b/packages/core/client/src/modules/actions/add-child/CreateChildInitializer.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import { ActionInitializer } from './ActionInitializer'; +import { ActionInitializer } from '../../../schema-initializer/items/ActionInitializer'; export const CreateChildInitializer = (props) => { const schema = { type: 'void', title: '{{ t("Add child") }}', 'x-action': 'create', - 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:addChild', 'x-component': 'Action', 'x-visible': '{{treeTable}}', 'x-component-props': { diff --git a/packages/core/client/src/modules/actions/add-child/addChildActionSettings.tsx b/packages/core/client/src/modules/actions/add-child/addChildActionSettings.tsx new file mode 100644 index 000000000..6bd6ce06c --- /dev/null +++ b/packages/core/client/src/modules/actions/add-child/addChildActionSettings.tsx @@ -0,0 +1,44 @@ +import { useSchemaToolbar } from '../../../application'; +import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; +import { useCollection_deprecated } from '../../../collection-manager'; +import { ButtonEditor } from '../../../schema-component/antd/action/Action.Designer'; +import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items'; +import { SchemaSettingsLinkageRules } from '../../../schema-settings'; + +export const addChildActionSettings = new SchemaSettings({ + name: 'actionSettings:addChild', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + const { buttonEditorProps } = useSchemaToolbar(); + return buttonEditorProps; + }, + }, + { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const { name } = useCollection_deprecated(); + const { linkageRulesProps } = useSchemaToolbar(); + return { + ...linkageRulesProps, + collectionName: name, + }; + }, + }, + { + name: 'openMode', + Component: SchemaSettingOpenModeSchemaItems, + componentProps: { + openMode: true, + openSize: true, + }, + }, + { + name: 'delete', + type: 'remove', + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/items/CreateActionInitializer.tsx b/packages/core/client/src/modules/actions/add-new/CreateActionInitializer.tsx similarity index 87% rename from packages/core/client/src/schema-initializer/items/CreateActionInitializer.tsx rename to packages/core/client/src/modules/actions/add-new/CreateActionInitializer.tsx index ccb139b94..ad3eb2383 100644 --- a/packages/core/client/src/schema-initializer/items/CreateActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/add-new/CreateActionInitializer.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { ActionInitializer } from './ActionInitializer'; -import { useSchemaInitializerItem } from '../../application'; +import { useSchemaInitializerItem } from '../../../application'; +import { ActionInitializer } from '../../../schema-initializer/items/ActionInitializer'; export const CreateActionInitializer = () => { const schema = { @@ -8,7 +8,8 @@ export const CreateActionInitializer = () => { 'x-action': 'create', 'x-acl-action': 'create', title: "{{t('Add new')}}", - 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:addNew', 'x-component': 'Action', 'x-decorator': 'ACLActionProvider', 'x-component-props': { diff --git a/packages/core/client/src/modules/actions/add-new/addNewActionSettings.tsx b/packages/core/client/src/modules/actions/add-new/addNewActionSettings.tsx new file mode 100644 index 000000000..392ea6699 --- /dev/null +++ b/packages/core/client/src/modules/actions/add-new/addNewActionSettings.tsx @@ -0,0 +1,57 @@ +import { useMemo } from 'react'; +import { useSchemaToolbar } from '../../../application'; +import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; +import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer'; +import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items'; +import { SchemaSettingsEnableChildCollections } from '../../../schema-settings/SchemaSettings'; + +export const addNewActionSettings = new SchemaSettings({ + name: 'actionSettings:addNew', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + const { buttonEditorProps } = useSchemaToolbar(); + return buttonEditorProps; + }, + }, + { + name: 'openMode', + Component: SchemaSettingOpenModeSchemaItems, + componentProps: { + openMode: true, + openSize: true, + }, + }, + { + name: 'enableChildCollections', + Component: SchemaSettingsEnableChildCollections, + useComponentProps() { + const { name } = useCollection_deprecated(); + return { + collectionName: name, + }; + }, + useVisible() { + const { name } = useCollection_deprecated(); + const { getChildrenCollections } = useCollectionManager_deprecated(); + const isChildCollectionAction = useMemo( + () => getChildrenCollections(name).length > 0, + [getChildrenCollections, name], + ); + return isChildCollectionAction; + }, + }, + { + name: 'delete', + sort: 100, + Component: RemoveButton as any, + useComponentProps() { + const { removeButtonProps } = useSchemaToolbar(); + return removeButtonProps; + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/CreateFormBlockInitializers.tsx b/packages/core/client/src/modules/actions/add-new/createFormBlockInitializers.tsx similarity index 81% rename from packages/core/client/src/schema-initializer/buttons/CreateFormBlockInitializers.tsx rename to packages/core/client/src/modules/actions/add-new/createFormBlockInitializers.tsx index e03027eae..17da54b81 100644 --- a/packages/core/client/src/schema-initializer/buttons/CreateFormBlockInitializers.tsx +++ b/packages/core/client/src/modules/actions/add-new/createFormBlockInitializers.tsx @@ -1,5 +1,5 @@ -import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; -import { gridRowColWrap } from '../utils'; +import { SchemaInitializer } from '../../../application/schema-initializer/SchemaInitializer'; +import { gridRowColWrap } from '../../../schema-initializer/utils'; export const createFormBlockInitializers = new SchemaInitializer({ name: 'CreateFormBlockInitializers', diff --git a/packages/core/client/src/schema-initializer/items/CustomizeAddRecordActionInitializer.tsx b/packages/core/client/src/modules/actions/add-record/CustomizeAddRecordActionInitializer.tsx similarity index 87% rename from packages/core/client/src/schema-initializer/items/CustomizeAddRecordActionInitializer.tsx rename to packages/core/client/src/modules/actions/add-record/CustomizeAddRecordActionInitializer.tsx index fc430d3e5..765424a8f 100644 --- a/packages/core/client/src/schema-initializer/items/CustomizeAddRecordActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/add-record/CustomizeAddRecordActionInitializer.tsx @@ -1,12 +1,13 @@ import React from 'react'; -import { BlockInitializer } from './BlockInitializer'; -import { useSchemaInitializerItem } from '../../application'; +import { useSchemaInitializerItem } from '../../../application'; +import { BlockInitializer } from '../../../schema-initializer/items/BlockInitializer'; export const CustomizeAddRecordActionInitializer = () => { const schema = { type: 'void', title: '{{t("Add record")}}', - 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:addRecord', 'x-component': 'Action', 'x-action': 'customize:create', 'x-component-props': { diff --git a/packages/core/client/src/modules/actions/add-record/customizeAddRecordActionSettings.tsx b/packages/core/client/src/modules/actions/add-record/customizeAddRecordActionSettings.tsx new file mode 100644 index 000000000..161621e35 --- /dev/null +++ b/packages/core/client/src/modules/actions/add-record/customizeAddRecordActionSettings.tsx @@ -0,0 +1,35 @@ +import { useSchemaToolbar } from '../../../application'; +import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; +import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer'; +import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items'; + +export const customizeAddRecordActionSettings = new SchemaSettings({ + name: 'actionSettings:addRecord', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + const { buttonEditorProps } = useSchemaToolbar(); + return buttonEditorProps; + }, + }, + { + name: 'openMode', + Component: SchemaSettingOpenModeSchemaItems, + componentProps: { + openMode: true, + openSize: true, + }, + }, + { + name: 'delete', + sort: 100, + Component: RemoveButton as any, + useComponentProps() { + const { removeButtonProps } = useSchemaToolbar(); + return removeButtonProps; + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/CusomeizeCreateFormBlockInitializers.tsx b/packages/core/client/src/modules/actions/add-record/customizeCreateFormBlockInitializers.tsx similarity index 75% rename from packages/core/client/src/schema-initializer/buttons/CusomeizeCreateFormBlockInitializers.tsx rename to packages/core/client/src/modules/actions/add-record/customizeCreateFormBlockInitializers.tsx index 54f358f9e..ba20a66ad 100644 --- a/packages/core/client/src/schema-initializer/buttons/CusomeizeCreateFormBlockInitializers.tsx +++ b/packages/core/client/src/modules/actions/add-record/customizeCreateFormBlockInitializers.tsx @@ -1,7 +1,7 @@ -import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; -import { gridRowColWrap } from '../utils'; +import { SchemaInitializer } from '../../../application/schema-initializer/SchemaInitializer'; +import { gridRowColWrap } from '../../../schema-initializer/utils'; -export const cusomeizeCreateFormBlockInitializers = new SchemaInitializer({ +export const customizeCreateFormBlockInitializers = new SchemaInitializer({ name: 'CusomeizeCreateFormBlockInitializers', wrap: gridRowColWrap, title: '{{t("Add block")}}', diff --git a/packages/core/client/src/schema-initializer/items/BulkDestroyActionInitializer.tsx b/packages/core/client/src/modules/actions/bulk-destroy/BulkDestroyActionInitializer.tsx similarity index 71% rename from packages/core/client/src/schema-initializer/items/BulkDestroyActionInitializer.tsx rename to packages/core/client/src/modules/actions/bulk-destroy/BulkDestroyActionInitializer.tsx index c8a3245e2..96158d885 100644 --- a/packages/core/client/src/schema-initializer/items/BulkDestroyActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/bulk-destroy/BulkDestroyActionInitializer.tsx @@ -1,15 +1,16 @@ import React from 'react'; -import { useCollection } from '../../collection-manager'; +import { useCollection_deprecated } from '../../../collection-manager'; -import { ActionInitializer } from './ActionInitializer'; +import { ActionInitializer } from '../../../schema-initializer/items/ActionInitializer'; export const BulkDestroyActionInitializer = (props) => { - const collection = useCollection(); + const collection = useCollection_deprecated(); const schema = { title: '{{ t("Delete") }}', 'x-action': 'destroy', 'x-component': 'Action', - 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:bulkDelete', 'x-decorator': 'ACLActionProvider', 'x-acl-action-props': { skipScopeCheck: true, diff --git a/packages/core/client/src/modules/actions/bulk-destroy/bulkDeleteActionSettings.tsx b/packages/core/client/src/modules/actions/bulk-destroy/bulkDeleteActionSettings.tsx new file mode 100644 index 000000000..698726ca8 --- /dev/null +++ b/packages/core/client/src/modules/actions/bulk-destroy/bulkDeleteActionSettings.tsx @@ -0,0 +1,30 @@ +import { useSchemaToolbar } from '../../../application'; +import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; +import { ButtonEditor, RemoveButton, SecondConFirm } from '../../../schema-component/antd/action/Action.Designer'; + +export const bulkDeleteActionSettings = new SchemaSettings({ + name: 'actionSettings:bulkDelete', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + const { buttonEditorProps } = useSchemaToolbar(); + return buttonEditorProps; + }, + }, + { + name: 'secondConFirm', + Component: SecondConFirm, + }, + { + name: 'remove', + sort: 100, + Component: RemoveButton as any, + useComponentProps() { + const { removeButtonProps } = useSchemaToolbar(); + return removeButtonProps; + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/items/DestroyActionInitializer.tsx b/packages/core/client/src/modules/actions/delete/DestroyActionInitializer.tsx similarity index 76% rename from packages/core/client/src/schema-initializer/items/DestroyActionInitializer.tsx rename to packages/core/client/src/modules/actions/delete/DestroyActionInitializer.tsx index 6afcd0566..c1ae72bfa 100644 --- a/packages/core/client/src/schema-initializer/items/DestroyActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/delete/DestroyActionInitializer.tsx @@ -1,13 +1,14 @@ import React from 'react'; -import { ActionInitializer } from './ActionInitializer'; +import { ActionInitializer } from '../../../schema-initializer/items/ActionInitializer'; export const DestroyActionInitializer = (props) => { const schema = { title: '{{ t("Delete") }}', 'x-action': 'destroy', 'x-component': 'Action', - 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:delete', 'x-component-props': { icon: 'DeleteOutlined', confirm: { diff --git a/packages/core/client/src/modules/actions/delete/deleteActionSettings.tsx b/packages/core/client/src/modules/actions/delete/deleteActionSettings.tsx new file mode 100644 index 000000000..fd3fb8a2e --- /dev/null +++ b/packages/core/client/src/modules/actions/delete/deleteActionSettings.tsx @@ -0,0 +1,39 @@ +import { useSchemaToolbar } from '../../../application'; +import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; +import { useCollection_deprecated } from '../../../collection-manager'; +import { ButtonEditor, SecondConFirm } from '../../../schema-component/antd/action/Action.Designer'; +import { SchemaSettingsLinkageRules } from '../../../schema-settings'; + +export const deleteActionSettings = new SchemaSettings({ + name: 'actionSettings:delete', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + const { buttonEditorProps } = useSchemaToolbar(); + return buttonEditorProps; + }, + }, + { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const { name } = useCollection_deprecated(); + const { linkageRulesProps } = useSchemaToolbar(); + return { + ...linkageRulesProps, + collectionName: name, + }; + }, + }, + { + name: 'secondConFirm', + Component: SecondConFirm, + }, + { + name: 'delete', + type: 'remove', + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/items/ExpandActionInitializer.tsx b/packages/core/client/src/modules/actions/expand-collapse/ExpandableActionInitializer.tsx similarity index 68% rename from packages/core/client/src/schema-initializer/items/ExpandActionInitializer.tsx rename to packages/core/client/src/modules/actions/expand-collapse/ExpandableActionInitializer.tsx index 3002d6276..cf1fcfd81 100644 --- a/packages/core/client/src/schema-initializer/items/ExpandActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/expand-collapse/ExpandableActionInitializer.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import { ActionInitializer } from './ActionInitializer'; +import { ActionInitializer } from '../../../schema-initializer/items/ActionInitializer'; -export const ExpandActionInitializer = (props) => { +export const ExpandableActionInitializer = (props) => { const schema = { 'x-action': 'expandAll', 'x-component': 'Action', - 'x-designer': 'Expand.Action.Design', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:expendable', 'x-component-props': { titleExpand: "{{t('Expand all')}}", titleCollapse: "{{t('Collapse all')}}", diff --git a/packages/core/client/src/modules/actions/expand-collapse/expendableActionSettings.tsx b/packages/core/client/src/modules/actions/expand-collapse/expendableActionSettings.tsx new file mode 100644 index 000000000..7d2274a5a --- /dev/null +++ b/packages/core/client/src/modules/actions/expand-collapse/expendableActionSettings.tsx @@ -0,0 +1,114 @@ +import { ISchema, useField, useFieldSchema } from '@formily/react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDesignable } from '../../..'; +import { useSchemaToolbar } from '../../../application'; +import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; +import { RemoveButton } from '../../../schema-component/antd/action/Action.Designer'; +import { SchemaSettingsModalItem } from '../../../schema-settings'; + +function ButtonEditor() { + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn } = useDesignable(); + const { t } = useTranslation(); + const { titleExpand, titleCollapse, iconExpand, iconCollapse } = fieldSchema['x-component-props'] || {}; + return ( + { + fieldSchema.title = t('Expand/Collapse'); + field.title = t('Expand/Collapse'); + field.componentProps.icon = iconExpand; + field.componentProps.danger = type === 'danger'; + field.componentProps.type = type; + fieldSchema['x-component-props'] = { + ...(fieldSchema['x-component-props'] || {}), + titleExpand, + titleCollapse, + iconExpand, + iconCollapse, + type, + danger: type === 'danger', + }; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + 'x-component-props': fieldSchema['x-component-props'], + title: fieldSchema.title, + }, + }); + dn.refresh(); + }} + /> + ); +} +export const expendableActionSettings = new SchemaSettings({ + name: 'actionSettings:expendable', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + const { buttonEditorProps } = useSchemaToolbar(); + return buttonEditorProps; + }, + }, + { + name: 'remove', + sort: 100, + Component: RemoveButton as any, + useComponentProps() { + const { removeButtonProps } = useSchemaToolbar(); + return removeButtonProps; + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/items/FilterActionInitializer.tsx b/packages/core/client/src/modules/actions/filter/FilterActionInitializer.tsx similarity index 69% rename from packages/core/client/src/schema-initializer/items/FilterActionInitializer.tsx rename to packages/core/client/src/modules/actions/filter/FilterActionInitializer.tsx index ecbdbf00d..3d93a3935 100644 --- a/packages/core/client/src/schema-initializer/items/FilterActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/filter/FilterActionInitializer.tsx @@ -1,13 +1,14 @@ import React from 'react'; -import { ActionInitializer } from './ActionInitializer'; +import { ActionInitializer } from '../../../schema-initializer/items/ActionInitializer'; export const FilterActionInitializer = (props) => { const schema = { type: 'void', title: '{{ t("Filter") }}', 'x-action': 'filter', - 'x-designer': 'Filter.Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:filter', 'x-component': 'Filter.Action', 'x-component-props': { icon: 'FilterOutlined', diff --git a/packages/core/client/src/modules/actions/filter/filterActionSettings.tsx b/packages/core/client/src/modules/actions/filter/filterActionSettings.tsx new file mode 100644 index 000000000..a57248b94 --- /dev/null +++ b/packages/core/client/src/modules/actions/filter/filterActionSettings.tsx @@ -0,0 +1,84 @@ +import { ISchema, useField, useFieldSchema } from '@formily/react'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; +import { FilterableFieldsSchemaSettingsItem } from '../../../schema-component/antd/filter/Filter.Action.Designer'; +import { useDesignable } from '../../../schema-component/hooks'; + +export const filterActionSettings = new SchemaSettings({ + name: 'actionSettings:filter', + items: [ + { + name: 'FilterableFields', + Component: FilterableFieldsSchemaSettingsItem, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'EditButton', + type: 'modal', + useComponentProps() { + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn } = useDesignable(); + const { t } = useTranslation(); + + return { + title: t('Edit button'), + schema: { + type: 'object', + title: t('Edit button'), + properties: { + title: { + 'x-decorator': 'FormItem', + 'x-component': 'Input', + title: t('Button title'), + default: fieldSchema.title, + 'x-component-props': {}, + }, + icon: { + 'x-decorator': 'FormItem', + 'x-component': 'IconPicker', + title: t('Button icon'), + default: fieldSchema?.['x-component-props']?.icon, + 'x-component-props': {}, + }, + }, + } as ISchema, + onSubmit: ({ title, icon }) => { + fieldSchema.title = title; + field.title = title; + field.componentProps.icon = icon; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props'].icon = icon; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + title, + 'x-component-props': { + ...fieldSchema['x-component-props'], + }, + }, + }); + dn.refresh(); + }, + }; + }, + }, + { + name: 'divider2', + type: 'divider', + }, + { + name: 'delete', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + breakRemoveOn: (s) => { + return s['x-component'] === 'Space' || s['x-component'] === 'ActionBar'; + }, + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/items/RefreshActionInitializer.tsx b/packages/core/client/src/modules/actions/refresh/RefreshActionInitializer.tsx similarity index 67% rename from packages/core/client/src/schema-initializer/items/RefreshActionInitializer.tsx rename to packages/core/client/src/modules/actions/refresh/RefreshActionInitializer.tsx index 3759c753a..e3548838d 100644 --- a/packages/core/client/src/schema-initializer/items/RefreshActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/refresh/RefreshActionInitializer.tsx @@ -1,12 +1,13 @@ import React from 'react'; -import { ActionInitializer } from './ActionInitializer'; +import { ActionInitializer } from '../../../schema-initializer/items/ActionInitializer'; export const RefreshActionInitializer = (props) => { const schema = { title: '{{ t("Refresh") }}', 'x-action': 'refresh', 'x-component': 'Action', - 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:refresh', 'x-component-props': { icon: 'ReloadOutlined', useProps: '{{ useRefreshActionProps }}', diff --git a/packages/core/client/src/modules/actions/refresh/refreshActionSettings.tsx b/packages/core/client/src/modules/actions/refresh/refreshActionSettings.tsx new file mode 100644 index 000000000..0c22b3333 --- /dev/null +++ b/packages/core/client/src/modules/actions/refresh/refreshActionSettings.tsx @@ -0,0 +1,30 @@ +import { useSchemaToolbar } from '../../../application'; +import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; +import { ButtonEditor, RemoveButton, SecondConFirm } from '../../../schema-component/antd/action/Action.Designer'; + +export const refreshActionSettings = new SchemaSettings({ + name: 'actionSettings:refresh', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + const { buttonEditorProps } = useSchemaToolbar(); + return buttonEditorProps; + }, + }, + { + name: 'secondConFirm', + Component: SecondConFirm, + }, + { + name: 'delete', + sort: 100, + Component: RemoveButton as any, + useComponentProps() { + const { removeButtonProps } = useSchemaToolbar(); + return removeButtonProps; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/actions/save-record/SaveRecordActionInitializer.tsx b/packages/core/client/src/modules/actions/save-record/SaveRecordActionInitializer.tsx new file mode 100644 index 000000000..22c3356d0 --- /dev/null +++ b/packages/core/client/src/modules/actions/save-record/SaveRecordActionInitializer.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { BlockInitializer } from '../../../schema-initializer/items'; +import { useSchemaInitializerItem } from '../../../application'; + +export const SaveRecordActionInitializer = () => { + const schema = { + title: '{{ t("Save record") }}', + 'x-action': 'customize:save', + 'x-component': 'Action', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:saveRecord', + 'x-designer-props': { + modalTip: + '{{ t("When the button is clicked, the following fields will be assigned and saved together with the fields in the form. If there are overlapping fields, the value here will overwrite the value in the form.") }}', + }, + 'x-action-settings': { + assignedValues: {}, + skipValidator: false, + onSuccess: { + manualClose: true, + redirecting: false, + successMessage: '{{t("Submitted successfully")}}', + }, + triggerWorkflows: [], + }, + 'x-component-props': { + useProps: '{{ useCreateActionProps }}', + }, + }; + + const itemConfig = useSchemaInitializerItem(); + return ; +}; diff --git a/packages/core/client/src/modules/actions/save-record/customizeSaveRecordActionSettings.tsx b/packages/core/client/src/modules/actions/save-record/customizeSaveRecordActionSettings.tsx new file mode 100644 index 000000000..0c5762131 --- /dev/null +++ b/packages/core/client/src/modules/actions/save-record/customizeSaveRecordActionSettings.tsx @@ -0,0 +1,60 @@ +import { useFieldSchema } from '@formily/react'; +import { isValid } from '@formily/shared'; +import { useSchemaToolbar } from '../../../application'; +import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; +import { + AfterSuccess, + AssignedFieldValues, + ButtonEditor, + RemoveButton, + SecondConFirm, + SkipValidation, + WorkflowConfig, +} from '../../../schema-component/antd/action/Action.Designer'; + +export const customizeSaveRecordActionSettings = new SchemaSettings({ + name: 'actionSettings:saveRecord', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + const { buttonEditorProps } = useSchemaToolbar(); + return buttonEditorProps; + }, + }, + { + name: 'secondConfirmation', + Component: SecondConFirm, + }, + { + name: 'assignFieldValues', + Component: AssignedFieldValues, + }, + { + name: 'skipRequiredValidation', + Component: SkipValidation, + }, + { + name: 'afterSuccessfulSubmission', + Component: AfterSuccess, + useVisible() { + const fieldSchema = useFieldSchema(); + return isValid(fieldSchema?.['x-action-settings']?.onSuccess); + }, + }, + { + name: 'bindWorkflow', + Component: WorkflowConfig, + }, + { + name: 'delete', + sort: 100, + Component: RemoveButton as any, + useComponentProps() { + const { removeButtonProps } = useSchemaToolbar(); + return removeButtonProps; + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/items/CreateSubmitActionInitializer.tsx b/packages/core/client/src/modules/actions/submit/CreateSubmitActionInitializer.tsx similarity index 71% rename from packages/core/client/src/schema-initializer/items/CreateSubmitActionInitializer.tsx rename to packages/core/client/src/modules/actions/submit/CreateSubmitActionInitializer.tsx index d7dd564a2..e233ab9f2 100644 --- a/packages/core/client/src/schema-initializer/items/CreateSubmitActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/submit/CreateSubmitActionInitializer.tsx @@ -1,12 +1,13 @@ import React from 'react'; -import { ActionInitializer } from './ActionInitializer'; +import { ActionInitializer } from '../../../schema-initializer/items/ActionInitializer'; export const CreateSubmitActionInitializer = (props) => { const schema = { title: '{{ t("Submit") }}', 'x-action': 'submit', 'x-component': 'Action', - 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:createSubmit', 'x-component-props': { type: 'primary', htmlType: 'submit', diff --git a/packages/core/client/src/schema-initializer/items/UpdateSubmitActionInitializer.tsx b/packages/core/client/src/modules/actions/submit/UpdateSubmitActionInitializer.tsx similarity index 67% rename from packages/core/client/src/schema-initializer/items/UpdateSubmitActionInitializer.tsx rename to packages/core/client/src/modules/actions/submit/UpdateSubmitActionInitializer.tsx index 11c4fe311..0bd0cb872 100644 --- a/packages/core/client/src/schema-initializer/items/UpdateSubmitActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/submit/UpdateSubmitActionInitializer.tsx @@ -1,13 +1,15 @@ import React from 'react'; -import { ActionInitializer } from './ActionInitializer'; +import { ActionInitializer } from '../../../schema-initializer/items/ActionInitializer'; export const UpdateSubmitActionInitializer = (props) => { const schema = { title: '{{ t("Submit") }}', 'x-action': 'submit', 'x-component': 'Action', - 'x-designer': 'Action.Designer', + // 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:updateSubmit', 'x-component-props': { type: 'primary', htmlType: 'submit', diff --git a/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx b/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx new file mode 100644 index 000000000..a7ec64efb --- /dev/null +++ b/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx @@ -0,0 +1,163 @@ +import { useFieldSchema, useField, connect, mapProps, ISchema } from '@formily/react'; +import { isValid } from '@formily/shared'; +import React, { useEffect, useState } from 'react'; +import { Tree as AntdTree } from 'antd'; +import { useSchemaToolbar } from '../../../application'; +import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; +import { + ButtonEditor, + RemoveButton, + SecondConFirm, + WorkflowConfig, +} from '../../../schema-component/antd/action/Action.Designer'; +import { useTranslation } from 'react-i18next'; +import { useDesignable } from '../../../schema-component'; +import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks/useCollectionState'; +import { useCollection_deprecated } from '../../../collection-manager'; +import { SchemaSettingsModalItem } from '../../../schema-settings/SchemaSettings'; + +const Tree = connect( + AntdTree, + mapProps((props, field: any) => { + useEffect(() => { + field.value = props.defaultCheckedKeys || []; + }, []); + const [checkedKeys, setCheckedKeys] = useState(props.defaultCheckedKeys || []); + const onCheck = (checkedKeys) => { + setCheckedKeys(checkedKeys); + field.value = checkedKeys; + }; + field.onCheck = onCheck; + return { + ...props, + checkedKeys, + onCheck, + }; + }), +); +export function SaveMode() { + const { dn } = useDesignable(); + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { name } = useCollection_deprecated(); + const { getEnableFieldTree, getOnLoadData } = useCollectionState(name); + + return ( + { + field.componentProps.saveMode = saveMode; + field.componentProps.filterKeys = filterKeys; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props'].saveMode = saveMode; + fieldSchema['x-component-props'].filterKeys = filterKeys; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-component-props': { + ...fieldSchema['x-component-props'], + }, + }, + }); + dn.refresh(); + }} + /> + ); +} +export const createSubmitActionSettings = new SchemaSettings({ + name: 'actionSettings:createSubmit', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + const { buttonEditorProps } = useSchemaToolbar(); + return buttonEditorProps; + }, + }, + { + name: 'secondConfirmation', + Component: SecondConFirm, + }, + { + name: 'workflowConfig', + Component: WorkflowConfig, + useVisible() { + const fieldSchema = useFieldSchema(); + return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows); + }, + }, + { + name: 'saveMode', + Component: SaveMode, + }, + { + name: 'remove', + sort: 100, + Component: RemoveButton as any, + useComponentProps() { + const { removeButtonProps } = useSchemaToolbar(); + return removeButtonProps; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx b/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx new file mode 100644 index 000000000..f10e286d6 --- /dev/null +++ b/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx @@ -0,0 +1,93 @@ +import { useFieldSchema } from '@formily/react'; +import { isValid } from '@formily/shared'; +import { useSchemaToolbar } from '../../../application'; +import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; +import { + ButtonEditor, + RemoveButton, + SecondConFirm, + WorkflowConfig, +} from '../../../schema-component/antd/action/Action.Designer'; +import { SaveMode } from './createSubmitActionSettings'; + +export const updateSubmitActionSettings = new SchemaSettings({ + name: 'actionSettings:updateSubmit', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + const { buttonEditorProps } = useSchemaToolbar(); + return buttonEditorProps; + }, + }, + { + name: 'secondConfirmation', + Component: SecondConFirm, + }, + { + name: 'workflowConfig', + Component: WorkflowConfig, + useVisible() { + const fieldSchema = useFieldSchema(); + return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows); + }, + }, + { + name: 'remove', + sort: 100, + Component: RemoveButton as any, + useComponentProps() { + const { removeButtonProps } = useSchemaToolbar(); + return removeButtonProps; + }, + }, + ], +}); + +// TODO: 0.20 发版之前后移除 +export const submitActionSettings = new SchemaSettings({ + name: 'actionSettings:submit', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + const { buttonEditorProps } = useSchemaToolbar(); + return buttonEditorProps; + }, + }, + { + name: 'secondConfirmation', + Component: SecondConFirm, + }, + { + name: 'workflowConfig', + Component: WorkflowConfig, + useVisible() { + const fieldSchema = useFieldSchema(); + return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows); + }, + }, + { + name: 'saveMode', + Component: SaveMode, + useVisible() { + const fieldSchema = useFieldSchema(); + return ( + fieldSchema['x-action'] === 'submit' && + fieldSchema.parent?.['x-initializer'] === 'CreateFormActionInitializers' + ); + }, + }, + { + name: 'remove', + sort: 100, + Component: RemoveButton as any, + useComponentProps() { + const { removeButtonProps } = useSchemaToolbar(); + return removeButtonProps; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/actions/update-record/UpdateRecordActionInitializer.tsx b/packages/core/client/src/modules/actions/update-record/UpdateRecordActionInitializer.tsx new file mode 100644 index 000000000..0b099eefc --- /dev/null +++ b/packages/core/client/src/modules/actions/update-record/UpdateRecordActionInitializer.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { BlockInitializer } from '../../../schema-initializer/items'; +import { useSchemaInitializerItem } from '../../../application'; + +export const UpdateRecordActionInitializer = (props) => { + const schema = { + title: '{{ t("Update record") }}', + 'x-component': props?.['x-component'] || 'Action.Link', + 'x-action': 'customize:update', + 'x-decorator': 'ACLActionProvider', + 'x-acl-action': 'update', + // 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:updateRecord', + 'x-action-settings': { + assignedValues: {}, + onSuccess: { + manualClose: true, + redirecting: false, + successMessage: '{{t("Updated successfully")}}', + }, + }, + 'x-component-props': { + useProps: '{{ useCustomizeUpdateActionProps }}', + }, + }; + + const itemConfig = useSchemaInitializerItem(); + return ; +}; diff --git a/packages/core/client/src/modules/actions/update-record/customizeUpdateRecordActionSettings.tsx b/packages/core/client/src/modules/actions/update-record/customizeUpdateRecordActionSettings.tsx new file mode 100644 index 000000000..f5e1a9a94 --- /dev/null +++ b/packages/core/client/src/modules/actions/update-record/customizeUpdateRecordActionSettings.tsx @@ -0,0 +1,58 @@ +import { useSchemaToolbar } from '../../../application'; +import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; +import { useCollection_deprecated } from '../../../collection-manager'; +import { + AfterSuccess, + AssignedFieldValues, + ButtonEditor, + RemoveButton, + SecondConFirm, +} from '../../../schema-component/antd/action/Action.Designer'; +import { SchemaSettingsLinkageRules } from '../../../schema-settings'; + +export const customizeUpdateRecordActionSettings = new SchemaSettings({ + name: 'actionSettings:updateRecord', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + const { buttonEditorProps } = useSchemaToolbar(); + return buttonEditorProps; + }, + }, + { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const { name } = useCollection_deprecated(); + const { linkageRulesProps } = useSchemaToolbar(); + return { + ...linkageRulesProps, + collectionName: name, + }; + }, + }, + { + name: 'secondConFirm', + Component: SecondConFirm, + }, + { + name: 'assignFieldValues', + Component: AssignedFieldValues, + }, + { + name: 'afterSuccessfulSubmission', + Component: AfterSuccess, + }, + { + name: 'delete', + sort: 100, + Component: RemoveButton as any, + useComponentProps() { + const { removeButtonProps } = useSchemaToolbar(); + return removeButtonProps; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/actions/view-edit-popup/PopupActionInitializer.tsx b/packages/core/client/src/modules/actions/view-edit-popup/PopupActionInitializer.tsx new file mode 100644 index 000000000..4d4149586 --- /dev/null +++ b/packages/core/client/src/modules/actions/view-edit-popup/PopupActionInitializer.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { BlockInitializer } from '../../../schema-initializer/items'; +import { useSchemaInitializerItem } from '../../../application'; + +export const PopupActionInitializer = (props) => { + const schema = { + type: 'void', + title: '{{ t("Popup") }}', + 'x-action': 'customize:popup', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:popup', + 'x-component': props?.['x-component'] || 'Action.Link', + 'x-component-props': { + openMode: 'drawer', + }, + properties: { + drawer: { + type: 'void', + title: '{{ t("Popup") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'TabPaneInitializers', + properties: { + tab1: { + type: 'void', + title: '{{t("Details")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'RecordBlockInitializers', + properties: {}, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const itemConfig = useSchemaInitializerItem(); + return ; +}; diff --git a/packages/core/client/src/schema-initializer/buttons/RecordFormBlockInitializers.tsx b/packages/core/client/src/modules/actions/view-edit-popup/RecordFormBlockInitializers.tsx similarity index 81% rename from packages/core/client/src/schema-initializer/buttons/RecordFormBlockInitializers.tsx rename to packages/core/client/src/modules/actions/view-edit-popup/RecordFormBlockInitializers.tsx index 06ef0c4d9..034f9ffec 100644 --- a/packages/core/client/src/schema-initializer/buttons/RecordFormBlockInitializers.tsx +++ b/packages/core/client/src/modules/actions/view-edit-popup/RecordFormBlockInitializers.tsx @@ -1,5 +1,5 @@ -import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; -import { gridRowColWrap } from '../utils'; +import { SchemaInitializer } from '../../../application/schema-initializer/SchemaInitializer'; +import { gridRowColWrap } from '../../../schema-initializer/utils'; export const recordFormBlockInitializers = new SchemaInitializer({ name: 'RecordFormBlockInitializers', diff --git a/packages/core/client/src/schema-initializer/items/UpdateActionInitializer.tsx b/packages/core/client/src/modules/actions/view-edit-popup/UpdateActionInitializer.tsx similarity index 88% rename from packages/core/client/src/schema-initializer/items/UpdateActionInitializer.tsx rename to packages/core/client/src/modules/actions/view-edit-popup/UpdateActionInitializer.tsx index b4c526ee1..2ab32a4d9 100644 --- a/packages/core/client/src/schema-initializer/items/UpdateActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/view-edit-popup/UpdateActionInitializer.tsx @@ -1,13 +1,14 @@ import React from 'react'; -import { ActionInitializer } from './ActionInitializer'; +import { ActionInitializer } from '../../../schema-initializer/items/ActionInitializer'; export const UpdateActionInitializer = (props) => { const schema = { type: 'void', title: '{{ t("Edit") }}', 'x-action': 'update', - 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:edit', 'x-component': 'Action', 'x-component-props': { openMode: 'drawer', diff --git a/packages/core/client/src/schema-initializer/items/ViewActionInitializer.tsx b/packages/core/client/src/modules/actions/view-edit-popup/ViewActionInitializer.tsx similarity index 88% rename from packages/core/client/src/schema-initializer/items/ViewActionInitializer.tsx rename to packages/core/client/src/modules/actions/view-edit-popup/ViewActionInitializer.tsx index ba3dbca00..c091def8d 100644 --- a/packages/core/client/src/schema-initializer/items/ViewActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/view-edit-popup/ViewActionInitializer.tsx @@ -1,13 +1,14 @@ import React from 'react'; -import { ActionInitializer } from './ActionInitializer'; +import { ActionInitializer } from '../../../schema-initializer/items/ActionInitializer'; export const ViewActionInitializer = (props) => { const schema = { type: 'void', title: '{{ t("View") }}', 'x-action': 'view', - 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:view', 'x-component': 'Action', 'x-component-props': { openMode: 'drawer', diff --git a/packages/core/client/src/modules/actions/view-edit-popup/customizePopupActionSettings.tsx b/packages/core/client/src/modules/actions/view-edit-popup/customizePopupActionSettings.tsx new file mode 100644 index 000000000..c715097d4 --- /dev/null +++ b/packages/core/client/src/modules/actions/view-edit-popup/customizePopupActionSettings.tsx @@ -0,0 +1,49 @@ +import { useSchemaToolbar } from '../../../application'; +import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; +import { useCollection_deprecated } from '../../../collection-manager'; +import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer'; +import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items'; +import { SchemaSettingsLinkageRules } from '../../../schema-settings'; + +export const customizePopupActionSettings = new SchemaSettings({ + name: 'actionSettings:popup', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + const { buttonEditorProps } = useSchemaToolbar(); + return buttonEditorProps; + }, + }, + { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const { name } = useCollection_deprecated(); + const { linkageRulesProps } = useSchemaToolbar(); + return { + ...linkageRulesProps, + collectionName: name, + }; + }, + }, + { + name: 'openMode', + Component: SchemaSettingOpenModeSchemaItems, + componentProps: { + openMode: true, + openSize: true, + }, + }, + { + name: 'remove', + sort: 100, + Component: RemoveButton as any, + useComponentProps() { + const { removeButtonProps } = useSchemaToolbar(); + return removeButtonProps; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/actions/view-edit-popup/editActionSettings.tsx b/packages/core/client/src/modules/actions/view-edit-popup/editActionSettings.tsx new file mode 100644 index 000000000..ebb7dc747 --- /dev/null +++ b/packages/core/client/src/modules/actions/view-edit-popup/editActionSettings.tsx @@ -0,0 +1,44 @@ +import { useSchemaToolbar } from '../../../application'; +import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; +import { useCollection_deprecated } from '../../../collection-manager'; +import { ButtonEditor } from '../../../schema-component/antd/action/Action.Designer'; +import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items'; +import { SchemaSettingsLinkageRules } from '../../../schema-settings'; + +export const editActionSettings = new SchemaSettings({ + name: 'actionSettings:edit', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + const { buttonEditorProps } = useSchemaToolbar(); + return buttonEditorProps; + }, + }, + { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const { name } = useCollection_deprecated(); + const { linkageRulesProps } = useSchemaToolbar(); + return { + ...linkageRulesProps, + collectionName: name, + }; + }, + }, + { + name: 'openMode', + Component: SchemaSettingOpenModeSchemaItems, + componentProps: { + openMode: true, + openSize: true, + }, + }, + { + name: 'delete', + type: 'remove', + }, + ], +}); diff --git a/packages/core/client/src/modules/actions/view-edit-popup/viewActionSettings.tsx b/packages/core/client/src/modules/actions/view-edit-popup/viewActionSettings.tsx new file mode 100644 index 000000000..1f94d08d9 --- /dev/null +++ b/packages/core/client/src/modules/actions/view-edit-popup/viewActionSettings.tsx @@ -0,0 +1,49 @@ +import { useSchemaToolbar } from '../../../application'; +import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; +import { useCollection_deprecated } from '../../../collection-manager'; +import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer'; +import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items'; +import { SchemaSettingsLinkageRules } from '../../../schema-settings'; + +export const viewActionSettings = new SchemaSettings({ + name: 'actionSettings:view', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + const { buttonEditorProps } = useSchemaToolbar(); + return buttonEditorProps; + }, + }, + { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const { name } = useCollection_deprecated(); + const { linkageRulesProps } = useSchemaToolbar(); + return { + ...linkageRulesProps, + collectionName: name, + }; + }, + }, + { + name: 'openMode', + Component: SchemaSettingOpenModeSchemaItems, + componentProps: { + openMode: true, + openSize: true, + }, + }, + { + name: 'remove', + sort: 100, + Component: RemoveButton as any, + useComponentProps() { + const { removeButtonProps } = useSchemaToolbar(); + return removeButtonProps; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/blocks/BlockSchemaToolbar.tsx b/packages/core/client/src/modules/blocks/BlockSchemaToolbar.tsx new file mode 100644 index 000000000..2e1a1c97a --- /dev/null +++ b/packages/core/client/src/modules/blocks/BlockSchemaToolbar.tsx @@ -0,0 +1,19 @@ +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useCollection_deprecated } from '../../collection-manager'; +import { SchemaToolbar } from '../../schema-settings/GeneralSchemaDesigner'; +import { useSchemaTemplate } from '../../schema-templates'; + +export const BlockSchemaToolbar = (props) => { + const { t } = useTranslation(); + const { name, title } = useCollection_deprecated(); + const template = useSchemaTemplate(); + const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName) + ? `${template?.name} ${t('(Fields only)')}` + : template?.name; + const toolbarTitle = useMemo(() => { + return [title || name, templateName].filter(Boolean); + }, [name, templateName, title]); + + return ; +}; diff --git a/packages/core/client/src/schema-initializer/buttons/DetailsActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/details-multi/DetailsActionInitializers.tsx similarity index 90% rename from packages/core/client/src/schema-initializer/buttons/DetailsActionInitializers.tsx rename to packages/core/client/src/modules/blocks/data-blocks/details-multi/DetailsActionInitializers.tsx index c13ff6853..a6041fc06 100644 --- a/packages/core/client/src/schema-initializer/buttons/DetailsActionInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/DetailsActionInitializers.tsx @@ -1,4 +1,4 @@ -import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; // 表单的操作配置 export const detailsActionInitializers = new SchemaInitializer({ diff --git a/packages/core/client/src/schema-initializer/items/DetailsBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/details-multi/DetailsBlockInitializer.tsx similarity index 62% rename from packages/core/client/src/schema-initializer/items/DetailsBlockInitializer.tsx rename to packages/core/client/src/modules/blocks/data-blocks/details-multi/DetailsBlockInitializer.tsx index 29fb7991b..42c0684a5 100644 --- a/packages/core/client/src/schema-initializer/items/DetailsBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/DetailsBlockInitializer.tsx @@ -1,13 +1,13 @@ import { TableOutlined } from '@ant-design/icons'; import React from 'react'; -import { useCollectionManager } from '../../collection-manager'; -import { createDetailsBlockSchema } from '../utils'; -import { DataBlockInitializer } from './DataBlockInitializer'; -import { useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; +import { useCollectionManager_deprecated } from '../../../../collection-manager'; +import { createDetailsBlockSchema } from '../../../../schema-initializer/utils'; +import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer'; export const DetailsBlockInitializer = () => { const { insert } = useSchemaInitializer(); - const { getCollection } = useCollectionManager(); + const { getCollection } = useCollectionManager_deprecated(); const itemConfig = useSchemaInitializerItem(); return ( { icon={} componentType={'Details'} onCreateBlockSchema={async ({ item }) => { - const collection = getCollection(item.name); + const collection = getCollection(item.name, item.dataSource); const schema = createDetailsBlockSchema({ collection: item.name, + dataSource: item.dataSource, rowKey: collection.filterTargetKey || 'id', actionInitializers: (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql' && 'DetailsActionInitializers', + settings: 'blockSettings:multiDataDetails', }); insert(schema); }} diff --git a/packages/core/client/src/modules/details-multi-data/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaInitializer.test.ts similarity index 89% rename from packages/core/client/src/modules/details-multi-data/__e2e__/schemaInitializer.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaInitializer.test.ts index 8207c30cc..d3bd9cf36 100644 --- a/packages/core/client/src/modules/details-multi-data/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaInitializer.test.ts @@ -66,30 +66,24 @@ test.describe('configure actions', () => { await page.getByLabel('schema-initializer-ActionBar-DetailsActionInitializers-general').hover(); await page.getByRole('menuitem', { name: 'Edit' }).click(); await page.getByRole('menuitem', { name: 'Delete' }).click(); - await page.getByRole('menuitem', { name: 'Duplicate' }).click(); await expect(page.getByRole('menuitem', { name: 'Edit' }).getByRole('switch')).toBeChecked(); await expect(page.getByRole('menuitem', { name: 'Delete' }).getByRole('switch')).toBeChecked(); - await expect(page.getByRole('menuitem', { name: 'Duplicate' }).getByRole('switch')).toBeChecked(); await page.mouse.move(300, 0); await expect(page.getByRole('button', { name: 'Edit' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Delete' })).toBeVisible(); - await expect(page.getByRole('button', { name: 'Duplicate' })).toBeVisible(); // delete buttons await page.getByLabel('schema-initializer-ActionBar-DetailsActionInitializers-general').hover(); await page.getByRole('menuitem', { name: 'Edit' }).click(); await page.getByRole('menuitem', { name: 'Delete' }).click(); - await page.getByRole('menuitem', { name: 'Duplicate' }).click(); await expect(page.getByRole('menuitem', { name: 'Edit' }).getByRole('switch')).not.toBeChecked(); await expect(page.getByRole('menuitem', { name: 'Delete' }).getByRole('switch')).not.toBeChecked(); - await expect(page.getByRole('menuitem', { name: 'Duplicate' }).getByRole('switch')).not.toBeChecked(); await page.mouse.move(300, 0); await expect(page.getByRole('button', { name: 'Edit' })).not.toBeVisible(); await expect(page.getByRole('button', { name: 'Delete' })).not.toBeVisible(); - await expect(page.getByRole('button', { name: 'Duplicate' })).not.toBeVisible(); }); }); diff --git a/packages/core/client/src/modules/details-multi-data/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaSettings.test.ts similarity index 79% rename from packages/core/client/src/modules/details-multi-data/__e2e__/schemaSettings.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaSettings.test.ts index bf8567e0c..01f300347 100644 --- a/packages/core/client/src/modules/details-multi-data/__e2e__/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaSettings.test.ts @@ -32,11 +32,10 @@ test.describe('actions schema settings', () => { test('edit & delete & duplicate', async ({ page, mockPage }) => { await mockPage(oneEmptyDetailsBlock).goto(); - // 创建 Edit & Delete & Duplicate 三个按钮 + // 创建 Edit & Delete 两个按钮 await page.getByLabel('schema-initializer-ActionBar-DetailsActionInitializers-general').hover(); await page.getByRole('menuitem', { name: 'Edit' }).click(); await page.getByRole('menuitem', { name: 'Delete' }).click(); - await page.getByRole('menuitem', { name: 'Duplicate' }).click(); await page.mouse.move(0, 300); // Edit button settings --------------------------------------------------------------- @@ -58,15 +57,5 @@ test.describe('actions schema settings', () => { }, supportedOptions: ['Edit button', 'Linkage rules', 'Delete'], }); - - // Duplicate settings ---------------------------------------------------------------- - await expectSettingsMenu({ - page, - showMenu: async () => { - await page.getByRole('button', { name: 'Duplicate' }).hover(); - await page.getByRole('button', { name: 'designer-schema-settings-Action' }).hover(); - }, - supportedOptions: ['Edit button', 'Duplicate mode', 'Open mode', 'Popup size', 'Delete'], - }); }); }); diff --git a/packages/core/client/src/modules/collapse-filter/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/templatesOfBug.ts similarity index 100% rename from packages/core/client/src/modules/collapse-filter/__e2e__/templatesOfBug.ts rename to packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/templatesOfBug.ts diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-multi/multiDataDetailsBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/details-multi/multiDataDetailsBlockSettings.ts new file mode 100644 index 000000000..d2cd919c9 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/multiDataDetailsBlockSettings.ts @@ -0,0 +1,196 @@ +import { ArrayItems } from '@formily/antd-v5'; +import { ISchema, useField, useFieldSchema } from '@formily/react'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useFormBlockContext } from '../../../../block-provider'; +import { useDetailsBlockContext } from '../../../../block-provider/DetailsBlockProvider'; +import { useCollection_deprecated, useSortFields } from '../../../../collection-manager'; +import { removeNullCondition, useDesignable } from '../../../../schema-component'; +import { SchemaSettingsBlockTitleItem, SchemaSettingsTemplate } from '../../../../schema-settings'; +import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope'; + +export const multiDataDetailsBlockSettings = new SchemaSettings({ + name: 'blockSettings:multiDataDetails', + items: [ + { + name: 'title', + Component: SchemaSettingsBlockTitleItem, + }, + { + name: 'dataScope', + Component: SchemaSettingsDataScope, + useComponentProps() { + const { name } = useCollection_deprecated(); + const fieldSchema = useFieldSchema(); + const { form } = useFormBlockContext(); + const field = useField(); + const { service } = useDetailsBlockContext(); + const { dn } = useDesignable(); + return { + collectionName: name, + defaultFilter: fieldSchema?.['x-decorator-props']?.params?.filter || {}, + form, + onSubmit: ({ filter }) => { + filter = removeNullCondition(filter); + const params = field.decoratorProps.params || {}; + params.filter = filter; + field.decoratorProps.params = params; + fieldSchema['x-decorator-props']['params'] = params; + service.run({ ...service.params?.[0], filter }); + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }, + }; + }, + }, + { + name: 'sortingRules', + type: 'modal', + useComponentProps() { + const { name } = useCollection_deprecated(); + const { t } = useTranslation(); + const fieldSchema = useFieldSchema(); + const field = useField(); + const { service } = useDetailsBlockContext(); + const { dn } = useDesignable(); + const sortFields = useSortFields(name); + const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; + const sort = defaultSort?.map((item: string) => { + return item.startsWith('-') + ? { + field: item.substring(1), + direction: 'desc', + } + : { + field: item, + direction: 'asc', + }; + }); + return { + title: t('Set default sorting rules'), + components: { + ArrayItems, + }, + schema: { + type: 'object', + title: t('Set default sorting rules'), + properties: { + sort: { + type: 'array', + default: sort, + 'x-component': 'ArrayItems', + 'x-decorator': 'FormItem', + items: { + type: 'object', + properties: { + space: { + type: 'void', + 'x-component': 'Space', + properties: { + sort: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.SortHandle', + }, + field: { + type: 'string', + enum: sortFields, + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-component-props': { + style: { + width: 260, + }, + }, + }, + direction: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Radio.Group', + 'x-component-props': { + optionType: 'button', + }, + enum: [ + { + label: t('ASC'), + value: 'asc', + }, + { + label: t('DESC'), + value: 'desc', + }, + ], + }, + remove: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.Remove', + }, + }, + }, + }, + }, + properties: { + add: { + type: 'void', + title: t('Add sort field'), + 'x-component': 'ArrayItems.Addition', + }, + }, + }, + }, + } as ISchema, + onSubmit({ sort }) { + const sortArr = sort.map((item) => { + return item.direction === 'desc' ? `-${item.field}` : item.field; + }); + const params = field.decoratorProps.params || {}; + params.sort = sortArr; + field.decoratorProps.params = params; + fieldSchema['x-decorator-props']['params'] = params; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + service.run({ ...service.params?.[0], sort: sortArr }); + }, + }; + }, + }, + { + name: 'template', + Component: SchemaSettingsTemplate, + useComponentProps() { + const { name } = useCollection_deprecated(); + const fieldSchema = useFieldSchema(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + return { + componentName: 'Details', + collectionName: name, + resourceName: defaultResource, + }; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/ReadPrettyFormActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/details-single/ReadPrettyFormActionInitializers.tsx new file mode 100644 index 000000000..98da59cb3 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/ReadPrettyFormActionInitializers.tsx @@ -0,0 +1,86 @@ +import { useCollection_deprecated } from '../../../..'; +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; + +const useVisibleCollection = () => { + const collection = useCollection_deprecated(); + return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; +}; +// 表单的操作配置 +export const readPrettyFormActionInitializers = new SchemaInitializer({ + name: 'ReadPrettyFormActionInitializers', + title: '{{t("Configure actions")}}', + icon: 'SettingOutlined', + style: { + marginLeft: 8, + }, + items: [ + { + type: 'itemGroup', + name: 'enableActions', + title: '{{t("Enable actions")}}', + children: [ + { + title: '{{t("Edit")}}', + name: 'edit', + Component: 'UpdateActionInitializer', + schema: { + 'x-component': 'Action', + 'x-decorator': 'ACLActionProvider', + 'x-component-props': { + type: 'primary', + }, + }, + useVisible: useVisibleCollection, + }, + { + title: '{{t("Delete")}}', + name: 'delete', + Component: 'DestroyActionInitializer', + schema: { + 'x-component': 'Action', + 'x-decorator': 'ACLActionProvider', + }, + useVisible: useVisibleCollection, + }, + ], + }, + { + name: 'divider', + type: 'divider', + }, + { + type: 'subMenu', + name: 'customize', + title: '{{t("Customize")}}', + children: [ + { + name: 'popup', + title: '{{t("Popup")}}', + Component: 'PopupActionInitializer', + useComponentProps() { + return { + 'x-component': 'Action', + }; + }, + }, + { + name: 'updateRecord', + title: '{{t("Update record")}}', + Component: 'UpdateRecordActionInitializer', + useComponentProps() { + return { + 'x-component': 'Action', + }; + }, + visible: useVisibleCollection, + }, + { + name: 'customRequest', + title: '{{t("Custom request")}}', + Component: 'CustomRequestInitializer', + useVisible: useVisibleCollection, + }, + ], + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormItemInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/details-single/ReadPrettyFormItemInitializers.tsx similarity index 84% rename from packages/core/client/src/schema-initializer/buttons/ReadPrettyFormItemInitializers.tsx rename to packages/core/client/src/modules/blocks/data-blocks/details-single/ReadPrettyFormItemInitializers.tsx index 357817dd7..0f0f2b677 100644 --- a/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormItemInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/ReadPrettyFormItemInitializers.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { SchemaInitializerChildren } from '../../application'; -import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; -import { useCompile } from '../../schema-component'; +import { SchemaInitializerChildren } from '../../../../application'; +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; +import { useCompile } from '../../../../schema-component'; import { gridRowColWrap, useAssociatedFormItemInitializerFields, useFormItemInitializerFields, useInheritsFormItemInitializerFields, -} from '../utils'; +} from '../../../../schema-initializer/utils'; const ParentCollectionFields = () => { const inheritFields = useInheritsFormItemInitializerFields(); @@ -77,7 +77,9 @@ export const readPrettyFormItemInitializers = new SchemaInitializer({ type: 'void', 'x-editable': false, 'x-decorator': 'FormItem', - 'x-designer': 'Markdown.Void.Designer', + // 'x-designer': 'Markdown.Void.Designer', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:markdown', 'x-component': 'Markdown.Void', 'x-component-props': { content: '{{t("This is a demo text, **supports Markdown syntax**.")}}', diff --git a/packages/core/client/src/schema-initializer/items/RecordReadPrettyFormBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/details-single/RecordReadPrettyFormBlockInitializer.tsx similarity index 77% rename from packages/core/client/src/schema-initializer/items/RecordReadPrettyFormBlockInitializer.tsx rename to packages/core/client/src/modules/blocks/data-blocks/details-single/RecordReadPrettyFormBlockInitializer.tsx index 67889efb9..39eae3464 100644 --- a/packages/core/client/src/schema-initializer/items/RecordReadPrettyFormBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/RecordReadPrettyFormBlockInitializer.tsx @@ -1,10 +1,13 @@ -import React from 'react'; import { FormOutlined } from '@ant-design/icons'; -import { useBlockAssociationContext, useBlockRequestContext } from '../../block-provider'; -import { useCollection } from '../../collection-manager'; -import { useSchemaTemplateManager } from '../../schema-templates'; -import { createReadPrettyFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; -import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import React from 'react'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; +import { useBlockAssociationContext, useBlockRequestContext } from '../../../../block-provider'; +import { useCollection_deprecated } from '../../../../collection-manager'; +import { useSchemaTemplateManager } from '../../../../schema-templates'; +import { + createReadPrettyFormBlockSchema, + useRecordCollectionDataSourceItems, +} from '../../../../schema-initializer/utils'; export const RecordReadPrettyFormBlockInitializer = () => { const itemConfig = useSchemaInitializerItem(); @@ -18,7 +21,7 @@ export const RecordReadPrettyFormBlockInitializer = () => { } = itemConfig; const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); - const currentCollection = useCollection(); + const currentCollection = useCollection_deprecated(); const collection = targetCollection || currentCollection; const association = useBlockAssociationContext(); const { block } = useBlockRequestContext(); @@ -37,10 +40,12 @@ export const RecordReadPrettyFormBlockInitializer = () => { actionInitializers, association, collection: collection.name, + dataSource: collection.dataSource, action: 'get', useSourceId: '{{ useSourceIdFromParentRecord }}', useParams: '{{ useParamsFromRecord }}', template: s, + settings: 'blockSettings:singleDataDetails', }); if (item.mode === 'reference') { blockSchema['x-template-key'] = item.template.key; @@ -55,9 +60,11 @@ export const RecordReadPrettyFormBlockInitializer = () => { actionInitializers, association, collection: collection.name, + dataSource: collection.dataSource, action: 'get', useSourceId: '{{ useSourceIdFromParentRecord }}', useParams: '{{ useParamsFromRecord }}', + settings: 'blockSettings:singleDataDetails', }), ); } diff --git a/packages/core/client/src/modules/details-single-data/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts similarity index 92% rename from packages/core/client/src/modules/details-single-data/__e2e__/schemaInitializer.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts index ae70441b8..607438eb2 100644 --- a/packages/core/client/src/modules/details-single-data/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts @@ -33,7 +33,7 @@ test.describe('configure actions', () => { page, showMenu: async () => { await page.getByLabel('action-Action-Edit-update-general-form-0').hover(); - await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-general' }).hover(); + await page.getByRole('button', { name: 'designer-schema-settings-Action-actionSettings:edit-general' }).hover(); }, supportedOptions: ['Edit button', 'Linkage rules', 'Open mode', 'Popup size', 'Delete'], }); @@ -43,10 +43,6 @@ test.describe('configure actions', () => { await createAction(page, 'Delete'); await expect(page.getByLabel('action-Action-Delete-destroy-general-form-0')).toBeVisible(); - // create duplicate ------------------------------------------------------------------------------------ - await createAction(page, 'Duplicate'); - await expect(page.getByLabel('action-Action-Duplicate-duplicate-general-form-0')).toBeVisible(); - // create print await createAction(page, 'Print'); await expect(page.getByLabel('action-Action-Print-print-general-form-0')).toBeVisible(); diff --git a/packages/core/client/src/modules/details-single-data/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaSettings.test.ts similarity index 100% rename from packages/core/client/src/modules/details-single-data/__e2e__/schemaSettings.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaSettings.test.ts diff --git a/packages/core/client/src/modules/details-multi-data/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/templatesOfBug.ts similarity index 100% rename from packages/core/client/src/modules/details-multi-data/__e2e__/templatesOfBug.ts rename to packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/templatesOfBug.ts diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/singleDataDetailsBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/singleDataDetailsBlockSettings.ts new file mode 100644 index 000000000..cd130c554 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/singleDataDetailsBlockSettings.ts @@ -0,0 +1,43 @@ +import { useFieldSchema } from '@formily/react'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useCollection_deprecated } from '../../../../collection-manager'; +import { SchemaSettingsBlockTitleItem, SchemaSettingsFormItemTemplate } from '../../../../schema-settings'; + +export const singleDataDetailsBlockSettings = new SchemaSettings({ + name: 'blockSettings:singleDataDetails', + items: [ + { + name: 'title', + Component: SchemaSettingsBlockTitleItem, + }, + { + name: 'formItemTemplate', + Component: SchemaSettingsFormItemTemplate, + useComponentProps() { + const { name } = useCollection_deprecated(); + const fieldSchema = useFieldSchema(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + return { + insertAdjacentPosition: 'beforeEnd', + componentName: 'ReadPrettyFormItem', + collectionName: name, + resourceName: defaultResource, + }; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/items/CreateFormBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/CreateFormBlockInitializer.tsx similarity index 74% rename from packages/core/client/src/schema-initializer/items/CreateFormBlockInitializer.tsx rename to packages/core/client/src/modules/blocks/data-blocks/form/CreateFormBlockInitializer.tsx index 32fabd337..1793c2961 100644 --- a/packages/core/client/src/schema-initializer/items/CreateFormBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/form/CreateFormBlockInitializer.tsx @@ -1,11 +1,11 @@ -import React from 'react'; import { FormOutlined } from '@ant-design/icons'; +import React from 'react'; -import { useBlockAssociationContext } from '../../block-provider'; -import { useCollection } from '../../collection-manager'; -import { useSchemaTemplateManager } from '../../schema-templates'; -import { createFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; -import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; +import { useBlockAssociationContext } from '../../../../block-provider'; +import { useCollection_deprecated } from '../../../../collection-manager'; +import { useSchemaTemplateManager } from '../../../../schema-templates'; +import { createFormBlockSchema, useRecordCollectionDataSourceItems } from '../../../../schema-initializer/utils'; // TODO: `SchemaInitializerItem` items export const CreateFormBlockInitializer = () => { @@ -14,7 +14,7 @@ export const CreateFormBlockInitializer = () => { const { getTemplateSchemaByMode } = useSchemaTemplateManager(); const { insert } = useSchemaInitializer(); const association = useBlockAssociationContext(); - const collection = useCollection(); + const collection = useCollection_deprecated(); return ( } @@ -26,8 +26,10 @@ export const CreateFormBlockInitializer = () => { const blockSchema = createFormBlockSchema({ actionInitializers: 'CreateFormActionInitializers', association, + dataSource: collection.dataSource, collection: collection.name, template: s, + settings: 'blockSettings:createForm', }); if (item.mode === 'reference') { blockSchema['x-template-key'] = item.template.key; @@ -41,7 +43,9 @@ export const CreateFormBlockInitializer = () => { createFormBlockSchema({ actionInitializers: 'CreateFormActionInitializers', association, + dataSource: collection.dataSource, collection: collection.name, + settings: 'blockSettings:createForm', }), ); } diff --git a/packages/core/client/src/schema-initializer/items/FormBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/FormBlockInitializer.tsx similarity index 69% rename from packages/core/client/src/schema-initializer/items/FormBlockInitializer.tsx rename to packages/core/client/src/modules/blocks/data-blocks/form/FormBlockInitializer.tsx index 901e8984c..96d53e497 100644 --- a/packages/core/client/src/schema-initializer/items/FormBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/form/FormBlockInitializer.tsx @@ -1,8 +1,8 @@ import { FormOutlined } from '@ant-design/icons'; import React from 'react'; -import { createFormBlockSchema } from '../utils'; -import { DataBlockInitializer } from './DataBlockInitializer'; -import { useSchemaInitializerItem } from '../../application'; +import { useSchemaInitializerItem } from '../../../../application'; +import { createFormBlockSchema } from '../../../../schema-initializer/utils'; +import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer'; export const FormBlockInitializer = () => { const itemConfig = useSchemaInitializerItem(); @@ -15,8 +15,10 @@ export const FormBlockInitializer = () => { templateWrap={(templateSchema, { item }) => { const s = createFormBlockSchema({ isCusomeizeCreate, + dataSource: item.dataSource, template: templateSchema, collection: item.name, + settings: 'blockSettings:createForm', }); if (item.template && item.mode === 'reference') { s['x-template-key'] = item.template.key; diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/FormItemSchemaToolbar.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/FormItemSchemaToolbar.tsx new file mode 100644 index 000000000..fdaa2dbfc --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/FormItemSchemaToolbar.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { SchemaToolbar } from '../../../../schema-settings'; + +export const FormItemSchemaToolbar = (props) => { + return ; +}; diff --git a/packages/core/client/src/schema-initializer/items/RecordFormBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/RecordFormBlockInitializer.tsx similarity index 77% rename from packages/core/client/src/schema-initializer/items/RecordFormBlockInitializer.tsx rename to packages/core/client/src/modules/blocks/data-blocks/form/RecordFormBlockInitializer.tsx index decfe4173..245fb6c0c 100644 --- a/packages/core/client/src/schema-initializer/items/RecordFormBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/form/RecordFormBlockInitializer.tsx @@ -1,17 +1,17 @@ -import React from 'react'; import { FormOutlined } from '@ant-design/icons'; -import { useBlockAssociationContext } from '../../block-provider'; -import { useCollection } from '../../collection-manager'; -import { useSchemaTemplateManager } from '../../schema-templates'; -import { createFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; -import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import React from 'react'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; +import { useBlockAssociationContext } from '../../../../block-provider'; +import { useCollection_deprecated } from '../../../../collection-manager'; +import { useSchemaTemplateManager } from '../../../../schema-templates'; +import { createFormBlockSchema, useRecordCollectionDataSourceItems } from '../../../../schema-initializer/utils'; export const RecordFormBlockInitializer = () => { const itemConfig = useSchemaInitializerItem(); const { onCreateBlockSchema, componentType, createBlockSchema, targetCollection, ...others } = itemConfig; const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); - const currentCollection = useCollection(); + const currentCollection = useCollection_deprecated(); const collection = targetCollection || currentCollection; const association = useBlockAssociationContext(); return ( @@ -25,11 +25,13 @@ export const RecordFormBlockInitializer = () => { const blockSchema = createFormBlockSchema({ association, collection: collection.name, + dataSource: collection.dataSource, action: 'get', useSourceId: '{{ useSourceIdFromParentRecord }}', useParams: '{{ useParamsFromRecord }}', actionInitializers: 'UpdateFormActionInitializers', template: s, + settings: 'blockSettings:editForm', }); if (item.mode === 'reference') { blockSchema['x-template-key'] = item.template.key; @@ -43,10 +45,12 @@ export const RecordFormBlockInitializer = () => { createFormBlockSchema({ association, collection: collection.name, + dataSource: collection.dataSource, action: 'get', useSourceId: '{{ useSourceIdFromParentRecord }}', useParams: '{{ useParamsFromRecord }}', actionInitializers: 'UpdateFormActionInitializers', + settings: 'blockSettings:editForm', }), ); } diff --git a/packages/core/client/src/modules/form-creation/__e2e__/dragAndDrop.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/dragAndDrop.test.ts similarity index 97% rename from packages/core/client/src/modules/form-creation/__e2e__/dragAndDrop.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/dragAndDrop.test.ts index f41fee45c..d1c87ff93 100644 --- a/packages/core/client/src/modules/form-creation/__e2e__/dragAndDrop.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/dragAndDrop.test.ts @@ -13,14 +13,12 @@ test('fields', async ({ page, mockPage }) => { .getByLabel('block-item-CollectionField-users-form-users.nickname') .getByLabel('designer-drag') .dragTo(page.getByLabel('block-item-CollectionField-users-form-users.username')); - await page.reload(); await page.getByLabel('block-item-CollectionField-users-form-users.nickname').hover(); await page .getByLabel('block-item-CollectionField-users-form-users.nickname') .getByLabel('designer-drag') .dragTo(page.getByLabel('block-item-CollectionField-users-form-users.email')); - await page.reload(); const nickname = await page.getByLabel('block-item-CollectionField-users-form-users.nickname').boundingBox(); const username = await page.getByLabel('block-item-CollectionField-users-form-users.username').boundingBox(); diff --git a/packages/core/client/src/modules/form-creation/__e2e__/lazyLoadAssociationFields.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/lazyLoadAssociationFields.test.ts similarity index 100% rename from packages/core/client/src/modules/form-creation/__e2e__/lazyLoadAssociationFields.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/lazyLoadAssociationFields.test.ts diff --git a/packages/core/client/src/modules/form-creation/__e2e__/lazyLoadVariables.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/lazyLoadVariables.test.ts similarity index 100% rename from packages/core/client/src/modules/form-creation/__e2e__/lazyLoadVariables.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/lazyLoadVariables.test.ts diff --git a/packages/core/client/src/modules/form-creation/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaInitializer.test.ts similarity index 100% rename from packages/core/client/src/modules/form-creation/__e2e__/schemaInitializer.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaInitializer.test.ts diff --git a/packages/core/client/src/modules/form-creation/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings.test.ts similarity index 97% rename from packages/core/client/src/modules/form-creation/__e2e__/schemaSettings.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings.test.ts index 6773dffec..4de6a0684 100644 --- a/packages/core/client/src/modules/form-creation/__e2e__/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings.test.ts @@ -291,6 +291,16 @@ test.describe('creation form block schema settings', () => { await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); await expect(page.getByRole('menuitem', { name: 'Save as block template' })).toBeVisible(); await expect(page.getByRole('menuitem', { name: 'Convert reference to duplicate' })).not.toBeVisible(); + + // 保存为模板之后,应该在 ui-schema-storage 页面显示出来 + await page.goto('/admin/settings/ui-schema-storage'); + await expect(page.getByRole('row', { name: 'General_Form' }).first()).toBeVisible(); + + // 删除创建的模板,以免影响其它测试 + await page.getByLabel('Select all').check(); + await page.getByLabel('action-Action-Delete-destroy-').click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect(page.getByRole('row', { name: 'General_Form' }).first()).toBeHidden(); }); test('delete', async ({ page, mockPage }) => { @@ -361,7 +371,13 @@ test.describe('creation form block schema settings', () => { // 保存为模板之后,应该在 ui-schema-storage 页面显示出来 ----------------------------------------- await page.goto('/admin/settings/ui-schema-storage'); - await expect(page.getByRole('row', { name: 'new_form_template' })).toBeVisible(); + await expect(page.getByRole('row', { name: 'new_form_template' }).first()).toBeVisible(); + + // 最后需要把保存的模板删除掉,以免影响其它测试 + await page.getByLabel('Select all').check(); + await page.getByLabel('action-Action-Delete-destroy-').click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect(page.getByRole('row', { name: 'new_form_template' }).first()).toBeHidden(); async function showSettingsMenu(page) { await page.getByLabel('block-item-CardItem-general-form').hover(); @@ -543,7 +559,9 @@ test.describe('creation form block schema settings', () => { // 测试子表单字段默认值 ------------------------------------------------------------------------------------------ await page.getByLabel(`block-item-CollectionField-users-form-users.nickname-Nickname`).hover(); - await page.getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-users-users.Nickname`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-fieldSettings:FormItem-users-users.Nickname`) + .hover(); await page.getByRole('menuitem', { name: 'Set default value', exact: true }).click(); await page.getByLabel('Set default value').getByRole('textbox').click(); await page.getByLabel('Set default value').getByRole('textbox').fill('test default value'); @@ -558,7 +576,9 @@ test.describe('creation form block schema settings', () => { // 为 username 设置一个变量默认值: {{ currentObject.nickname }} await page.getByLabel(`block-item-CollectionField-users-form-users.username-Username`).hover(); - await page.getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-users-users.Username`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-fieldSettings:FormItem-users-users.Username`) + .hover(); await page.getByRole('menuitem', { name: 'Set default value', exact: true }).click(); await page.getByLabel('variable-button').click(); await page.getByRole('menuitemcheckbox', { name: 'Current object' }).click(); @@ -613,7 +633,7 @@ test.describe('creation form block schema settings', () => { // 测试子表格字段默认值 ------------------------------------------------------------------------------------------ await page.getByRole('button', { name: 'Nickname', exact: true }).hover(); await page - .getByRole('button', { name: 'designer-schema-settings-TableV2.Column-TableV2.Column.Designer-users' }) + .getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-users' }) .hover(); await page.getByRole('menuitem', { name: 'Set default value', exact: true }).click(); await page.mouse.move(300, 0); @@ -624,7 +644,7 @@ test.describe('creation form block schema settings', () => { // 确保下拉选项被隐藏 await page.getByRole('button', { name: 'Nickname', exact: true }).hover(); await page - .getByRole('button', { name: 'designer-schema-settings-TableV2.Column-TableV2.Column.Designer-users' }) + .getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-users' }) .hover(); await page.mouse.move(300, 0); @@ -642,7 +662,7 @@ test.describe('creation form block schema settings', () => { // 为 username 设置一个变量默认值: {{ currentObject.nickname }} await page.getByRole('button', { name: 'Username', exact: true }).hover(); await page - .getByRole('button', { name: 'designer-schema-settings-TableV2.Column-TableV2.Column.Designer-users' }) + .getByRole('button', { name: 'designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-users' }) .hover(); await page.getByRole('menuitem', { name: 'Set default value', exact: true }).click(); await page.mouse.move(300, 0); @@ -1182,9 +1202,10 @@ test.describe('creation form block schema settings', () => { //删除模板 await page.getByTestId('plugin-settings-button').click(); - await page.getByLabel('ui-schema-storage').click(); + await page.getByRole('link', { name: 'Block templates' }).click(); await page.getByRole('menuitem', { name: 'layout Block templates' }).click(); - await page.getByLabel('action-Action.Link-Delete-destroy-uiSchemaTemplates-table-Users_Form').click(); + await page.getByLabel('Select all').check(); + await page.getByLabel('action-Action-Delete-destroy-').click(); await page.getByRole('button', { name: 'OK', exact: true }).click(); }); }); @@ -1199,7 +1220,7 @@ test.describe('actions schema settings', () => { await page.getByRole('button', { name: 'Submit' }).hover(); await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-users' }).hover(); }, - supportedOptions: ['Edit button', 'Bind workflows', 'Delete'], + supportedOptions: ['Edit button', 'Second confirmation', 'Bind workflows', 'Delete'], }); }); diff --git a/packages/core/client/src/modules/form-creation/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/templatesOfBug.ts similarity index 100% rename from packages/core/client/src/modules/form-creation/__e2e__/templatesOfBug.ts rename to packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/templatesOfBug.ts diff --git a/packages/core/client/src/modules/form-edit/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts similarity index 100% rename from packages/core/client/src/modules/form-edit/__e2e__/schemaInitializer.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts diff --git a/packages/core/client/src/modules/form-edit/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaSettings.test.ts similarity index 92% rename from packages/core/client/src/modules/form-edit/__e2e__/schemaSettings.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaSettings.test.ts index 0c9e7759f..c0b0c6dd7 100644 --- a/packages/core/client/src/modules/form-edit/__e2e__/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaSettings.test.ts @@ -113,6 +113,16 @@ test.describe('edit form block schema settings', () => { await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); await expect(page.getByRole('menuitem', { name: 'Save as block template' })).toBeVisible(); await expect(page.getByRole('menuitem', { name: 'Convert reference to duplicate' })).not.toBeVisible(); + + // 保存为模板之后,应该在 ui-schema-storage 页面显示出来 + await page.goto('/admin/settings/ui-schema-storage'); + await expect(page.getByRole('row', { name: 'General_Form' }).first()).toBeVisible(); + + // 删除创建的模板,以免影响其它测试 + await page.getByLabel('Select all').check(); + await page.getByLabel('action-Action-Delete-destroy-').click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect(page.getByRole('row', { name: 'General_Form' }).first()).toBeHidden(); }); test('Delete', async ({ page, mockPage, mockRecord }) => { diff --git a/packages/core/client/src/modules/details-single-data/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts similarity index 100% rename from packages/core/client/src/modules/details-single-data/__e2e__/templatesOfBug.ts rename to packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/createFormActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/createFormActionInitializers.tsx new file mode 100644 index 000000000..6c83d1d63 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/createFormActionInitializers.tsx @@ -0,0 +1,47 @@ +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; +import { formTriggerWorkflowActionInitializerV2 } from './formActionInitializers'; + +export const createFormActionInitializers = new SchemaInitializer({ + name: 'CreateFormActionInitializers', + title: '{{t("Configure actions")}}', + icon: 'SettingOutlined', + items: [ + { + type: 'itemGroup', + title: '{{t("Enable actions")}}', + name: 'enableActions', + children: [ + { + name: 'submit', + title: '{{t("Submit")}}', + Component: 'CreateSubmitActionInitializer', + schema: { + 'x-action-settings': {}, + }, + }, + ], + }, + { + name: 'divider', + type: 'divider', + }, + { + type: 'subMenu', + title: '{{t("Customize")}}', + name: 'customize', + children: [ + { + name: 'saveRecord', + title: '{{t("Save record")}}', + Component: 'SaveRecordActionInitializer', + }, + formTriggerWorkflowActionInitializerV2, + { + name: 'customRequest', + title: '{{t("Custom request")}}', + Component: 'CustomRequestInitializer', + }, + ], + }, + ], +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/createFormBlockSettings.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/createFormBlockSettings.tsx new file mode 100644 index 000000000..0e7551ed9 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/createFormBlockSettings.tsx @@ -0,0 +1,143 @@ +import { useFieldSchema } from '@formily/react'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useFormBlockContext } from '../../../../block-provider'; +import { useCollection_deprecated } from '../../../../collection-manager'; +import { + SchemaSettingsBlockTitleItem, + SchemaSettingsDataTemplates, + SchemaSettingsFormItemTemplate, + SchemaSettingsLinkageRules, +} from '../../../../schema-settings'; + +// TODO: 0.20 版之后可以删除 +export const creationFormBlockSettings = new SchemaSettings({ + name: 'blockSettings:creationForm', + items: [ + { + name: 'title', + Component: SchemaSettingsBlockTitleItem, + }, + { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const { name } = useCollection_deprecated(); + return { + collectionName: name, + }; + }, + }, + { + name: 'dataTemplates', + Component: SchemaSettingsDataTemplates, + useVisible() { + const { action } = useFormBlockContext(); + return !action; + }, + useComponentProps() { + const { name } = useCollection_deprecated(); + return { + collectionName: name, + }; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'formItemTemplate', + Component: SchemaSettingsFormItemTemplate, + useComponentProps() { + const { name } = useCollection_deprecated(); + const fieldSchema = useFieldSchema(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + return { + componentName: 'FormItem', + collectionName: name, + resourceName: defaultResource, + }; + }, + }, + { + name: 'divider2', + type: 'divider', + }, + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }, + }, + ], +}); + +export const createFormBlockSettings = new SchemaSettings({ + name: 'blockSettings:createForm', + items: [ + { + name: 'title', + Component: SchemaSettingsBlockTitleItem, + }, + { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const { name } = useCollection_deprecated(); + return { + collectionName: name, + }; + }, + }, + { + name: 'dataTemplates', + Component: SchemaSettingsDataTemplates, + useVisible() { + const { action } = useFormBlockContext(); + return !action; + }, + useComponentProps() { + const { name } = useCollection_deprecated(); + return { + collectionName: name, + }; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'formItemTemplate', + Component: SchemaSettingsFormItemTemplate, + useComponentProps() { + const { name } = useCollection_deprecated(); + const fieldSchema = useFieldSchema(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + return { + componentName: 'FormItem', + collectionName: name, + resourceName: defaultResource, + }; + }, + }, + { + name: 'divider2', + type: 'divider', + }, + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/editFormBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/form/editFormBlockSettings.ts new file mode 100644 index 000000000..134fa5235 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/editFormBlockSettings.ts @@ -0,0 +1,76 @@ +import { useFieldSchema } from '@formily/react'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useFormBlockContext } from '../../../../block-provider'; +import { useCollection_deprecated } from '../../../../collection-manager'; +import { + SchemaSettingsBlockTitleItem, + SchemaSettingsDataTemplates, + SchemaSettingsFormItemTemplate, + SchemaSettingsLinkageRules, +} from '../../../../schema-settings'; + +export const editFormBlockSettings = new SchemaSettings({ + name: 'blockSettings:editForm', + items: [ + { + name: 'title', + Component: SchemaSettingsBlockTitleItem, + }, + { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const { name } = useCollection_deprecated(); + return { + collectionName: name, + }; + }, + }, + { + name: 'dataTemplates', + Component: SchemaSettingsDataTemplates, + useVisible() { + const { action } = useFormBlockContext(); + return !action; + }, + useComponentProps() { + const { name } = useCollection_deprecated(); + return { + collectionName: name, + }; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'formItemTemplate', + Component: SchemaSettingsFormItemTemplate, + useComponentProps() { + const { name } = useCollection_deprecated(); + const fieldSchema = useFieldSchema(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + return { + componentName: 'FormItem', + collectionName: name, + resourceName: defaultResource, + }; + }, + }, + { + name: 'divider2', + type: 'divider', + }, + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/fieldSettingsFormItem.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/fieldSettingsFormItem.tsx new file mode 100644 index 000000000..5cf95d7f5 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/fieldSettingsFormItem.tsx @@ -0,0 +1,479 @@ +import { ArrayCollapse, FormLayout } from '@formily/antd-v5'; +import { Field } from '@formily/core'; +import { ISchema, useField, useFieldSchema } from '@formily/react'; +import _ from 'lodash'; +import { useTranslation } from 'react-i18next'; +import { useApp, useSchemaToolbar } from '../../../../application'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useFormBlockContext } from '../../../../block-provider'; +import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../../collection-manager'; +import { useFieldComponentName } from '../../../../common/useFieldComponentName'; +import { useDesignable, useValidateSchema } from '../../../../schema-component'; +import { useIsFormReadPretty } from '../../../../schema-component/antd/form-item/FormItem.Settings'; +import { getTempFieldState } from '../../../../schema-component/antd/form-v2/utils'; +import { isPatternDisabled } from '../../../../schema-settings'; +import { ActionType } from '../../../../schema-settings/LinkageRules/type'; +import { SchemaSettingsDefaultValue } from '../../../../schema-settings/SchemaSettingsDefaultValue'; +import { useIsAllowToSetDefaultValue } from '../../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; + +export const fieldSettingsFormItem = new SchemaSettings({ + name: 'fieldSettings:FormItem', + items: [ + { + name: 'decoratorOptions', + type: 'itemGroup', + hideIfNoChildren: true, + useComponentProps() { + const { t } = useTranslation(); + return { + title: t('Generic properties'), + }; + }, + useChildren(): any { + return [ + { + name: 'editFieldTitle', + type: 'modal', + useComponentProps() { + const { t } = useTranslation(); + const { dn } = useDesignable(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); + const collectionField = + getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + + return { + title: t('Edit field title'), + schema: { + type: 'object', + title: t('Edit field title'), + properties: { + title: { + title: t('Field title'), + default: field?.title, + description: `${t('Original field title: ')}${collectionField?.uiSchema?.title}`, + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-component-props': {}, + }, + }, + } as ISchema, + onSubmit({ title }) { + if (title) { + field.title = title; + fieldSchema.title = title; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + title: fieldSchema.title, + }, + }); + } + dn.refresh(); + }, + }; + }, + }, + { + name: 'displayTitle', + type: 'switch', + useComponentProps() { + const { t } = useTranslation(); + const { dn } = useDesignable(); + const field = useField(); + const fieldSchema = useFieldSchema(); + + return { + title: t('Display title'), + checked: fieldSchema['x-decorator-props']?.['showTitle'] ?? true, + onChange(checked) { + fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {}; + fieldSchema['x-decorator-props']['showTitle'] = checked; + field.decoratorProps.showTitle = checked; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + 'x-decorator-props': { + ...fieldSchema['x-decorator-props'], + showTitle: checked, + }, + }, + }); + dn.refresh(); + }, + }; + }, + }, + { + name: 'editDescription', + type: 'modal', + useComponentProps() { + const { t } = useTranslation(); + const { dn } = useDesignable(); + const field = useField(); + const fieldSchema = useFieldSchema(); + + return { + title: t('Edit description'), + schema: { + type: 'object', + title: t('Edit description'), + properties: { + description: { + // title: t('Description'), + default: field?.description, + 'x-decorator': 'FormItem', + 'x-component': 'Input.TextArea', + 'x-component-props': {}, + }, + }, + } as ISchema, + onSubmit({ description }) { + field.description = description; + fieldSchema.description = description; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + description: fieldSchema.description, + }, + }); + dn.refresh(); + }, + }; + }, + }, + { + name: 'editTooltip', + type: 'modal', + useComponentProps() { + const { t } = useTranslation(); + const { dn } = useDesignable(); + const field = useField(); + const fieldSchema = useFieldSchema(); + + return { + title: t('Edit tooltip'), + schema: { + type: 'object', + title: t('Edit tooltip'), + properties: { + tooltip: { + default: fieldSchema?.['x-decorator-props']?.tooltip, + 'x-decorator': 'FormItem', + 'x-component': 'Input.TextArea', + 'x-component-props': {}, + }, + }, + } as ISchema, + onSubmit({ tooltip }) { + field.decoratorProps.tooltip = tooltip; + fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {}; + fieldSchema['x-decorator-props']['tooltip'] = tooltip; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + dn.refresh(); + }, + }; + }, + }, + { + name: 'required', + type: 'switch', + useVisible() { + const field = useField(); + const fieldSchema = useFieldSchema(); + const { required = true } = useSchemaToolbar(); + return !field.readPretty && fieldSchema['x-component'] !== 'FormField' && required; + }, + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn, refresh } = useDesignable(); + + return { + title: t('Required'), + checked: fieldSchema.required as boolean, + onChange(required) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + field.required = required; + fieldSchema['required'] = required; + schema['required'] = required; + dn.emit('patch', { + schema, + }); + refresh(); + }, + }; + }, + }, + { + name: 'setDefaultValue', + useVisible() { + const { isAllowToSetDefaultValue } = useIsAllowToSetDefaultValue(); + return isAllowToSetDefaultValue(); + }, + Component: SchemaSettingsDefaultValue, + }, + { + name: 'pattern', + type: 'select', + useVisible() { + const { form } = useFormBlockContext(); + const fieldSchema = useFieldSchema(); + return form && !form?.readPretty && !isPatternDisabled(fieldSchema); + }, + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn } = useDesignable(); + let readOnlyMode = 'editable'; + if (fieldSchema['x-disabled'] === true) { + readOnlyMode = 'readonly'; + } + if (fieldSchema['x-read-pretty'] === true) { + readOnlyMode = 'read-pretty'; + } + return { + title: t('Pattern'), + options: [ + { label: t('Editable'), value: 'editable' }, + { label: t('Readonly'), value: 'readonly' }, + { label: t('Easy-reading'), value: 'read-pretty' }, + ], + value: readOnlyMode, + onChange(v) { + const schema: ISchema = { + ['x-uid']: fieldSchema['x-uid'], + }; + + switch (v) { + case 'readonly': { + fieldSchema['x-read-pretty'] = false; + fieldSchema['x-disabled'] = true; + schema['x-read-pretty'] = false; + schema['x-disabled'] = true; + field.readPretty = false; + field.disabled = true; + _.set(field, 'initStateOfLinkageRules.pattern', getTempFieldState(true, ActionType.ReadOnly)); + break; + } + case 'read-pretty': { + fieldSchema['x-read-pretty'] = true; + fieldSchema['x-disabled'] = false; + schema['x-read-pretty'] = true; + schema['x-disabled'] = false; + field.readPretty = true; + _.set(field, 'initStateOfLinkageRules.pattern', getTempFieldState(true, ActionType.ReadPretty)); + break; + } + default: { + fieldSchema['x-read-pretty'] = false; + fieldSchema['x-disabled'] = false; + schema['x-read-pretty'] = false; + schema['x-disabled'] = false; + field.readPretty = false; + field.disabled = false; + _.set(field, 'initStateOfLinkageRules.pattern', getTempFieldState(true, ActionType.Editable)); + break; + } + } + + dn.emit('patch', { + schema, + }); + + dn.refresh(); + }, + }; + }, + }, + { + name: 'setValidationRules', + type: 'modal', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn, refresh } = useDesignable(); + const validateSchema = useValidateSchema(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); + const collectionField = + getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + + return { + title: t('Set validation rules'), + components: { ArrayCollapse, FormLayout }, + schema: { + type: 'object', + title: t('Set validation rules'), + properties: { + rules: { + type: 'array', + default: fieldSchema?.['x-validator'], + 'x-component': 'ArrayCollapse', + 'x-decorator': 'FormItem', + 'x-component-props': { + accordion: true, + }, + maxItems: 3, + items: { + type: 'object', + 'x-component': 'ArrayCollapse.CollapsePanel', + 'x-component-props': { + header: '{{ t("Validation rule") }}', + }, + properties: { + index: { + type: 'void', + 'x-component': 'ArrayCollapse.Index', + }, + layout: { + type: 'void', + 'x-component': 'FormLayout', + 'x-component-props': { + labelStyle: { + marginTop: '6px', + }, + labelCol: 8, + wrapperCol: 16, + }, + properties: { + ...validateSchema, + message: { + type: 'string', + title: '{{ t("Error message") }}', + 'x-decorator': 'FormItem', + 'x-component': 'Input.TextArea', + 'x-component-props': { + autoSize: { + minRows: 2, + maxRows: 2, + }, + }, + }, + }, + }, + remove: { + type: 'void', + 'x-component': 'ArrayCollapse.Remove', + }, + moveUp: { + type: 'void', + 'x-component': 'ArrayCollapse.MoveUp', + }, + moveDown: { + type: 'void', + 'x-component': 'ArrayCollapse.MoveDown', + }, + }, + }, + properties: { + add: { + type: 'void', + title: '{{ t("Add validation rule") }}', + 'x-component': 'ArrayCollapse.Addition', + 'x-reactions': { + dependencies: ['rules'], + fulfill: { + state: { + disabled: '{{$deps[0].length >= 3}}', + }, + }, + }, + }, + }, + }, + }, + } as ISchema, + onSubmit(v) { + const rules = []; + for (const rule of v.rules) { + rules.push(_.pickBy(rule, _.identity)); + } + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + if (['percent'].includes(collectionField?.interface)) { + for (const rule of rules) { + if (!!rule.maxValue || !!rule.minValue) { + rule['percentMode'] = true; + } + + if (rule.percentFormat) { + rule['percentFormats'] = true; + } + } + } + const concatValidator = _.concat([], collectionField?.uiSchema?.['x-validator'] || [], rules); + field.validator = concatValidator; + fieldSchema['x-validator'] = rules; + schema['x-validator'] = rules; + dn.emit('patch', { + schema, + }); + refresh(); + }, + }; + }, + useVisible() { + const { form } = useFormBlockContext(); + const isFormReadPretty = useIsFormReadPretty(); + const validateSchema = useValidateSchema(); + return form && !isFormReadPretty && validateSchema; + }, + }, + ]; + }, + }, + { + name: 'componentOptions', + type: 'itemGroup', + hideIfNoChildren: true, + useComponentProps() { + const { t } = useTranslation(); + return { + title: t('Specific properties'), + }; + }, + useChildren() { + const app = useApp(); + const fieldComponentName = useFieldComponentName(); + const componentSettings = app.schemaSettingsManager.get(`fieldSettings:component:${fieldComponentName}`); + return componentSettings?.items || []; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'delete', + type: 'remove', + sort: 100, + useComponentProps() { + const { t } = useTranslation(); + + return { + removeParentsIfNoChildren: true, + confirm: { + title: t('Delete field'), + }, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/formActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/formActionInitializers.tsx new file mode 100644 index 000000000..13ac761f2 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/formActionInitializers.tsx @@ -0,0 +1,54 @@ +import { SchemaInitializerItemType } from '../../../../application'; +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; + +export const formTriggerWorkflowActionInitializerV2: SchemaInitializerItemType = { + name: 'submitToWorkflow', + title: '{{t("Submit to workflow", { ns: "workflow" })}}', + Component: 'FormTriggerWorkflowActionInitializerV2', +}; + +// 表单的操作配置 +export const formActionInitializers = new SchemaInitializer({ + name: 'FormActionInitializers', + title: '{{t("Configure actions")}}', + icon: 'SettingOutlined', + items: [ + { + type: 'itemGroup', + name: 'enableActions', + title: '{{t("Enable actions")}}', + children: [ + { + name: 'submit', + title: '{{t("Submit")}}', + Component: 'CreateSubmitActionInitializer', + schema: { + 'x-action-settings': {}, + }, + }, + ], + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'custom', + type: 'subMenu', + title: '{{t("Customize")}}', + children: [ + { + name: 'saveRecord', + title: '{{t("Save record")}}', + Component: 'SaveRecordActionInitializer', + }, + formTriggerWorkflowActionInitializerV2, + { + name: 'customRequest', + title: '{{t("Custom request")}}', + Component: 'CustomRequestInitializer', + }, + ], + }, + ], +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/formItemInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/formItemInitializers.tsx new file mode 100644 index 000000000..63f3c6bed --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/formItemInitializers.tsx @@ -0,0 +1,37 @@ +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; +import { gridRowColWrap, useFormItemInitializerFields } from '../../../../schema-initializer/utils'; +import { ParentCollectionFields, AssociatedFields } from '../../../../schema-initializer/buttons/FormItemInitializers'; + +// 表单里配置字段 + +export const formItemInitializers = new SchemaInitializer({ + name: 'FormItemInitializers', + wrap: gridRowColWrap, + icon: 'SettingOutlined', + title: '{{t("Configure fields")}}', + items: [ + { + type: 'itemGroup', + name: 'displayFields', + title: '{{t("Display fields")}}', + useChildren: useFormItemInitializerFields, + }, + { + name: 'parentCollectionFields', + Component: ParentCollectionFields, + }, + { + name: 'associationFields', + Component: AssociatedFields, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'addText', + title: '{{t("Add text")}}', + Component: 'MarkdownFormItemInitializer', + }, + ], +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/index.ts b/packages/core/client/src/modules/blocks/data-blocks/form/index.ts new file mode 100644 index 000000000..b78f5584f --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/index.ts @@ -0,0 +1,11 @@ +export * from './createFormBlockSettings'; +export * from './CreateFormBlockInitializer'; +export * from './FormBlockInitializer'; +export * from './FormItemSchemaToolbar'; +export * from './RecordFormBlockInitializer'; +export * from './editFormBlockSettings'; +export * from './createFormActionInitializers'; +export * from './formActionInitializers'; +export * from './formItemInitializers'; +export * from './fieldSettingsFormItem'; +export * from './updateFormActionInitializers'; diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/updateFormActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/updateFormActionInitializers.tsx new file mode 100644 index 000000000..2eaeab61f --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/updateFormActionInitializers.tsx @@ -0,0 +1,58 @@ +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; +import { formTriggerWorkflowActionInitializerV2 } from './formActionInitializers'; + +export const updateFormActionInitializers = new SchemaInitializer({ + name: 'UpdateFormActionInitializers', + title: '{{t("Configure actions")}}', + icon: 'SettingOutlined', + items: [ + { + type: 'itemGroup', + title: '{{t("Enable actions")}}', + name: 'enableActions', + children: [ + { + name: 'submit', + title: '{{t("Submit")}}', + Component: 'UpdateSubmitActionInitializer', + schema: { + 'x-action-settings': {}, + }, + }, + ], + }, + { + name: 'divider', + type: 'divider', + }, + { + type: 'subMenu', + title: '{{t("Customize")}}', + name: 'customize', + children: [ + { + name: 'popup', + title: '{{t("Popup")}}', + Component: 'PopupActionInitializer', + useComponentProps() { + return { + 'x-component': 'Action', + }; + }, + }, + { + name: 'saveRecord', + title: '{{t("Save record")}}', + Component: 'SaveRecordActionInitializer', + }, + formTriggerWorkflowActionInitializerV2, + { + type: 'item', + name: 'customRequest', + title: '{{t("Custom request")}}', + Component: 'CustomRequestInitializer', + }, + ], + }, + ], +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardActionInitializers.tsx new file mode 100644 index 000000000..67ae9874a --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardActionInitializers.tsx @@ -0,0 +1,82 @@ +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; +import { useCollection_deprecated } from '../../../../collection-manager'; + +// 表单的操作配置 +export const gridCardActionInitializers = new SchemaInitializer({ + name: 'GridCardActionInitializers', + title: "{{t('Configure actions')}}", + icon: 'SettingOutlined', + style: { + marginLeft: 8, + }, + items: [ + { + type: 'itemGroup', + title: "{{t('Enable actions')}}", + name: 'enableActions', + children: [ + { + name: 'filter', + title: "{{t('Filter')}}", + Component: 'FilterActionInitializer', + schema: { + 'x-align': 'left', + }, + }, + { + name: 'addNew', + title: "{{t('Add new')}}", + Component: 'CreateActionInitializer', + schema: { + 'x-align': 'right', + 'x-decorator': 'ACLActionProvider', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + }, + useVisible() { + const collection = useCollection_deprecated(); + return !['view', 'file', 'sql'].includes(collection.template) || collection?.writableView; + }, + }, + { + name: 'refresh', + title: "{{t('Refresh')}}", + Component: 'RefreshActionInitializer', + schema: { + 'x-align': 'right', + }, + }, + { + name: 'import', + title: "{{t('Import')}}", + Component: 'ImportActionInitializer', + schema: { + 'x-align': 'right', + 'x-acl-action': 'importXlsx', + 'x-decorator': 'ACLActionProvider', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + }, + useVisible() { + const collection = useCollection_deprecated(); + return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; + }, + }, + { + name: 'export', + title: "{{t('Export')}}", + Component: 'ExportActionInitializer', + schema: { + 'x-align': 'right', + 'x-decorator': 'ACLActionProvider', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + }, + }, + ], + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/items/GridCardBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardBlockInitializer.tsx similarity index 56% rename from packages/core/client/src/schema-initializer/items/GridCardBlockInitializer.tsx rename to packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardBlockInitializer.tsx index 897183517..d8c41e53f 100644 --- a/packages/core/client/src/schema-initializer/items/GridCardBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/GridCardBlockInitializer.tsx @@ -1,13 +1,13 @@ -import React from 'react'; import { OrderedListOutlined } from '@ant-design/icons'; -import { createGridCardBlockSchema } from '../utils'; -import { DataBlockInitializer } from './DataBlockInitializer'; -import { useCollectionManager } from '../../collection-manager'; -import { useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import React from 'react'; +import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; +import { useCollectionManager_deprecated } from '../../../../collection-manager'; +import { createGridCardBlockSchema } from '../../../../schema-initializer/utils'; +import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer'; export const GridCardBlockInitializer = () => { const { insert } = useSchemaInitializer(); - const { getCollection } = useCollectionManager(); + const { getCollection } = useCollectionManager_deprecated(); const itemConfig = useSchemaInitializerItem(); return ( { icon={} componentType={'GridCard'} onCreateBlockSchema={async ({ item }) => { - const collection = getCollection(item.name); + const collection = getCollection(item.name, item.dataSource); const schema = createGridCardBlockSchema({ collection: item.name, + dataSource: item.dataSource, rowKey: collection.filterTargetKey || 'id', + settings: 'blockSettings:gridCard', }); insert(schema); }} diff --git a/packages/core/client/src/modules/grid-card/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts similarity index 100% rename from packages/core/client/src/modules/grid-card/__e2e__/schemaInitializer.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts diff --git a/packages/core/client/src/modules/grid-card/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaSettings.test.ts similarity index 100% rename from packages/core/client/src/modules/grid-card/__e2e__/schemaSettings.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaSettings.test.ts diff --git a/packages/core/client/src/modules/form-edit/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/templatesOfBug.ts similarity index 100% rename from packages/core/client/src/modules/form-edit/__e2e__/templatesOfBug.ts rename to packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/templatesOfBug.ts diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardBlockSettings.ts new file mode 100644 index 000000000..cc8a796f6 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardBlockSettings.ts @@ -0,0 +1,285 @@ +import { ArrayItems } from '@formily/antd-v5'; +import { ISchema } from '@formily/json-schema'; +import { useField, useFieldSchema } from '@formily/react'; +import _ from 'lodash'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useFormBlockContext } from '../../../../block-provider'; +import { useCollection_deprecated, useSortFields } from '../../../../collection-manager'; +import { removeNullCondition, useDesignable } from '../../../../schema-component'; +import { + defaultColumnCount, + gridSizes, + pageSizeOptions, + screenSizeMaps, + screenSizeTitleMaps, +} from '../../../../schema-component/antd/grid-card/options'; +import { SchemaSettingsTemplate } from '../../../../schema-settings'; +import { columnCountMarks } from './utils'; +import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope'; + +export const gridCardBlockSettings = new SchemaSettings({ + name: 'blockSettings:gridCard', + items: [ + { + name: 'SetTheCountOfColumnsDisplayedInARow', + type: 'modal', + useComponentProps() { + const { t } = useTranslation(); + const fieldSchema = useFieldSchema(); + const field = useField(); + const { dn } = useDesignable(); + const columnCount = field.decoratorProps.columnCount || defaultColumnCount; + + const columnCountSchema = useMemo(() => { + return { + 'x-component': 'Slider', + 'x-decorator': 'FormItem', + 'x-component-props': { + min: 1, + max: 24, + marks: columnCountMarks, + tooltip: { + formatter: (value) => `${value}${t('Column')}`, + }, + step: null, + }, + }; + }, [t]); + + const columnCountProperties = useMemo(() => { + return gridSizes.reduce((o, k) => { + o[k] = { + ...columnCountSchema, + title: t(screenSizeTitleMaps[k]), + description: `${t('Screen size')} ${screenSizeMaps[k]} ${t('pixels')}`, + }; + return o; + }, {}); + }, [columnCountSchema, t]); + + return { + title: t('Set the count of columns displayed in a row'), + initialValues: columnCount, + schema: { + type: 'object', + title: t('Set the count of columns displayed in a row'), + properties: columnCountProperties, + } as ISchema, + + onSubmit: (columnCount) => { + _.set(fieldSchema, 'x-decorator-props.columnCount', columnCount); + field.decoratorProps.columnCount = columnCount; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }, + }; + }, + }, + { + name: 'SetTheDataScope', + Component: SchemaSettingsDataScope, + useComponentProps() { + const { name } = useCollection_deprecated(); + const fieldSchema = useFieldSchema(); + const { form } = useFormBlockContext(); + const field = useField(); + const { dn } = useDesignable(); + const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; + + return { + collectionName: name, + defaultFilter: fieldSchema?.['x-decorator-props']?.params?.filter || {}, + form: form, + onSubmit: ({ filter }) => { + filter = removeNullCondition(filter); + _.set(fieldSchema, 'x-decorator-props.params.filter', filter); + field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params }; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }, + }; + }, + }, + { + name: 'SetDefaultSortingRules', + type: 'modal', + useComponentProps() { + const { name } = useCollection_deprecated(); + const { t } = useTranslation(); + const fieldSchema = useFieldSchema(); + const field = useField(); + const { dn } = useDesignable(); + const sortFields = useSortFields(name); + const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; + + const sort = defaultSort?.map((item: string) => { + return item.startsWith('-') + ? { + field: item.substring(1), + direction: 'desc', + } + : { + field: item, + direction: 'asc', + }; + }); + + return { + title: t('Set default sorting rules'), + components: { ArrayItems }, + schema: { + type: 'object', + title: t('Set default sorting rules'), + properties: { + sort: { + type: 'array', + default: sort, + 'x-component': 'ArrayItems', + 'x-decorator': 'FormItem', + items: { + type: 'object', + properties: { + space: { + type: 'void', + 'x-component': 'Space', + properties: { + sort: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.SortHandle', + }, + field: { + type: 'string', + enum: sortFields, + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-component-props': { + style: { + width: 260, + }, + }, + }, + direction: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Radio.Group', + 'x-component-props': { + optionType: 'button', + }, + enum: [ + { + label: t('ASC'), + value: 'asc', + }, + { + label: t('DESC'), + value: 'desc', + }, + ], + }, + remove: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.Remove', + }, + }, + }, + }, + }, + properties: { + add: { + type: 'void', + title: t('Add sort field'), + 'x-component': 'ArrayItems.Addition', + }, + }, + }, + }, + } as ISchema, + onSubmit: ({ sort }) => { + const sortArr = sort.map((item) => { + return item.direction === 'desc' ? `-${item.field}` : item.field; + }); + + _.set(fieldSchema, 'x-decorator-props.params.sort', sortArr); + field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params }; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }, + }; + }, + }, + { + name: 'RecordsPerPage', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const fieldSchema = useFieldSchema(); + const field = useField(); + const { dn } = useDesignable(); + + return { + title: t('Records per page'), + value: field.decoratorProps?.params?.pageSize || 20, + options: pageSizeOptions.map((v) => ({ value: v })), + onChange: (pageSize) => { + _.set(fieldSchema, 'x-decorator-props.params.pageSize', pageSize); + field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params, page: 1 }; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }, + }; + }, + }, + { + name: 'ConvertReferenceToDuplicate', + Component: SchemaSettingsTemplate, + useComponentProps() { + const { name } = useCollection_deprecated(); + const fieldSchema = useFieldSchema(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + + return { + componentName: 'GridCard', + collectionName: name, + resourceName: defaultResource, + }; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'delete', + type: 'remove', + useComponentProps() { + return { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardItemActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardItemActionInitializers.tsx new file mode 100644 index 000000000..3118ebe22 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/gridCardItemActionInitializers.tsx @@ -0,0 +1,100 @@ +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; +import { useCollection_deprecated } from '../../../../collection-manager'; + +export const gridCardItemActionInitializers = new SchemaInitializer({ + name: 'GridCardItemActionInitializers', + title: '{{t("Configure actions")}}', + icon: 'SettingOutlined', + items: [ + { + type: 'itemGroup', + title: '{{t("Enable actions")}}', + name: 'enable-actions', + children: [ + { + name: 'view', + title: '{{t("View")}}', + Component: 'ViewActionInitializer', + schema: { + 'x-component': 'Action.Link', + 'x-action': 'view', + 'x-decorator': 'ACLActionProvider', + 'x-align': 'left', + }, + }, + { + name: 'edit', + title: '{{t("Edit")}}', + Component: 'UpdateActionInitializer', + schema: { + 'x-component': 'Action.Link', + 'x-action': 'update', + 'x-decorator': 'ACLActionProvider', + 'x-align': 'left', + }, + useVisible() { + const collection = useCollection_deprecated(); + return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; + }, + }, + { + name: 'delete', + title: '{{t("Delete")}}', + Component: 'DestroyActionInitializer', + schema: { + 'x-component': 'Action.Link', + 'x-action': 'destroy', + 'x-decorator': 'ACLActionProvider', + 'x-align': 'left', + }, + useVisible() { + const collection = useCollection_deprecated(); + return collection.template !== 'sql'; + }, + }, + ], + }, + { + name: 'divider', + type: 'divider', + }, + { + type: 'subMenu', + title: '{{t("Customize")}}', + name: 'customize', + children: [ + { + name: 'popup', + title: '{{t("Popup")}}', + Component: 'PopupActionInitializer', + useComponentProps() { + return { + 'x-component': 'Action.Link', + }; + }, + }, + { + name: 'update-record', + title: '{{t("Update record")}}', + Component: 'UpdateRecordActionInitializer', + useVisible() { + const collection = useCollection_deprecated(); + return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; + }, + }, + { + name: 'customRequest', + title: '{{t("Custom request")}}', + Component: 'CustomRequestInitializer', + schema: { + 'x-action': 'customize:table:request', + }, + useVisible() { + const collection = useCollection_deprecated(); + return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; + }, + }, + ], + }, + ], +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/utils.ts b/packages/core/client/src/modules/blocks/data-blocks/grid-card/utils.ts new file mode 100644 index 000000000..b8b1b31a8 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/utils.ts @@ -0,0 +1,4 @@ +export const columnCountMarks = [1, 2, 3, 4, 6, 8, 12, 24].reduce((obj, cur) => { + obj[cur] = cur; + return obj; +}, {}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/ListActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/list/ListActionInitializers.tsx new file mode 100644 index 000000000..ab4e0bbdc --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/list/ListActionInitializers.tsx @@ -0,0 +1,86 @@ +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; +import { useCollection_deprecated } from '../../../../collection-manager'; + +// 表单的操作配置 +export const listActionInitializers = new SchemaInitializer({ + name: 'ListActionInitializers', + title: "{{t('Configure actions')}}", + icon: 'SettingOutlined', + style: { + marginLeft: 8, + }, + items: [ + { + type: 'itemGroup', + name: 'enableActions', + title: "{{t('Enable actions')}}", + children: [ + { + name: 'filter', + title: "{{t('Filter')}}", + Component: 'FilterActionInitializer', + schema: { + 'x-align': 'left', + }, + }, + { + name: 'addNew', + title: "{{t('Add new')}}", + Component: 'CreateActionInitializer', + schema: { + 'x-align': 'right', + 'x-decorator': 'ACLActionProvider', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + }, + useVisible() { + const collection = useCollection_deprecated(); + return ( + (collection.template !== 'view' || collection?.writableView) && + collection.template !== 'file' && + collection.template !== 'sql' + ); + }, + }, + { + name: 'refresh', + title: "{{t('Refresh')}}", + Component: 'RefreshActionInitializer', + schema: { + 'x-align': 'right', + }, + }, + { + name: 'import', + title: "{{t('Import')}}", + Component: 'ImportActionInitializer', + schema: { + 'x-align': 'right', + 'x-acl-action': 'importXlsx', + 'x-decorator': 'ACLActionProvider', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + }, + useVisible() { + const collection = useCollection_deprecated(); + return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; + }, + }, + { + name: 'export', + title: "{{t('Export')}}", + Component: 'ExportActionInitializer', + schema: { + 'x-align': 'right', + 'x-decorator': 'ACLActionProvider', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + }, + }, + ], + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/items/ListBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/list/ListBlockInitializer.tsx similarity index 56% rename from packages/core/client/src/schema-initializer/items/ListBlockInitializer.tsx rename to packages/core/client/src/modules/blocks/data-blocks/list/ListBlockInitializer.tsx index 6602a050d..9271db04f 100644 --- a/packages/core/client/src/schema-initializer/items/ListBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/list/ListBlockInitializer.tsx @@ -1,12 +1,12 @@ -import React from 'react'; import { OrderedListOutlined } from '@ant-design/icons'; -import { createListBlockSchema } from '../utils'; -import { DataBlockInitializer } from './DataBlockInitializer'; -import { useCollectionManager } from '../../collection-manager'; -import { useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import React from 'react'; +import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; +import { useCollectionManager_deprecated } from '../../../../collection-manager'; +import { createListBlockSchema } from '../../../../schema-initializer/utils'; +import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer'; export const ListBlockInitializer = () => { - const { getCollection } = useCollectionManager(); + const { getCollection } = useCollectionManager_deprecated(); const { insert } = useSchemaInitializer(); const itemConfig = useSchemaInitializerItem(); return ( @@ -15,10 +15,12 @@ export const ListBlockInitializer = () => { icon={} componentType={'List'} onCreateBlockSchema={async ({ item }) => { - const collection = getCollection(item.name); + const collection = getCollection(item.name, item.dataSource); const schema = createListBlockSchema({ collection: item.name, + dataSource: item.dataSource, rowKey: collection.filterTargetKey || 'id', + settings: 'blockSettings:list', }); insert(schema); }} diff --git a/packages/core/client/src/modules/list/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts similarity index 100% rename from packages/core/client/src/modules/list/__e2e__/schemaInitializer.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts diff --git a/packages/core/client/src/modules/list/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaSettings.test.ts similarity index 100% rename from packages/core/client/src/modules/list/__e2e__/schemaSettings.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaSettings.test.ts diff --git a/packages/core/client/src/modules/form-filter/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/templatesOfBug.ts similarity index 100% rename from packages/core/client/src/modules/form-filter/__e2e__/templatesOfBug.ts rename to packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/templatesOfBug.ts diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/listBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/list/listBlockSettings.ts new file mode 100644 index 000000000..3f73310bb --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/list/listBlockSettings.ts @@ -0,0 +1,223 @@ +import { ArrayItems } from '@formily/antd-v5'; +import { ISchema, useField, useFieldSchema } from '@formily/react'; +import _ from 'lodash'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useFormBlockContext } from '../../../../block-provider'; +import { useCollection_deprecated, useSortFields } from '../../../../collection-manager'; +import { removeNullCondition, useDesignable } from '../../../../schema-component'; +import { SchemaSettingsBlockTitleItem, SchemaSettingsTemplate } from '../../../../schema-settings'; +import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope'; + +export const listBlockSettings = new SchemaSettings({ + name: 'blockSettings:list', + items: [ + { + name: 'EditBlockTitle', + Component: SchemaSettingsBlockTitleItem, + }, + { + name: 'SetTheDataScope', + Component: SchemaSettingsDataScope, + useComponentProps() { + const { name } = useCollection_deprecated(); + const fieldSchema = useFieldSchema(); + const { form } = useFormBlockContext(); + const field = useField(); + const { dn } = useDesignable(); + + return { + collectionName: name, + defaultFilter: fieldSchema?.['x-decorator-props']?.params?.filter || {}, + form: form, + onSubmit: ({ filter }) => { + filter = removeNullCondition(filter); + _.set(fieldSchema, 'x-decorator-props.params.filter', filter); + field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params, page: 1 }; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }, + }; + }, + }, + { + name: 'SetDefaultSortingRules', + type: 'modal', + useComponentProps() { + const { name } = useCollection_deprecated(); + const { t } = useTranslation(); + const fieldSchema = useFieldSchema(); + const field = useField(); + const { dn } = useDesignable(); + const sortFields = useSortFields(name); + const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; + const sort = defaultSort?.map((item: string) => { + return item.startsWith('-') + ? { + field: item.substring(1), + direction: 'desc', + } + : { + field: item, + direction: 'asc', + }; + }); + + return { + title: t('Set default sorting rules'), + components: { ArrayItems }, + schema: { + type: 'object', + title: t('Set default sorting rules'), + properties: { + sort: { + type: 'array', + default: sort, + 'x-component': 'ArrayItems', + 'x-decorator': 'FormItem', + items: { + type: 'object', + properties: { + space: { + type: 'void', + 'x-component': 'Space', + properties: { + sort: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.SortHandle', + }, + field: { + type: 'string', + enum: sortFields, + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-component-props': { + style: { + width: 260, + }, + }, + }, + direction: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Radio.Group', + 'x-component-props': { + optionType: 'button', + }, + enum: [ + { + label: t('ASC'), + value: 'asc', + }, + { + label: t('DESC'), + value: 'desc', + }, + ], + }, + remove: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.Remove', + }, + }, + }, + }, + }, + properties: { + add: { + type: 'void', + title: t('Add sort field'), + 'x-component': 'ArrayItems.Addition', + }, + }, + }, + }, + } as ISchema, + onSubmit: ({ sort }) => { + const sortArr = sort.map((item) => { + return item.direction === 'desc' ? `-${item.field}` : item.field; + }); + + _.set(fieldSchema, 'x-decorator-props.params.sort', sortArr); + field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params, page: 1 }; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }, + }; + }, + }, + { + name: 'RecordsPerPage', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const fieldSchema = useFieldSchema(); + const field = useField(); + const { dn } = useDesignable(); + + return { + title: t('Records per page'), + value: field.decoratorProps?.params?.pageSize || 20, + options: [ + { label: '10', value: 10 }, + { label: '20', value: 20 }, + { label: '50', value: 50 }, + { label: '80', value: 80 }, + { label: '100', value: 100 }, + ], + onChange: (pageSize) => { + _.set(fieldSchema, 'x-decorator-props.params.pageSize', pageSize); + field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params, page: 1 }; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }, + }; + }, + }, + { + name: 'ConvertReferenceToDuplicate', + Component: SchemaSettingsTemplate, + useComponentProps() { + const { name } = useCollection_deprecated(); + const fieldSchema = useFieldSchema(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + + return { + componentName: 'List', + collectionName: name, + resourceName: defaultResource, + }; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'delete', + type: 'remove', + useComponentProps() { + return { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/listItemActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/list/listItemActionInitializers.tsx new file mode 100644 index 000000000..063c4f60c --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/list/listItemActionInitializers.tsx @@ -0,0 +1,100 @@ +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; +import { useCollection_deprecated } from '../../../../collection-manager'; + +export const listItemActionInitializers = new SchemaInitializer({ + name: 'ListItemActionInitializers', + title: '{{t("Configure actions")}}', + icon: 'SettingOutlined', + items: [ + { + type: 'itemGroup', + name: 'enableActions', + title: '{{t("Enable actions")}}', + children: [ + { + name: 'view', + title: '{{t("View")}}', + Component: 'ViewActionInitializer', + schema: { + 'x-component': 'Action.Link', + 'x-action': 'view', + 'x-decorator': 'ACLActionProvider', + 'x-align': 'left', + }, + }, + { + name: 'edit', + title: '{{t("Edit")}}', + Component: 'UpdateActionInitializer', + schema: { + 'x-component': 'Action.Link', + 'x-action': 'update', + 'x-decorator': 'ACLActionProvider', + 'x-align': 'left', + }, + useVisible() { + const collection = useCollection_deprecated(); + return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; + }, + }, + { + name: 'delete', + title: '{{t("Delete")}}', + Component: 'DestroyActionInitializer', + schema: { + 'x-component': 'Action.Link', + 'x-action': 'destroy', + 'x-decorator': 'ACLActionProvider', + 'x-align': 'left', + }, + useVisible() { + const collection = useCollection_deprecated(); + return collection.template !== 'sql'; + }, + }, + ], + }, + { + name: 'divider', + type: 'divider', + }, + { + type: 'subMenu', + title: '{{t("Customize")}}', + name: 'customize', + children: [ + { + name: 'popup', + title: '{{t("Popup")}}', + Component: 'PopupActionInitializer', + useComponentProps() { + return { + 'x-component': 'Action.Link', + }; + }, + }, + { + name: 'updateRecord', + title: '{{t("Update record")}}', + Component: 'UpdateRecordActionInitializer', + useVisible() { + const collection = useCollection_deprecated(); + return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; + }, + }, + { + name: 'customRequest', + title: '{{t("Custom request")}}', + Component: 'CustomRequestInitializer', + schema: { + 'x-action': 'customize:table:request', + }, + useVisible() { + const collection = useCollection_deprecated(); + return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; + }, + }, + ], + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/items/TableSelectorInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/table-selector/TableSelectorInitializer.tsx similarity index 67% rename from packages/core/client/src/schema-initializer/items/TableSelectorInitializer.tsx rename to packages/core/client/src/modules/blocks/data-blocks/table-selector/TableSelectorInitializer.tsx index 9bdcaa5c5..85733dc98 100644 --- a/packages/core/client/src/schema-initializer/items/TableSelectorInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table-selector/TableSelectorInitializer.tsx @@ -1,24 +1,25 @@ import { FormOutlined } from '@ant-design/icons'; import React from 'react'; -import { useCollection } from '../../collection-manager'; -import { createTableSelectorSchema } from '../utils'; -import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import { useCollection_deprecated } from '../../../../collection-manager'; +import { createTableSelectorSchema } from '../../../../schema-initializer/utils'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; export const TableSelectorInitializer = () => { const itemConfig = useSchemaInitializerItem(); const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; const { insert } = useSchemaInitializer(); - const collection = useCollection(); + const collection = useCollection_deprecated(); return ( } {...others} - onClick={async ({ item }) => { + onClick={async () => { insert( createTableSelectorSchema({ rowKey: collection.filterTargetKey, collection: collection.name, + dataSource: collection.dataSource, resource: collection.name, }), ); diff --git a/packages/core/client/src/modules/table-data-selector/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/schemaInitializer.test.ts similarity index 100% rename from packages/core/client/src/modules/table-data-selector/__e2e__/schemaInitializer.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/schemaInitializer.test.ts diff --git a/packages/core/client/src/modules/table-data-selector/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/schemaSettings.test.ts similarity index 85% rename from packages/core/client/src/modules/table-data-selector/__e2e__/schemaSettings.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/schemaSettings.test.ts index a1c659878..140f6c45f 100644 --- a/packages/core/client/src/modules/table-data-selector/__e2e__/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/schemaSettings.test.ts @@ -15,5 +15,5 @@ test.describe('table data selector schema settings', () => { async function showSettingsMenu(page) { await page.getByLabel('block-item-CardItem-users-table-selector').hover(); - await page.getByLabel('designer-schema-settings-CardItem-TableSelectorDesigner-users').hover(); + await page.getByLabel('designer-schema-settings-CardItem-blockSettings:tableSelector-users').hover(); } diff --git a/packages/core/client/src/modules/grid-card/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/templatesOfBug.ts similarity index 100% rename from packages/core/client/src/modules/grid-card/__e2e__/templatesOfBug.ts rename to packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/templatesOfBug.ts diff --git a/packages/core/client/src/modules/table-data-selector/__e2e__/utils.ts b/packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/utils.ts similarity index 100% rename from packages/core/client/src/modules/table-data-selector/__e2e__/utils.ts rename to packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/utils.ts diff --git a/packages/core/client/src/modules/blocks/data-blocks/table-selector/index.ts b/packages/core/client/src/modules/blocks/data-blocks/table-selector/index.ts new file mode 100644 index 000000000..4b3e254fb --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/table-selector/index.ts @@ -0,0 +1,2 @@ +export * from './TableSelectorInitializer'; +export * from './tableSelectorBlockSettings'; diff --git a/packages/core/client/src/modules/blocks/data-blocks/table-selector/tableSelectorBlockSettings.ts b/packages/core/client/src/modules/blocks/data-blocks/table-selector/tableSelectorBlockSettings.ts new file mode 100644 index 000000000..ff736f6ef --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/table-selector/tableSelectorBlockSettings.ts @@ -0,0 +1,279 @@ +import { ArrayItems } from '@formily/antd-v5'; +import { ISchema, useField, useFieldSchema } from '@formily/react'; +import { cloneDeep } from 'lodash'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { recursiveParent, useFormBlockContext, useTableSelectorContext } from '../../../../block-provider'; +import { + useCollection_deprecated, + useCollectionManager_deprecated, + useSortFields, +} from '../../../../collection-manager'; +import { removeNullCondition, useDesignable } from '../../../../schema-component'; +import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope'; + +export const tableSelectorBlockSettings = new SchemaSettings({ + name: 'blockSettings:tableSelector', + items: [ + { + name: 'SetTheDataScope', + Component: SchemaSettingsDataScope, + useComponentProps: () => { + const { name } = useCollection_deprecated(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { form } = useFormBlockContext(); + const { service, extraFilter } = useTableSelectorContext(); + const { dn } = useDesignable(); + const onDataScopeSubmit = useCallback( + ({ filter }) => { + filter = removeNullCondition(filter); + const params = field.decoratorProps.params || {}; + params.filter = filter; + field.decoratorProps.params = params; + fieldSchema['x-decorator-props']['params'] = params; + let serviceFilter = cloneDeep(filter); + if (extraFilter) { + if (serviceFilter) { + serviceFilter = { + $and: [extraFilter, serviceFilter], + }; + } else { + serviceFilter = extraFilter; + } + } + service.run({ ...service.params?.[0], filter: serviceFilter, page: 1 }); + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }, + [dn, field.decoratorProps, fieldSchema, service, extraFilter], + ); + + return { + collectionName: name, + defaultFilter: fieldSchema?.['x-decorator-props']?.params?.filter || {}, + form: form, + onSubmit: onDataScopeSubmit, + }; + }, + }, + { + name: 'treeTable', + type: 'switch', + useComponentProps: () => { + const field = useField(); + const fieldSchema = useFieldSchema(); + const { service } = useTableSelectorContext(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + + return { + title: t('Tree table'), + defaultChecked: true, + checked: field.decoratorProps.treeTable !== false, + onChange: (flag) => { + field.form.clearFormGraph(`${field.address}.*`); + field.decoratorProps.treeTable = flag; + fieldSchema['x-decorator-props'].treeTable = flag; + const params = { + ...service.params?.[0], + tree: flag ? true : null, + }; + dn.emit('patch', { + schema: fieldSchema, + }); + dn.refresh(); + service.run(params); + }, + }; + }, + useVisible: () => { + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const fieldSchema = useFieldSchema(); + const collection = useCollection_deprecated(); + const collectionFieldSchema = recursiveParent(fieldSchema, 'CollectionField'); + const collectionField = getCollectionJoinField(collectionFieldSchema?.['x-collection-field']); + + return collection?.tree && collectionField?.target === collectionField?.collectionName; + }, + }, + { + name: 'SetDefaultSortingRules', + type: 'modal', + useComponentProps() { + const { name } = useCollection_deprecated(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const sortFields = useSortFields(name); + const { service } = useTableSelectorContext(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; + const sort = defaultSort?.map((item: string) => { + return item.startsWith('-') + ? { + field: item.substring(1), + direction: 'desc', + } + : { + field: item, + direction: 'asc', + }; + }); + + return { + title: t('Set default sorting rules'), + components: { ArrayItems }, + schema: { + type: 'object', + title: t('Set default sorting rules'), + properties: { + sort: { + type: 'array', + default: sort, + 'x-component': 'ArrayItems', + 'x-decorator': 'FormItem', + items: { + type: 'object', + properties: { + space: { + type: 'void', + 'x-component': 'Space', + properties: { + sort: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.SortHandle', + }, + field: { + type: 'string', + enum: sortFields, + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-component-props': { + style: { + width: 260, + }, + }, + }, + direction: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Radio.Group', + 'x-component-props': { + optionType: 'button', + }, + enum: [ + { + label: t('ASC'), + value: 'asc', + }, + { + label: t('DESC'), + value: 'desc', + }, + ], + }, + remove: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.Remove', + }, + }, + }, + }, + }, + properties: { + add: { + type: 'void', + title: t('Add sort field'), + 'x-component': 'ArrayItems.Addition', + }, + }, + }, + }, + } as ISchema, + onSubmit: ({ sort }) => { + const sortArr = sort.map((item) => { + return item.direction === 'desc' ? `-${item.field}` : item.field; + }); + const params = field.decoratorProps.params || {}; + params.sort = sortArr; + field.decoratorProps.params = params; + fieldSchema['x-decorator-props']['params'] = params; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + service.run({ ...service.params?.[0], sort: sortArr }); + }, + }; + }, + useVisible() { + const field = useField(); + const { dragSort } = field.decoratorProps; + return !dragSort; + }, + }, + { + name: 'RecordsPerPage', + type: 'select', + useComponentProps() { + const field = useField(); + const fieldSchema = useFieldSchema(); + const { service } = useTableSelectorContext(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + + return { + title: t('Records per page'), + value: field.decoratorProps?.params?.pageSize || 20, + options: [ + { label: '10', value: 10 }, + { label: '20', value: 20 }, + { label: '50', value: 50 }, + { label: '100', value: 100 }, + { label: '200', value: 200 }, + ], + onChange: (pageSize) => { + const params = field.decoratorProps.params || {}; + params.pageSize = pageSize; + field.decoratorProps.params = params; + fieldSchema['x-decorator-props']['params'] = params; + service.run({ ...service.params?.[0], pageSize, page: 1 }); + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }, + }; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'delete', + type: 'remove', + useComponentProps() { + return { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }; + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/TableActionColumnInitializers.tsx similarity index 66% rename from packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx rename to packages/core/client/src/modules/blocks/data-blocks/table/TableActionColumnInitializers.tsx index c23571282..5262b2e86 100644 --- a/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/TableActionColumnInitializers.tsx @@ -3,12 +3,12 @@ import { ISchema, useFieldSchema } from '@formily/react'; import _ from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useAPIClient } from '../../api-client'; -import { SchemaInitializerActionModal, SchemaInitializerItem, useSchemaInitializer } from '../../application'; -import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; -import { useCollection } from '../../collection-manager'; -import { createDesignable, useDesignable } from '../../schema-component'; -import { useGetAriaLabelOfDesigner } from '../../schema-settings/hooks/useGetAriaLabelOfDesigner'; +import { useAPIClient } from '../../../../api-client'; +import { SchemaInitializerActionModal, SchemaInitializerItem, useSchemaInitializer } from '../../../../application'; +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; +import { useCollection_deprecated } from '../../../../collection-manager'; +import { createDesignable, useDesignable } from '../../../../schema-component'; +import { useGetAriaLabelOfDesigner } from '../../../../schema-settings/hooks/useGetAriaLabelOfDesigner'; export const Resizable = () => { const { t } = useTranslation(); @@ -132,7 +132,7 @@ export const tableActionColumnInitializers = new SchemaInitializer({ 'x-decorator': 'ACLActionProvider', }, useVisible() { - const collection = useCollection(); + const collection = useCollection_deprecated(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, }, @@ -147,7 +147,7 @@ export const tableActionColumnInitializers = new SchemaInitializer({ 'x-decorator': 'ACLActionProvider', }, useVisible() { - const collection = useCollection(); + const collection = useCollection_deprecated(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, }, @@ -163,7 +163,7 @@ export const tableActionColumnInitializers = new SchemaInitializer({ }, useVisible() { const fieldSchema = useFieldSchema(); - const collection = useCollection(); + const collection = useCollection_deprecated(); const { treeTable } = fieldSchema?.parent?.parent['x-decorator-props'] || {}; return collection.tree && treeTable !== false; }, @@ -183,79 +183,15 @@ export const tableActionColumnInitializers = new SchemaInitializer({ type: 'item', title: '{{t("Popup")}}', name: 'popup', - Component: 'CustomizeActionInitializer', - schema: { - type: 'void', - title: '{{ t("Popup") }}', - 'x-action': 'customize:popup', - 'x-designer': 'Action.Designer', - 'x-component': 'Action.Link', - 'x-component-props': { - openMode: 'drawer', - }, - properties: { - drawer: { - type: 'void', - title: '{{ t("Popup") }}', - 'x-component': 'Action.Container', - 'x-component-props': { - className: 'nb-action-popup', - }, - properties: { - tabs: { - type: 'void', - 'x-component': 'Tabs', - 'x-component-props': {}, - 'x-initializer': 'TabPaneInitializers', - properties: { - tab1: { - type: 'void', - title: '{{t("Details")}}', - 'x-component': 'Tabs.TabPane', - 'x-designer': 'Tabs.Designer', - 'x-component-props': {}, - properties: { - grid: { - type: 'void', - 'x-component': 'Grid', - 'x-initializer': 'RecordBlockInitializers', - properties: {}, - }, - }, - }, - }, - }, - }, - }, - }, - }, + Component: 'PopupActionInitializer', }, { type: 'item', title: '{{t("Update record")}}', name: 'updateRecord', - Component: 'CustomizeActionInitializer', - schema: { - title: '{{t("Update record")}}', - 'x-component': 'Action.Link', - 'x-action': 'customize:update', - 'x-decorator': 'ACLActionProvider', - 'x-acl-action': 'update', - 'x-designer': 'Action.Designer', - 'x-action-settings': { - assignedValues: {}, - onSuccess: { - manualClose: true, - redirecting: false, - successMessage: '{{t("Updated successfully")}}', - }, - }, - 'x-component-props': { - useProps: '{{ useCustomizeUpdateActionProps }}', - }, - }, + Component: 'UpdateRecordActionInitializer', useVisible() { - const collection = useCollection(); + const collection = useCollection_deprecated(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, }, @@ -267,14 +203,14 @@ export const tableActionColumnInitializers = new SchemaInitializer({ 'x-action': 'customize:table:request', }, useVisible() { - const collection = useCollection(); + const collection = useCollection_deprecated(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, }, ], }, { - name: 'divider', + name: 'divider2', type: 'divider', }, { diff --git a/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/TableActionInitializers.tsx similarity index 85% rename from packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx rename to packages/core/client/src/modules/blocks/data-blocks/table/TableActionInitializers.tsx index 0fc882026..c4028aedd 100644 --- a/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/TableActionInitializers.tsx @@ -1,6 +1,6 @@ import { useFieldSchema } from '@formily/react'; -import { useCollection } from '../../'; -import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; +import { useCollection_deprecated } from '../../../..'; +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; // 表格操作配置 export const tableActionInitializers = new SchemaInitializer({ @@ -38,7 +38,7 @@ export const tableActionInitializers = new SchemaInitializer({ }, }, useVisible() { - const collection = useCollection(); + const collection = useCollection_deprecated(); return !['view', 'file', 'sql'].includes(collection.template) || collection?.writableView; }, }, @@ -52,7 +52,7 @@ export const tableActionInitializers = new SchemaInitializer({ 'x-decorator': 'ACLActionProvider', }, useVisible() { - const collection = useCollection(); + const collection = useCollection_deprecated(); return !['view', 'sql'].includes(collection.template) || collection?.writableView; }, }, @@ -68,13 +68,13 @@ export const tableActionInitializers = new SchemaInitializer({ { name: 'toggle', title: "{{t('Expand/Collapse')}}", - Component: 'ExpandActionInitializer', + Component: 'ExpandableActionInitializer', schema: { 'x-align': 'right', }, useVisible() { const schema = useFieldSchema(); - const collection = useCollection(); + const collection = useCollection_deprecated(); const { treeTable } = schema?.parent?.['x-decorator-props'] || {}; return collection.tree && treeTable !== false; }, @@ -85,7 +85,7 @@ export const tableActionInitializers = new SchemaInitializer({ name: 'divider', type: 'divider', useVisible() { - const collection = useCollection(); + const collection = useCollection_deprecated(); return !['view', 'sql'].includes(collection.template) || collection?.writableView; }, }, @@ -110,7 +110,7 @@ export const tableActionInitializers = new SchemaInitializer({ }, ], useVisible() { - const collection = useCollection(); + const collection = useCollection_deprecated(); return !['view', 'sql'].includes(collection.template) || collection?.writableView; }, }, diff --git a/packages/core/client/src/schema-initializer/items/TableBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/TableBlockInitializer.tsx similarity index 55% rename from packages/core/client/src/schema-initializer/items/TableBlockInitializer.tsx rename to packages/core/client/src/modules/blocks/data-blocks/table/TableBlockInitializer.tsx index fc6701381..7bdd89c42 100644 --- a/packages/core/client/src/schema-initializer/items/TableBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/TableBlockInitializer.tsx @@ -1,13 +1,13 @@ -import React from 'react'; import { TableOutlined } from '@ant-design/icons'; -import { useCollectionManager } from '../../collection-manager'; -import { DataBlockInitializer } from './DataBlockInitializer'; -import { createTableBlockSchema } from '../utils'; -import { useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application/schema-initializer/context'; +import { useCollectionManager_deprecated } from '../../../../collection-manager/hooks/useCollectionManager_deprecated'; +import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer'; +import { createTableBlockSchema } from '../../../../schema-initializer/utils'; +import React from 'react'; export const TableBlockInitializer = () => { const { insert } = useSchemaInitializer(); - const { getCollection } = useCollectionManager(); + const { getCollection } = useCollectionManager_deprecated(); const itemConfig = useSchemaInitializerItem(); return ( { icon={} componentType={'Table'} onCreateBlockSchema={async ({ item }) => { - const collection = getCollection(item.name); + const collection = getCollection(item.name, item.dataSource); const schema = createTableBlockSchema({ collection: item.name, + dataSource: item.dataSource, rowKey: collection.filterTargetKey || 'id', }); insert(schema); diff --git a/packages/core/client/src/schema-initializer/buttons/TableColumnInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/TableColumnInitializers.tsx similarity index 86% rename from packages/core/client/src/schema-initializer/buttons/TableColumnInitializers.tsx rename to packages/core/client/src/modules/blocks/data-blocks/table/TableColumnInitializers.tsx index 240876a10..c892e4c06 100644 --- a/packages/core/client/src/schema-initializer/buttons/TableColumnInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/TableColumnInitializers.tsx @@ -1,14 +1,14 @@ import { useFieldSchema } from '@formily/react'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { SchemaInitializerChildren } from '../../application'; -import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; -import { useCompile } from '../../schema-component'; +import { SchemaInitializerChildren } from '../../../../application'; +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; +import { useCompile } from '../../../../schema-component'; import { useAssociatedTableColumnInitializerFields, useInheritsTableColumnInitializerFields, useTableColumnInitializerFields, -} from '../utils'; +} from '../../../../schema-initializer/utils'; // 表格列配置 const ParentCollectionFields = () => { @@ -52,14 +52,16 @@ export const tableColumnInitializers = new SchemaInitializer({ insertPosition: 'beforeEnd', icon: 'SettingOutlined', title: '{{t("Configure columns")}}', - wrap: (s) => { + wrap: (s, { isInSubTable }) => { if (s['x-action-column']) { return s; } return { type: 'void', 'x-decorator': 'TableV2.Column.Decorator', - 'x-designer': 'TableV2.Column.Designer', + // 'x-designer': 'TableV2.Column.Designer', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', 'x-component': 'TableV2.Column', properties: { [s.name]: { diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/TableColumnSchemaToolbar.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/TableColumnSchemaToolbar.tsx new file mode 100644 index 000000000..39365850e --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/table/TableColumnSchemaToolbar.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { SchemaToolbar } from '../../../../schema-settings'; + +export const TableColumnSchemaToolbar = (props) => { + return ; +}; diff --git a/packages/core/client/src/modules/table/__e2e__/actions/duplicate.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/actions/duplicate.test.ts similarity index 100% rename from packages/core/client/src/modules/table/__e2e__/actions/duplicate.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/actions/duplicate.test.ts diff --git a/packages/core/client/src/modules/table/__e2e__/actions/filter.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/actions/filter.test.ts similarity index 100% rename from packages/core/client/src/modules/table/__e2e__/actions/filter.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/actions/filter.test.ts diff --git a/packages/core/client/src/modules/table/__e2e__/checkboxForTableRow.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/checkboxForTableRow.test.ts similarity index 100% rename from packages/core/client/src/modules/table/__e2e__/checkboxForTableRow.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/checkboxForTableRow.test.ts diff --git a/packages/core/client/src/modules/table/__e2e__/dragAndDrop.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/dragAndDrop.test.ts similarity index 100% rename from packages/core/client/src/modules/table/__e2e__/dragAndDrop.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/dragAndDrop.test.ts diff --git a/packages/core/client/src/modules/table/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts similarity index 100% rename from packages/core/client/src/modules/table/__e2e__/schemaInitializer.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts diff --git a/packages/core/client/src/modules/table/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts similarity index 96% rename from packages/core/client/src/modules/table/__e2e__/schemaSettings.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts index bcaf65e6e..36c36afb3 100644 --- a/packages/core/client/src/modules/table/__e2e__/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts @@ -31,7 +31,8 @@ test.describe('table block schema settings', () => { }); }); - test('fix block', async ({ page, mockPage }) => { + // 现有固定区块功能会被废弃,现 skip 掉 + test.skip('fix block', async ({ page, mockPage }) => { await mockPage(oneTableBlockWithAddNewAndViewAndEditAndBasicFields).goto(); const tableSize = await page.getByLabel('block-item-CardItem-general-table').boundingBox(); @@ -94,15 +95,18 @@ test.describe('table block schema settings', () => { // 开启之后,隐藏 Set default sorting rules 选项 await page.getByRole('menuitem', { name: 'Enable drag and drop sorting' }).click(); + await page.getByText('Drag and drop sorting field').click(); + await page.getByText('sort', { exact: true }).click(); await expect( page.getByRole('menuitem', { name: 'Enable drag and drop sorting' }).getByRole('switch'), ).toBeChecked(); await expect(page.getByRole('menuitem', { name: 'Set default sorting rules' })).toBeHidden(); - // 显示出来 email 和 ID await page.getByLabel('schema-initializer-TableV2-TableColumnInitializers-general').hover(); await page.getByRole('menuitem', { name: 'email' }).click(); await page.getByRole('menuitem', { name: 'ID', exact: true }).click(); + await page.getByLabel('schema-initializer-TableV2-').click(); + await page.mouse.move(300, 0); // 默认的排序 @@ -120,7 +124,6 @@ test.describe('table block schema settings', () => { .dragTo(page.getByLabel('table-index-1').getByRole('img', { name: 'menu' })); await page.reload(); - await page.waitForLoadState('networkidle'); email1 = await page.getByText(records[0].email).boundingBox(); email2 = await page.getByText(records[1].email).boundingBox(); @@ -267,21 +270,19 @@ test.describe('table block schema settings', () => { // 点击左边 Table 的某一行 await page.getByLabel('block-item-CardItem-roles-table').getByRole('cell', { name: 'Root', exact: true }).click(); await expect( - page - .getByLabel('block-item-CardItem-users-table') - .getByRole('row', { name: 'Member' }) - .filter({ hasNotText: 'Root' }), - ).toHaveCount(0, { timeout: 1000 * 10 }); + page.getByLabel('block-item-CardItem-users-table').getByRole('row').filter({ hasNotText: 'Root' }), + ).toHaveCount(1, { + timeout: 1000 * 10, + }); await expect(page.getByLabel('block-item-CardItem-users-table').getByRole('row', { name: 'Root' })).toBeVisible(); // 再次点击,会取消筛选效果 await page.getByLabel('block-item-CardItem-roles-table').getByRole('cell', { name: 'Root', exact: true }).click(); await expect( - page - .getByLabel('block-item-CardItem-users-table') - .getByRole('row', { name: 'Member' }) - .filter({ hasNotText: 'Root' }), - ).toHaveCount(3, { timeout: 1000 * 10 }); + page.getByLabel('block-item-CardItem-users-table').getByRole('row').filter({ hasNotText: 'Root' }), + ).toHaveCount(4, { + timeout: 1000 * 10, + }); await expect(page.getByLabel('block-item-CardItem-users-table').getByRole('row', { name: 'Root' })).toBeVisible(); }); @@ -582,7 +583,9 @@ test.describe('actions schema settings', () => { const showMenu = async (page: Page) => { await page.getByLabel('action-Action.Link-Popup-customize:popup-general-table-0').hover(); - await page.getByRole('button', { name: 'designer-schema-settings-Action.Link-Action.Designer-general' }).hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:popup-general' }) + .hover(); }; test('supported options', async ({ page, mockPage, mockRecord }) => { @@ -637,7 +640,9 @@ test.describe('actions schema settings', () => { const showMenu = async (page: Page) => { await page.getByLabel('action-Action.Link-Update record-customize:update-general-table-0').hover(); - await page.getByRole('button', { name: 'designer-schema-settings-Action.Link-Action.Designer-general' }).hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:updateRecord-general' }) + .hover(); }; test('supported options', async ({ page, mockPage, mockRecord }) => { @@ -664,7 +669,7 @@ test.describe('actions schema settings', () => { test.describe('add child', () => { const showMenu = async (page: Page) => { await page.getByLabel('action-Action.Link-Add child-create-treeCollection-table-0').hover(); - await page.getByLabel('designer-schema-settings-Action.Link-Action.Designer-tree').hover(); + await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:addChild-tree').hover(); }; test('supported options', async ({ page, mockPage, mockRecord }) => { @@ -685,7 +690,7 @@ test.describe('actions schema settings', () => { await page.getByRole('button', { name: 'Submit' }).click(); // 添加 add child 按钮 - await page.getByRole('button', { name: 'Actions' }).hover(); + await page.getByRole('button', { name: 'Actions', exact: true }).hover(); await page.getByLabel('designer-schema-settings-TableV2.Column-TableV2.ActionColumnDesigner-tree').hover(); await page.getByRole('menuitem', { name: 'Add child' }).click(); diff --git a/packages/core/client/src/modules/table/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts similarity index 100% rename from packages/core/client/src/modules/table/__e2e__/templatesOfBug.ts rename to packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts diff --git a/packages/core/client/src/modules/table-tree/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/tree/schemaSettings.test.ts similarity index 100% rename from packages/core/client/src/modules/table-tree/__e2e__/schemaSettings.test.ts rename to packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/tree/schemaSettings.test.ts diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/index.ts b/packages/core/client/src/modules/blocks/data-blocks/table/index.ts new file mode 100644 index 000000000..643720b49 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/table/index.ts @@ -0,0 +1,7 @@ +export * from './TableActionColumnInitializers'; +export * from './TableActionInitializers'; +export * from './TableBlockInitializer'; +export * from './TableColumnSchemaToolbar'; +export * from './tableBlockSettings'; +export * from './tableColumnSettings'; +export * from './TableColumnInitializers'; diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx new file mode 100644 index 000000000..1a97d3374 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx @@ -0,0 +1,385 @@ +import { ISchema } from '@formily/json-schema'; +import { useField, useFieldSchema } from '@formily/react'; +import { useAPIClient } from '../../../../api-client'; +import { useTableBlockContext, useFormBlockContext } from '../../../../block-provider'; +import { + useCollectionManager_deprecated, + useCollection_deprecated, + useSortFields, +} from '../../../../collection-manager'; +import { mergeFilter, FilterBlockType } from '../../../../filter-provider'; +import { useDesignable, removeNullCondition } from '../../../../schema-component'; +import { + SchemaSettingsBlockTitleItem, + SchemaSettingsSortField, + SchemaSettingsDataScope, + SchemaSettingsConnectDataBlocks, + SchemaSettingsTemplate, +} from '../../../../schema-settings'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ArrayItems } from '@formily/antd-v5'; +import { FixedBlockDesignerItem } from '../../../../schema-component/antd/page/FixedBlockDesignerItem'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; + +export const tableBlockSettings = new SchemaSettings({ + name: 'blockSettings:table', + items: [ + { + name: 'editBlockTitle', + Component: SchemaSettingsBlockTitleItem, + }, + { + name: 'treeTable', + type: 'switch', + useComponentProps: () => { + const { getCollectionField, getCollection } = useCollectionManager_deprecated(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { service } = useTableBlockContext(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + const collection = useCollection_deprecated(); + const { resource } = field.decoratorProps; + const collectionField = resource && getCollectionField(resource); + const treeCollection = resource?.includes('.') + ? getCollection(collectionField?.target)?.tree + : !!collection?.tree; + + return { + title: t('Tree table'), + defaultChecked: true, + checked: treeCollection ? field.decoratorProps.treeTable !== false : false, + onChange: (flag) => { + field.decoratorProps.treeTable = flag; + fieldSchema['x-decorator-props'].treeTable = flag; + const params = { + ...service.params?.[0], + tree: flag ? true : null, + }; + dn.emit('patch', { + schema: fieldSchema, + }); + dn.refresh(); + service.run(params); + }, + }; + }, + useVisible: () => { + const { getCollectionField } = useCollectionManager_deprecated(); + const field = useField(); + const collection = useCollection_deprecated(); + const { resource } = field.decoratorProps; + const collectionField = resource && getCollectionField(resource); + + return collection?.tree && collectionField?.collectionName === collectionField?.target; + }, + }, + { + name: 'enableDragAndDropSorting', + type: 'switch', + useComponentProps: () => { + const { getCollectionField } = useCollectionManager_deprecated(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { service } = useTableBlockContext(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + const { resource } = field.decoratorProps; + const collectionField = resource && getCollectionField(resource); + const api = useAPIClient(); + + return { + title: t('Enable drag and drop sorting'), + checked: field.decoratorProps.dragSort, + onChange: async (dragSort) => { + if (dragSort && collectionField) { + const { data } = await api.resource('collections.fields', collectionField.collectionName).update({ + filterByTk: collectionField.name, + values: { + sortable: true, + }, + }); + const sortBy = data?.data?.[0]?.sortBy; + fieldSchema['x-decorator-props'].dragSortBy = sortBy; + } + field.decoratorProps.dragSort = dragSort; + fieldSchema['x-decorator-props'].dragSort = dragSort; + service.run({ ...service.params?.[0], sort: fieldSchema['x-decorator-props'].dragSortBy }); + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }, + }; + }, + }, + { + name: 'SortField', + Component: SchemaSettingsSortField, + useVisible() { + const field = useField(); + return field.decoratorProps.dragSort; + }, + }, + { + name: 'FixBlock', + Component: FixedBlockDesignerItem, + }, + { + name: 'SetTheDataScope', + Component: SchemaSettingsDataScope, + useComponentProps: () => { + const { name } = useCollection_deprecated(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { form } = useFormBlockContext(); + const { service } = useTableBlockContext(); + const { dn } = useDesignable(); + const onDataScopeSubmit = useCallback( + ({ filter }) => { + filter = removeNullCondition(filter); + const params = field.decoratorProps.params || {}; + params.filter = filter; + field.decoratorProps.params = params; + fieldSchema['x-decorator-props']['params'] = params; + const filters = service.params?.[1]?.filters || {}; + service.run( + { ...service.params?.[0], filter: mergeFilter([...Object.values(filters), filter]), page: 1 }, + { filters }, + ); + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }, + [dn, field.decoratorProps, fieldSchema, service], + ); + + return { + collectionName: name, + defaultFilter: fieldSchema?.['x-decorator-props']?.params?.filter || {}, + form: form, + onSubmit: onDataScopeSubmit, + }; + }, + }, + { + name: 'SetDefaultSortingRules', + type: 'modal', + useComponentProps() { + const { name } = useCollection_deprecated(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const sortFields = useSortFields(name); + const { service } = useTableBlockContext(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; + const sort = defaultSort?.map((item: string) => { + return item?.startsWith('-') + ? { + field: item.substring(1), + direction: 'desc', + } + : { + field: item, + direction: 'asc', + }; + }); + + return { + title: t('Set default sorting rules'), + components: { ArrayItems }, + schema: { + type: 'object', + title: t('Set default sorting rules'), + properties: { + sort: { + type: 'array', + default: sort, + 'x-component': 'ArrayItems', + 'x-decorator': 'FormItem', + items: { + type: 'object', + properties: { + space: { + type: 'void', + 'x-component': 'Space', + properties: { + sort: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.SortHandle', + }, + field: { + type: 'string', + enum: sortFields, + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-component-props': { + style: { + width: 260, + }, + }, + }, + direction: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Radio.Group', + 'x-component-props': { + optionType: 'button', + }, + enum: [ + { + label: t('ASC'), + value: 'asc', + }, + { + label: t('DESC'), + value: 'desc', + }, + ], + }, + remove: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.Remove', + }, + }, + }, + }, + }, + properties: { + add: { + type: 'void', + title: t('Add sort field'), + 'x-component': 'ArrayItems.Addition', + }, + }, + }, + }, + } as ISchema, + onSubmit: ({ sort }) => { + const sortArr = sort.map((item) => { + return item.direction === 'desc' ? `-${item.field}` : item.field; + }); + const params = field.decoratorProps.params || {}; + params.sort = sortArr; + field.decoratorProps.params = params; + fieldSchema['x-decorator-props']['params'] = params; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + service.run({ ...service.params?.[0], sort: sortArr }); + }, + }; + }, + useVisible() { + const field = useField(); + const { dragSort } = field.decoratorProps; + + return !dragSort; + }, + }, + { + name: 'RecordsPerPage', + type: 'select', + useComponentProps() { + const field = useField(); + const fieldSchema = useFieldSchema(); + const { service } = useTableBlockContext(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + + return { + title: t('Records per page'), + value: field.decoratorProps?.params?.pageSize || 20, + options: [ + { label: '10', value: 10 }, + { label: '20', value: 20 }, + { label: '50', value: 50 }, + { label: '100', value: 100 }, + { label: '200', value: 200 }, + ], + onChange: (pageSize) => { + const params = field.decoratorProps.params || {}; + params.pageSize = pageSize; + field.decoratorProps.params = params; + fieldSchema['x-decorator-props']['params'] = params; + service.run({ ...service.params?.[0], pageSize, page: 1 }); + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }, + }; + }, + }, + { + name: 'ConnectDataBlocks', + Component: SchemaSettingsConnectDataBlocks, + useComponentProps() { + const { t } = useTranslation(); + return { + type: FilterBlockType.TABLE, + emptyDescription: t('No blocks to connect'), + }; + }, + }, + { + name: 'divider', + type: 'divider', + useVisible: () => { + const fieldSchema = useFieldSchema(); + const supportTemplate = !fieldSchema?.['x-decorator-props']?.disableTemplate; + return supportTemplate; + }, + }, + { + name: 'ConvertReferenceToDuplicate', + Component: SchemaSettingsTemplate, + useComponentProps() { + const { name } = useCollection_deprecated(); + const fieldSchema = useFieldSchema(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + return { + componentName: 'Table', + collectionName: name, + resourceName: defaultResource, + }; + }, + useVisible: () => { + const fieldSchema = useFieldSchema(); + const supportTemplate = !fieldSchema?.['x-decorator-props']?.disableTemplate; + return supportTemplate; + }, + }, + { + name: 'divider2', + type: 'divider', + }, + { + name: 'delete', + type: 'remove', + useComponentProps: () => { + return { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/tableColumnSettings.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/tableColumnSettings.tsx new file mode 100644 index 000000000..e3f93c109 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/table/tableColumnSettings.tsx @@ -0,0 +1,338 @@ +import { ISchema } from '@formily/json-schema'; +import { useField, useFieldSchema } from '@formily/react'; +import { useTranslation } from 'react-i18next'; +import { useApp } from '../../../../application'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useCollectionManager_deprecated } from '../../../../collection-manager'; +import { useDesignable } from '../../../../schema-component'; +import { useAssociationFieldContext } from '../../../../schema-component/antd/association-field/hooks'; +import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator'; +import { SchemaSettingsDefaultValue, isPatternDisabled } from '../../../../schema-settings'; +import { useFieldComponentName } from './utils'; + +export const tableColumnSettings = new SchemaSettings({ + name: 'fieldSettings:TableColumn', + items: [ + { + name: 'decoratorOptions', + type: 'itemGroup', + hideIfNoChildren: true, + useComponentProps() { + const { t } = useTranslation(); + return { + title: t('Generic properties'), + }; + }, + children: [ + { + name: 'customColumnTitle', + type: 'modal', + useComponentProps() { + const { fieldSchema, collectionField } = useColumnSchema(); + const field: any = useField(); + const { t } = useTranslation(); + const columnSchema = useFieldSchema(); + const { dn } = useDesignable(); + + return { + title: t('Custom column title'), + schema: { + type: 'object', + title: t('Custom column title'), + properties: { + title: { + title: t('Column title'), + default: columnSchema?.title, + description: `${t('Original field title: ')}${ + collectionField?.uiSchema?.title || fieldSchema?.title + }`, + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-component-props': {}, + }, + }, + } as ISchema, + onSubmit: ({ title }) => { + if (title) { + field.title = title; + columnSchema.title = title; + dn.emit('patch', { + schema: { + 'x-uid': columnSchema['x-uid'], + title: columnSchema.title, + }, + }); + } + dn.refresh(); + }, + }; + }, + }, + { + name: 'columnWidth', + type: 'modal', + useComponentProps() { + const field: any = useField(); + const { t } = useTranslation(); + const columnSchema = useFieldSchema(); + const { dn } = useDesignable(); + + return { + title: t('Column width'), + schema: { + type: 'object', + title: t('Column width'), + properties: { + width: { + default: columnSchema?.['x-component-props']?.['width'] || 200, + 'x-decorator': 'FormItem', + 'x-component': 'InputNumber', + 'x-component-props': {}, + }, + }, + } as ISchema, + onSubmit: ({ width }) => { + const props = columnSchema['x-component-props'] || {}; + props['width'] = width; + const schema: ISchema = { + ['x-uid']: columnSchema['x-uid'], + }; + schema['x-component-props'] = props; + columnSchema['x-component-props'] = props; + field.componentProps.width = width; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, + }, + { + name: 'sortable', + type: 'switch', + useVisible() { + const { collectionField } = useColumnSchema(); + const { getInterface } = useCollectionManager_deprecated(); + const interfaceCfg = getInterface(collectionField?.interface); + const { currentMode } = useAssociationFieldContext(); + + return interfaceCfg?.sortable === true && !currentMode; + }, + useComponentProps() { + const field: any = useField(); + const { t } = useTranslation(); + const columnSchema = useFieldSchema(); + const { dn } = useDesignable(); + + return { + title: t('Sortable'), + checked: field.componentProps.sorter, + onChange: (v) => { + const schema: ISchema = { + ['x-uid']: columnSchema['x-uid'], + }; + columnSchema['x-component-props'] = { + ...columnSchema['x-component-props'], + sorter: v, + }; + schema['x-component-props'] = columnSchema['x-component-props']; + field.componentProps.sorter = v; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, + }, + { + name: 'setDefaultValue', + useVisible() { + const { fieldSchema } = useColumnSchema(); + return !fieldSchema['x-read-pretty']; + }, + Component: SchemaSettingsDefaultValue as any, + useComponentProps() { + const { fieldSchema } = useColumnSchema(); + return { + fieldSchema, + }; + }, + }, + { + name: 'required', + type: 'switch', + useVisible() { + const { uiSchema, fieldSchema } = useColumnSchema(); + const field: any = useField(); + const isSubTableColumn = ['QuickEdit', 'FormItem'].includes(fieldSchema['x-decorator']); + return isSubTableColumn && !field.readPretty && !uiSchema?.['x-read-pretty']; + }, + useComponentProps() { + const { fieldSchema } = useColumnSchema(); + const field: any = useField(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + + return { + key: 'required', + title: t('Required'), + checked: fieldSchema.required as boolean, + onChange: (required) => { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['required'] = required; + schema['required'] = required; + const path = field.path?.splice(field.path?.length - 1, 1); + field.form.query(`${path.concat(`*.` + fieldSchema.name)}`).forEach((f) => { + f.required = required; + }); + dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, + }, + { + name: 'pattern', + type: 'select', + useVisible() { + const { fieldSchema, collectionField } = useColumnSchema(); + const field: any = useField(); + const isSubTableColumn = ['QuickEdit', 'FormItem'].includes(fieldSchema['x-decorator']); + return ( + isSubTableColumn && + !field?.readPretty && + collectionField?.interface !== 'o2m' && + !isPatternDisabled(fieldSchema) + ); + }, + useComponentProps() { + const { fieldSchema } = useColumnSchema(); + const field: any = useField(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + let readOnlyMode = 'editable'; + if (fieldSchema['x-disabled'] === true) { + readOnlyMode = 'readonly'; + } + if (fieldSchema['x-read-pretty'] === true) { + readOnlyMode = 'read-pretty'; + } + + return { + key: 'pattern', + title: t('Pattern'), + options: [ + { label: t('Editable'), value: 'editable' }, + { label: t('Readonly'), value: 'readonly' }, + { label: t('Easy-reading'), value: 'read-pretty' }, + ], + value: readOnlyMode, + onChange: (v) => { + const schema: ISchema = { + ['x-uid']: fieldSchema['x-uid'], + }; + const path = field.path?.splice(field.path?.length - 1, 1); + switch (v) { + case 'readonly': { + fieldSchema['x-read-pretty'] = false; + fieldSchema['x-disabled'] = true; + schema['x-read-pretty'] = false; + schema['x-disabled'] = true; + field.form.query(`${path.concat(`*.` + fieldSchema.name)}`).forEach((f) => { + f.readonly = true; + f.disabled = true; + }); + break; + } + case 'read-pretty': { + fieldSchema['x-read-pretty'] = true; + fieldSchema['x-disabled'] = false; + schema['x-read-pretty'] = true; + schema['x-disabled'] = false; + field.form.query(`${path.concat(`*.` + fieldSchema.name)}`).forEach((f) => { + f.readPretty = true; + }); + break; + } + default: { + fieldSchema['x-read-pretty'] = false; + fieldSchema['x-disabled'] = false; + schema['x-read-pretty'] = false; + schema['x-disabled'] = false; + field.form.query(`${path.concat(`*.` + fieldSchema.name)}`).forEach((f) => { + f.readPretty = false; + f.disabled + false; + }); + break; + } + } + + dn.emit('patch', { + schema, + }); + + dn.refresh(); + }, + }; + }, + }, + ], + }, + { + name: 'componentOptions', + type: 'itemGroup', + hideIfNoChildren: true, + useComponentProps() { + const { t } = useTranslation(); + return { + title: t('Specific properties'), + }; + }, + useChildren() { + const app = useApp(); + const fieldComponentName = useFieldComponentName(); + const map = { + Select: 'Select', + DatePicker: 'DatePicker', + Nester: 'Nester', + SubTable: 'SubTable', + Picker: 'Picker', + PopoverNester: 'PopoverNester', + Tag: 'Tag', + }; + const componentSettings = app.schemaSettingsManager.get( + `fieldSettings:component:${map[fieldComponentName] || fieldComponentName}`, + ); + return componentSettings?.items || []; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'delete', + type: 'remove', + sort: 100, + useComponentProps() { + const { t } = useTranslation(); + + return { + removeParentsIfNoChildren: true, + confirm: { + title: t('Delete field'), + }, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/utils.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/utils.tsx new file mode 100644 index 000000000..de54e3c7a --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/table/utils.tsx @@ -0,0 +1,22 @@ +import { Field } from '@formily/core'; +import { useField, useFieldSchema } from '@formily/react'; +import { useIsFileField } from '../../../../schema-component'; +import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator'; + +export function useFieldComponentName(): string { + const { fieldSchema: tableColumnSchema, collectionField } = useColumnSchema(); + const field = useField(); + const isFileField = useIsFileField(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const map = { + // AssociationField 的 mode 默认值是 Select + AssociationField: 'Select', + }; + const fieldComponentName = + fieldSchema?.['x-component-props']?.['mode'] || + field?.componentProps?.['mode'] || + (isFileField ? 'FileManager' : '') || + collectionField?.uiSchema?.['x-component']; + return map[fieldComponentName] || fieldComponentName; +} diff --git a/packages/core/client/src/modules/blocks/filter-blocks/collapse/CollapseItemSchemaToolbar.tsx b/packages/core/client/src/modules/blocks/filter-blocks/collapse/CollapseItemSchemaToolbar.tsx new file mode 100644 index 000000000..17d7bbab3 --- /dev/null +++ b/packages/core/client/src/modules/blocks/filter-blocks/collapse/CollapseItemSchemaToolbar.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { SchemaToolbar } from '../../../../schema-settings'; + +export const CollapseItemSchemaToolbar = (props) => { + return ; +}; diff --git a/packages/core/client/src/schema-initializer/items/FilterCollapseBlockInitializer.tsx b/packages/core/client/src/modules/blocks/filter-blocks/collapse/FilterCollapseBlockInitializer.tsx similarity index 52% rename from packages/core/client/src/schema-initializer/items/FilterCollapseBlockInitializer.tsx rename to packages/core/client/src/modules/blocks/filter-blocks/collapse/FilterCollapseBlockInitializer.tsx index 1d6af4924..a7e7ad999 100644 --- a/packages/core/client/src/schema-initializer/items/FilterCollapseBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/filter-blocks/collapse/FilterCollapseBlockInitializer.tsx @@ -1,28 +1,37 @@ import { TableOutlined } from '@ant-design/icons'; import React from 'react'; -import { useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -import { createCollapseBlockSchema } from '../utils'; -import { DataBlockInitializer } from './DataBlockInitializer'; +import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; +import { createCollapseBlockSchema } from '../../../../schema-initializer/utils'; +import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer'; +import { Collection } from '../../../../data-source'; -export const FilterCollapseBlockInitializer = ({ filterItems }) => { +export const FilterCollapseBlockInitializer = ({ + filterMenuItemChildren, + onlyCurrentDataSource, +}: { + filterMenuItemChildren: (collection: Collection) => boolean; + onlyCurrentDataSource: boolean; +}) => { const itemConfig = useSchemaInitializerItem(); const { insert } = useSchemaInitializer(); return ( } componentType={'FilterCollapse'} onCreateBlockSchema={async ({ item }) => { const schema = createCollapseBlockSchema({ + dataSource: item.dataSource, collection: item.collectionName || item.name, // 与数据区块做区分 blockType: 'filter', }); insert(schema); }} - filterItems={filterItems} + filter={filterMenuItemChildren} /> ); }; diff --git a/packages/core/client/src/modules/collapse-filter/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/filter-blocks/collapse/__e2e__/schemaInitializer.test.ts similarity index 100% rename from packages/core/client/src/modules/collapse-filter/__e2e__/schemaInitializer.test.ts rename to packages/core/client/src/modules/blocks/filter-blocks/collapse/__e2e__/schemaInitializer.test.ts diff --git a/packages/core/client/src/modules/collapse-filter/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/filter-blocks/collapse/__e2e__/schemaSettings.test.ts similarity index 100% rename from packages/core/client/src/modules/collapse-filter/__e2e__/schemaSettings.test.ts rename to packages/core/client/src/modules/blocks/filter-blocks/collapse/__e2e__/schemaSettings.test.ts diff --git a/packages/core/client/src/modules/list/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/blocks/filter-blocks/collapse/__e2e__/templatesOfBug.ts similarity index 100% rename from packages/core/client/src/modules/list/__e2e__/templatesOfBug.ts rename to packages/core/client/src/modules/blocks/filter-blocks/collapse/__e2e__/templatesOfBug.ts diff --git a/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseBlockSettings.ts b/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseBlockSettings.ts new file mode 100644 index 000000000..68d8b5263 --- /dev/null +++ b/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseBlockSettings.ts @@ -0,0 +1,63 @@ +import { useFieldSchema } from '@formily/react'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useCollection_deprecated } from '../../../../collection-manager'; +import { FilterBlockType } from '../../../../filter-provider'; +import { + SchemaSettingsBlockTitleItem, + SchemaSettingsConnectDataBlocks, + SchemaSettingsTemplate, +} from '../../../../schema-settings'; + +export const filterCollapseBlockSettings = new SchemaSettings({ + name: 'blockSettings:filterCollapse', + items: [ + { + name: 'EditBlockTitle', + Component: SchemaSettingsBlockTitleItem, + }, + { + name: 'ConvertReferenceToDuplicate', + Component: SchemaSettingsTemplate, + useComponentProps() { + const { name } = useCollection_deprecated(); + const fieldSchema = useFieldSchema(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + + return { + componentName: 'FilterCollapse', + collectionName: name, + resourceName: defaultResource, + }; + }, + }, + { + name: 'ConnectDataBlocks', + Component: SchemaSettingsConnectDataBlocks, + useComponentProps() { + const { t } = useTranslation(); + + return { + type: FilterBlockType.COLLAPSE, + emptyDescription: t('No blocks to connect'), + }; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'delete', + type: 'remove', + useComponentProps() { + return { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseItemFieldSettings.ts b/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseItemFieldSettings.ts new file mode 100644 index 000000000..63ed930f7 --- /dev/null +++ b/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseItemFieldSettings.ts @@ -0,0 +1,214 @@ +import { ISchema, useField, useFieldSchema } from '@formily/react'; +import _ from 'lodash'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useFormBlockContext } from '../../../../block-provider'; +import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../../collection-manager'; +import { useCompile, useDesignable } from '../../../../schema-component'; +import { SchemaSettingsDefaultSortingRules } from '../../../../schema-settings'; +import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope'; + +export const filterCollapseItemFieldSettings = new SchemaSettings({ + name: 'fieldSettings:FilterCollapseItem', + items: [ + { + name: 'decoratorOptions', + type: 'itemGroup', + hideIfNoChildren: true, + useComponentProps() { + const { t } = useTranslation(); + return { + title: t('Generic properties'), + }; + }, + useChildren() { + return [ + { + name: 'customTitle', + type: 'modal', + useComponentProps() { + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); + const collectionField = + getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + const { dn } = useDesignable(); + + return { + title: t('Custom title'), + schema: { + type: 'object', + title: t('Custom title'), + properties: { + title: { + default: fieldSchema?.title, + description: `${t('Original title: ')}${collectionField?.uiSchema?.title || fieldSchema?.title}`, + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-component-props': {}, + }, + }, + } as ISchema, + onSubmit: ({ title }) => { + if (title) { + // field.title = title; + fieldSchema.title = title; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + title: fieldSchema.title, + }, + }); + } + dn.refresh(); + }, + }; + }, + }, + { + name: 'defaultCollapse', + type: 'switch', + useComponentProps() { + const fieldSchema = useFieldSchema(); + const field = useField(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + + return { + title: t('Default collapse'), + checked: field.componentProps.defaultCollapse, + onChange: (v) => { + field.componentProps.defaultCollapse = v; + fieldSchema['x-component-props']['defaultCollapse'] = v; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-component-props': fieldSchema['x-component-props'], + }, + }); + dn.refresh(); + }, + }; + }, + }, + { + name: 'setTheDataScope', + Component: SchemaSettingsDataScope, + useComponentProps() { + const fieldSchema = useFieldSchema(); + const { form } = useFormBlockContext(); + const field = useField(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); + const collectionField = + getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + const { dn } = useDesignable(); + + return { + collectionName: collectionField?.target, + defaultFilter: fieldSchema?.['x-component-props']?.params?.filter || {}, + form: form, + onSubmit: ({ filter }) => { + _.set(field.componentProps, 'params', { + ...field.componentProps?.params, + filter, + }); + fieldSchema['x-component-props']['params'] = field.componentProps.params; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-component-props': fieldSchema['x-component-props'], + }, + }); + }, + }; + }, + }, + { + name: 'setDefaultSortingRules', + Component: SchemaSettingsDefaultSortingRules, + useComponentProps() { + const fieldSchema = useFieldSchema(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); + const collectionField = + getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + + return { + name: collectionField?.target, + }; + }, + }, + { + name: 'titleField', + type: 'select', + useComponentProps() { + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); + const collectionField = + getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + const { getCollectionFields } = useCollectionManager_deprecated(); + const compile = useCompile(); + const { dn } = useDesignable(); + const targetFields = collectionField?.target ? getCollectionFields(collectionField?.target) : []; + const options = targetFields + .filter((field) => !field?.target && field.type !== 'boolean') + .map((field) => ({ + value: field?.name, + label: compile(field?.uiSchema?.title) || field?.name, + })); + const onTitleFieldChange = (label) => { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + const fieldNames = { + label, + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['fieldNames'] = fieldNames; + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }; + + return { + key: 'title-field', + title: t('Title field'), + options: options, + value: fieldSchema['x-component-props']?.fieldNames?.label, + onChange: onTitleFieldChange, + }; + }, + }, + ]; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'delete', + type: 'remove', + sort: 100, + useComponentProps() { + const { t } = useTranslation(); + + return { + removeParentsIfNoChildren: true, + confirm: { + title: t('Delete field'), + }, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseItemInitializer.tsx b/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseItemInitializer.tsx new file mode 100644 index 000000000..d403f06f8 --- /dev/null +++ b/packages/core/client/src/modules/blocks/filter-blocks/collapse/filterCollapseItemInitializer.tsx @@ -0,0 +1,157 @@ +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; +import { useOptionalFieldList } from '../../../../block-provider/hooks'; +import { useCollectionManager_deprecated } from '../../../../collection-manager'; +import { useAssociatedFields } from '../../../../filter-provider/utils'; + +/** + * @deprecated + */ +export const associationFilterFilterBlockInitializer = new SchemaInitializer({ + name: 'AssociationFilter.FilterBlockInitializer', + style: { marginTop: 16 }, + icon: 'SettingOutlined', + title: '{{t("Configure fields")}}', + items: [ + { + type: 'itemGroup', + name: 'associationFields', + title: '{{t("Association fields")}}', + useChildren() { + const associatedFields = useAssociatedFields(); + const useProps = '{{useAssociationFilterBlockProps}}'; + const children = associatedFields.map((field) => ({ + name: field.key, + title: field.uiSchema?.title, + Component: 'AssociationFilterDesignerDisplayField', + schema: { + name: field.name, + title: field.uiSchema?.title, + type: 'void', + // 'x-designer': 'AssociationFilter.Item.Designer', + 'x-toolbar': 'CollapseItemSchemaToolbar', + 'x-settings': 'fieldSettings:FilterCollapseItem', + 'x-component': 'AssociationFilter.Item', + 'x-component-props': { + fieldNames: { + label: field.targetKey || 'id', + }, + useProps, + }, + properties: {}, + }, + })); + return children; + }, + }, + { + name: 'choicesFields', + type: 'itemGroup', + title: '{{t("Choices fields")}}', + hideIfNoChildren: true, + useChildren() { + const optionalList = useOptionalFieldList(); + const useProps = '{{useAssociationFilterBlockProps}}'; + const optionalChildren = optionalList.map((field) => ({ + name: field.key, + title: field.uiSchema.title, + Component: 'AssociationFilterDesignerDisplayField', + schema: { + name: field.name, + title: field.uiSchema.title, + interface: field.interface, + type: 'void', + // 'x-designer': 'AssociationFilter.Item.Designer', + 'x-toolbar': 'CollapseItemSchemaToolbar', + 'x-settings': 'fieldSettings:FilterCollapseItem', + 'x-component': 'AssociationFilter.Item', + 'x-component-props': { + fieldNames: { + label: field.name, + }, + useProps, + }, + properties: {}, + }, + })); + + return optionalChildren; + }, + }, + ], +}); + +export const filterCollapseItemInitializer = new SchemaInitializer({ + // name: 'AssociationFilter.FilterBlockInitializer', + name: 'AssociationFilterInitializers', + style: { marginTop: 16 }, + icon: 'SettingOutlined', + title: '{{t("Configure fields")}}', + items: [ + { + type: 'itemGroup', + name: 'associationFields', + title: '{{t("Association fields")}}', + useChildren() { + const associatedFields = useAssociatedFields(); + const useProps = '{{useAssociationFilterBlockProps}}'; + const cm = useCollectionManager_deprecated(); + const children = associatedFields.map((field) => ({ + name: field.key, + title: field.uiSchema?.title, + Component: 'AssociationFilterDesignerDisplayField', + schema: { + name: field.name, + title: field.uiSchema?.title, + type: 'void', + // 'x-designer': 'AssociationFilter.Item.Designer', + 'x-toolbar': 'CollapseItemSchemaToolbar', + 'x-settings': 'fieldSettings:FilterCollapseItem', + 'x-component': 'AssociationFilter.Item', + 'x-component-props': { + fieldNames: { + label: cm.getCollection(field.target)?.getPrimaryKey() || 'id', + }, + useProps, + }, + properties: {}, + }, + })); + return children; + }, + }, + { + name: 'choicesFields', + type: 'itemGroup', + title: '{{t("Choices fields")}}', + hideIfNoChildren: true, + useChildren() { + const optionalList = useOptionalFieldList(); + const useProps = '{{useAssociationFilterBlockProps}}'; + const optionalChildren = optionalList.map((field) => ({ + name: field.key, + title: field.uiSchema.title, + Component: 'AssociationFilterDesignerDisplayField', + schema: { + name: field.name, + title: field.uiSchema.title, + interface: field.interface, + type: 'void', + // 'x-designer': 'AssociationFilter.Item.Designer', + 'x-toolbar': 'CollapseItemSchemaToolbar', + 'x-settings': 'fieldSettings:FilterCollapseItem', + 'x-component': 'AssociationFilter.Item', + 'x-component-props': { + fieldNames: { + label: field.name, + }, + useProps, + }, + properties: {}, + }, + })); + + return optionalChildren; + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/FilterFormActionInitializers.tsx b/packages/core/client/src/modules/blocks/filter-blocks/form/FilterFormActionInitializers.tsx similarity index 88% rename from packages/core/client/src/schema-initializer/buttons/FilterFormActionInitializers.tsx rename to packages/core/client/src/modules/blocks/filter-blocks/form/FilterFormActionInitializers.tsx index 340fcd56a..12be8c91c 100644 --- a/packages/core/client/src/schema-initializer/buttons/FilterFormActionInitializers.tsx +++ b/packages/core/client/src/modules/blocks/filter-blocks/form/FilterFormActionInitializers.tsx @@ -1,4 +1,4 @@ -import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; // 表单的操作配置 export const filterFormActionInitializers = new SchemaInitializer({ diff --git a/packages/core/client/src/modules/blocks/filter-blocks/form/FilterFormBlockInitializer.tsx b/packages/core/client/src/modules/blocks/filter-blocks/form/FilterFormBlockInitializer.tsx new file mode 100644 index 000000000..a6aa6e28f --- /dev/null +++ b/packages/core/client/src/modules/blocks/filter-blocks/form/FilterFormBlockInitializer.tsx @@ -0,0 +1,42 @@ +import { FormOutlined } from '@ant-design/icons'; +import React from 'react'; +import { useSchemaInitializerItem } from '../../../../application'; +import { createFilterFormBlockSchema } from '../../../../schema-initializer/utils'; +import { FilterBlockInitializer } from '../../../../schema-initializer/items/FilterBlockInitializer'; +import { Collection } from '../../../../data-source'; + +export const FilterFormBlockInitializer = ({ + filterMenuItemChildren, + onlyCurrentDataSource, +}: { + filterMenuItemChildren: (collection: Collection) => boolean; + onlyCurrentDataSource: boolean; +}) => { + const itemConfig = useSchemaInitializerItem(); + + return ( + } + onlyCurrentDataSource={onlyCurrentDataSource} + componentType={'FilterFormItem'} + templateWrap={(templateSchema, { item }) => { + const s = createFilterFormBlockSchema({ + template: templateSchema, + dataSource: item.dataSource, + collection: item.name || item.collectionName, + settings: 'blockSettings:filterForm', + }); + if (item.template && item.mode === 'reference') { + s['x-template-key'] = item.template.key; + } + return s; + }} + createBlockSchema={(options) => { + options = { ...options, settings: 'blockSettings:filterForm' }; + return createFilterFormBlockSchema(options); + }} + filter={filterMenuItemChildren} + /> + ); +}; diff --git a/packages/core/client/src/modules/form-filter/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/schemaInitializer.test.ts similarity index 100% rename from packages/core/client/src/modules/form-filter/__e2e__/schemaInitializer.test.ts rename to packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/schemaInitializer.test.ts diff --git a/packages/core/client/src/modules/form-filter/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/schemaSettings.test.ts similarity index 100% rename from packages/core/client/src/modules/form-filter/__e2e__/schemaSettings.test.ts rename to packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/schemaSettings.test.ts diff --git a/packages/core/client/src/modules/markdown/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/templatesOfBug.ts similarity index 100% rename from packages/core/client/src/modules/markdown/__e2e__/templatesOfBug.ts rename to packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/templatesOfBug.ts diff --git a/packages/core/client/src/schema-component/antd/form-v2/Form.FilterSettings.tsx b/packages/core/client/src/modules/blocks/filter-blocks/form/filterFormBlockSettings.ts similarity index 75% rename from packages/core/client/src/schema-component/antd/form-v2/Form.FilterSettings.tsx rename to packages/core/client/src/modules/blocks/filter-blocks/form/filterFormBlockSettings.ts index 990110c98..707e9aa67 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/Form.FilterSettings.tsx +++ b/packages/core/client/src/modules/blocks/filter-blocks/form/filterFormBlockSettings.ts @@ -1,17 +1,17 @@ import { useFieldSchema } from '@formily/react'; -import { SchemaSettings } from '../../../application/schema-settings'; -import { useCollection } from '../../../collection-manager'; -import { FilterBlockType } from '../../../filter-provider'; import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useCollection_deprecated } from '../../../../collection-manager'; +import { FilterBlockType } from '../../../../filter-provider'; import { + SchemaSettingsBlockTitleItem, + SchemaSettingsConnectDataBlocks, SchemaSettingsFormItemTemplate, SchemaSettingsLinkageRules, - SchemaSettingsConnectDataBlocks, - SchemaSettingsBlockTitleItem, -} from '../../../schema-settings'; +} from '../../../../schema-settings'; -export const formFilterSettings = new SchemaSettings({ - name: 'FormFilterSettings', +export const filterFormBlockSettings = new SchemaSettings({ + name: 'blockSettings:filterForm', items: [ { name: 'title', @@ -21,7 +21,7 @@ export const formFilterSettings = new SchemaSettings({ name: 'formItemTemplate', Component: SchemaSettingsFormItemTemplate, useComponentProps() { - const { name } = useCollection(); + const { name } = useCollection_deprecated(); const fieldSchema = useFieldSchema(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource; return { @@ -35,7 +35,7 @@ export const formFilterSettings = new SchemaSettings({ name: 'linkageRules', Component: SchemaSettingsLinkageRules, useComponentProps() { - const { name } = useCollection(); + const { name } = useCollection_deprecated(); return { collectionName: name, }; diff --git a/packages/core/client/src/modules/blocks/filter-blocks/form/filterFormItemFieldSettings.ts b/packages/core/client/src/modules/blocks/filter-blocks/form/filterFormItemFieldSettings.ts new file mode 100644 index 000000000..1bb204988 --- /dev/null +++ b/packages/core/client/src/modules/blocks/filter-blocks/form/filterFormItemFieldSettings.ts @@ -0,0 +1,359 @@ +import { ArrayCollapse, FormLayout } from '@formily/antd-v5'; +import { Field } from '@formily/core'; +import { ISchema, useField, useFieldSchema } from '@formily/react'; +import _ from 'lodash'; +import { useTranslation } from 'react-i18next'; +import { useApp } from '../../../../application'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../../collection-manager'; +import { useFieldComponentName } from '../../../../common/useFieldComponentName'; +import { EditOperator, useDesignable, useValidateSchema } from '../../../../schema-component'; + +export const filterFormItemFieldSettings = new SchemaSettings({ + name: 'fieldSettings:FilterFormItem', + items: [ + { + name: 'decoratorOptions', + type: 'itemGroup', + hideIfNoChildren: true, + useComponentProps() { + const { t } = useTranslation(); + return { + title: t('Generic properties'), + }; + }, + useChildren() { + return [ + { + name: 'editFieldTitle', + type: 'modal', + useComponentProps() { + const { t } = useTranslation(); + const { dn } = useDesignable(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); + const collectionField = + getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + + return { + title: t('Edit field title'), + schema: { + type: 'object', + title: t('Edit field title'), + properties: { + title: { + title: t('Field title'), + default: field?.title, + description: `${t('Original field title: ')}${collectionField?.uiSchema?.title}`, + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-component-props': {}, + }, + }, + } as ISchema, + onSubmit({ title }) { + if (title) { + field.title = title; + fieldSchema.title = title; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + title: fieldSchema.title, + }, + }); + } + dn.refresh(); + }, + }; + }, + }, + { + name: 'displayTitle', + type: 'switch', + useComponentProps() { + const { t } = useTranslation(); + const { dn } = useDesignable(); + const field = useField(); + const fieldSchema = useFieldSchema(); + + return { + title: t('Display title'), + checked: fieldSchema['x-decorator-props']?.['showTitle'] ?? true, + onChange(checked) { + fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {}; + fieldSchema['x-decorator-props']['showTitle'] = checked; + field.decoratorProps.showTitle = checked; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + 'x-decorator-props': { + ...fieldSchema['x-decorator-props'], + showTitle: checked, + }, + }, + }); + dn.refresh(); + }, + }; + }, + }, + { + name: 'editDescription', + type: 'modal', + useComponentProps() { + const { t } = useTranslation(); + const { dn } = useDesignable(); + const field = useField(); + const fieldSchema = useFieldSchema(); + + return { + title: t('Edit description'), + schema: { + type: 'object', + title: t('Edit description'), + properties: { + description: { + // title: t('Description'), + default: field?.description, + 'x-decorator': 'FormItem', + 'x-component': 'Input.TextArea', + 'x-component-props': {}, + }, + }, + } as ISchema, + onSubmit({ description }) { + field.description = description; + fieldSchema.description = description; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + description: fieldSchema.description, + }, + }); + dn.refresh(); + }, + }; + }, + }, + { + name: 'editTooltip', + type: 'modal', + useComponentProps() { + const { t } = useTranslation(); + const { dn } = useDesignable(); + const field = useField(); + const fieldSchema = useFieldSchema(); + + return { + title: t('Edit tooltip'), + schema: { + type: 'object', + title: t('Edit tooltip'), + properties: { + tooltip: { + default: fieldSchema?.['x-decorator-props']?.tooltip, + 'x-decorator': 'FormItem', + 'x-component': 'Input.TextArea', + 'x-component-props': {}, + }, + }, + } as ISchema, + onSubmit({ tooltip }) { + field.decoratorProps.tooltip = tooltip; + fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {}; + fieldSchema['x-decorator-props']['tooltip'] = tooltip; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + dn.refresh(); + }, + }; + }, + }, + { + name: 'setValidationRules', + type: 'modal', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn, refresh } = useDesignable(); + const validateSchema = useValidateSchema(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); + const collectionField = + getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + + return { + title: t('Set validation rules'), + components: { ArrayCollapse, FormLayout }, + schema: { + type: 'object', + title: t('Set validation rules'), + properties: { + rules: { + type: 'array', + default: fieldSchema?.['x-validator'], + 'x-component': 'ArrayCollapse', + 'x-decorator': 'FormItem', + 'x-component-props': { + accordion: true, + }, + maxItems: 3, + items: { + type: 'object', + 'x-component': 'ArrayCollapse.CollapsePanel', + 'x-component-props': { + header: '{{ t("Validation rule") }}', + }, + properties: { + index: { + type: 'void', + 'x-component': 'ArrayCollapse.Index', + }, + layout: { + type: 'void', + 'x-component': 'FormLayout', + 'x-component-props': { + labelStyle: { + marginTop: '6px', + }, + labelCol: 8, + wrapperCol: 16, + }, + properties: { + ...validateSchema, + message: { + type: 'string', + title: '{{ t("Error message") }}', + 'x-decorator': 'FormItem', + 'x-component': 'Input.TextArea', + 'x-component-props': { + autoSize: { + minRows: 2, + maxRows: 2, + }, + }, + }, + }, + }, + remove: { + type: 'void', + 'x-component': 'ArrayCollapse.Remove', + }, + moveUp: { + type: 'void', + 'x-component': 'ArrayCollapse.MoveUp', + }, + moveDown: { + type: 'void', + 'x-component': 'ArrayCollapse.MoveDown', + }, + }, + }, + properties: { + add: { + type: 'void', + title: '{{ t("Add validation rule") }}', + 'x-component': 'ArrayCollapse.Addition', + 'x-reactions': { + dependencies: ['rules'], + fulfill: { + state: { + disabled: '{{$deps[0].length >= 3}}', + }, + }, + }, + }, + }, + }, + }, + } as ISchema, + onSubmit(v) { + const rules = []; + for (const rule of v.rules) { + rules.push(_.pickBy(rule, _.identity)); + } + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + if (['percent'].includes(collectionField?.interface)) { + for (const rule of rules) { + if (!!rule.maxValue || !!rule.minValue) { + rule['percentMode'] = true; + } + + if (rule.percentFormat) { + rule['percentFormats'] = true; + } + } + } + const concatValidator = _.concat([], collectionField?.uiSchema?.['x-validator'] || [], rules); + field.validator = concatValidator; + fieldSchema['x-validator'] = rules; + schema['x-validator'] = rules; + dn.emit('patch', { + schema, + }); + refresh(); + }, + }; + }, + useVisible() { + const validateSchema = useValidateSchema(); + return !!validateSchema; + }, + }, + { + name: 'operator', + Component: EditOperator, + }, + ]; + }, + }, + { + name: 'componentOptions', + type: 'itemGroup', + hideIfNoChildren: true, + useComponentProps() { + const { t } = useTranslation(); + return { + title: t('Specific properties'), + }; + }, + useChildren() { + const app = useApp(); + const fieldComponentName = useFieldComponentName(); + const componentSettings = app.schemaSettingsManager.get(`fieldSettings:component:${fieldComponentName}`); + return componentSettings?.items || []; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'delete', + type: 'remove', + sort: 100, + useComponentProps() { + const { t } = useTranslation(); + + return { + removeParentsIfNoChildren: true, + confirm: { + title: t('Delete field'), + }, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/blocks/filter-blocks/form/filterFormItemInitializers.tsx b/packages/core/client/src/modules/blocks/filter-blocks/form/filterFormItemInitializers.tsx new file mode 100644 index 000000000..0e8206f3e --- /dev/null +++ b/packages/core/client/src/modules/blocks/filter-blocks/form/filterFormItemInitializers.tsx @@ -0,0 +1,38 @@ +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; +import { gridRowColWrap, useFilterFormItemInitializerFields } from '../../../../schema-initializer/utils'; +import { + FilterParentCollectionFields, + FilterAssociatedFields, +} from '../../../../schema-initializer/buttons/FormItemInitializers'; + +export const filterFormItemInitializers = new SchemaInitializer({ + name: 'FilterFormItemInitializers', + wrap: gridRowColWrap, + icon: 'SettingOutlined', + title: '{{t("Configure fields")}}', + items: [ + { + type: 'itemGroup', + name: 'displayFields', + title: '{{t("Display fields")}}', + useChildren: useFilterFormItemInitializerFields, + }, + { + name: 'parentCollectionFields', + Component: FilterParentCollectionFields, + }, + { + name: 'associationFields', + Component: FilterAssociatedFields, + }, + { + name: 'divider', + type: 'divider', + }, + { + title: '{{t("Add text")}}', + Component: 'MarkdownFormItemInitializer', + name: 'addText', + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/items/MarkdownBlockInitializer.tsx b/packages/core/client/src/modules/blocks/other-blocks/markdown/MarkdownBlockInitializer.tsx similarity index 89% rename from packages/core/client/src/schema-initializer/items/MarkdownBlockInitializer.tsx rename to packages/core/client/src/modules/blocks/other-blocks/markdown/MarkdownBlockInitializer.tsx index 23e714f00..4a56295e8 100644 --- a/packages/core/client/src/schema-initializer/items/MarkdownBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/other-blocks/markdown/MarkdownBlockInitializer.tsx @@ -2,7 +2,7 @@ import { FormOutlined } from '@ant-design/icons'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; export const MarkdownBlockInitializer = () => { const { insert } = useSchemaInitializer(); @@ -16,7 +16,7 @@ export const MarkdownBlockInitializer = () => { onClick={() => { insert({ type: 'void', - 'x-designer': 'Markdown.Void.Designer', + 'x-settings': 'blockSettings:markdown', 'x-decorator': 'CardItem', 'x-decorator-props': { name: 'markdown', diff --git a/packages/core/client/src/modules/blocks/other-blocks/markdown/MarkdownFormItemInitializer.tsx b/packages/core/client/src/modules/blocks/other-blocks/markdown/MarkdownFormItemInitializer.tsx new file mode 100644 index 000000000..7bea997b3 --- /dev/null +++ b/packages/core/client/src/modules/blocks/other-blocks/markdown/MarkdownFormItemInitializer.tsx @@ -0,0 +1,28 @@ +import { FormOutlined } from '@ant-design/icons'; +import React from 'react'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; + +export const MarkdownFormItemInitializer = () => { + const { insert } = useSchemaInitializer(); + const itemConfig = useSchemaInitializerItem(); + return ( + } + onClick={() => { + insert({ + type: 'void', + 'x-editable': false, + 'x-decorator': 'FormItem', + // 'x-designer': 'Markdown.Void.Designer', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:markdown', + 'x-component': 'Markdown.Void', + 'x-component-props': { + content: '{{t("This is a demo text, **supports Markdown syntax**.")}}', + }, + }); + }} + /> + ); +}; diff --git a/packages/core/client/src/modules/markdown/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/other-blocks/markdown/__e2e__/schemaInitializer.test.ts similarity index 100% rename from packages/core/client/src/modules/markdown/__e2e__/schemaInitializer.test.ts rename to packages/core/client/src/modules/blocks/other-blocks/markdown/__e2e__/schemaInitializer.test.ts diff --git a/packages/core/client/src/modules/markdown/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/other-blocks/markdown/__e2e__/schemaSettings.test.ts similarity index 100% rename from packages/core/client/src/modules/markdown/__e2e__/schemaSettings.test.ts rename to packages/core/client/src/modules/blocks/other-blocks/markdown/__e2e__/schemaSettings.test.ts diff --git a/packages/core/client/src/modules/table-data-selector/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/blocks/other-blocks/markdown/__e2e__/templatesOfBug.ts similarity index 100% rename from packages/core/client/src/modules/table-data-selector/__e2e__/templatesOfBug.ts rename to packages/core/client/src/modules/blocks/other-blocks/markdown/__e2e__/templatesOfBug.ts diff --git a/packages/core/client/src/modules/blocks/other-blocks/markdown/markdownBlockSettings.ts b/packages/core/client/src/modules/blocks/other-blocks/markdown/markdownBlockSettings.ts new file mode 100644 index 000000000..477d00805 --- /dev/null +++ b/packages/core/client/src/modules/blocks/other-blocks/markdown/markdownBlockSettings.ts @@ -0,0 +1,40 @@ +import { useField } from '@formily/react'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; + +export const markdownBlockSettings = new SchemaSettings({ + name: 'blockSettings:markdown', + items: [ + { + name: 'EditMarkdown', + type: 'item', + useComponentProps() { + const field = useField(); + const { t } = useTranslation(); + + return { + title: t('Edit markdown'), + onClick: () => { + field.editable = true; + }, + }; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'delete', + type: 'remove', + useComponentProps() { + return { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/dialog/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/dialog/__e2e__/schemaInitializer.test.ts index 3dc59dd0e..702785999 100644 --- a/packages/core/client/src/modules/dialog/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/dialog/__e2e__/schemaInitializer.test.ts @@ -27,6 +27,8 @@ test.describe('where to open a popup and what can be added to it', () => { await page.getByText('Form').click(); await page.getByText('Markdown').click(); + await page.mouse.move(300, 0); + await expect(page.getByLabel('block-item-CardItem-general-form')).toBeVisible(); await expect(page.getByLabel('block-item-Markdown.Void-general-markdown')).toBeVisible(); }); @@ -49,6 +51,9 @@ test.describe('where to open a popup and what can be added to it', () => { await page.getByLabel('schema-initializer-Grid-CusomeizeCreateFormBlockInitializers-general').hover(); await page.getByText('Form').hover(); await page.getByRole('menuitem', { name: 'Users' }).click(); + + // add Markdown + await page.getByLabel('schema-initializer-Grid-CusomeizeCreateFormBlockInitializers-general').hover(); await page.getByRole('menuitem', { name: 'Markdown' }).click(); await expect(page.getByLabel('block-item-CardItem-users-form')).toBeVisible(); @@ -84,17 +89,19 @@ test.describe('where to open a popup and what can be added to it', () => { // 删除已创建的 blocks,腾出页面空间 // delete details block await page.getByText('GeneralConfigure actionsConfigure fields').hover(); - await page.getByLabel('designer-schema-settings-CardItem-FormV2.ReadPrettyDesigner-general').hover(); + await page.getByLabel('designer-schema-settings-CardItem-blockSettings:singleDataDetails-general').hover(); await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.getByRole('button', { name: 'OK', exact: true }).click(); // delete form block await page.getByLabel('block-item-CardItem-general-form').hover(); - await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); + await page.getByLabel('designer-schema-settings-CardItem-blockSettings:editForm-general').hover(); await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.getByRole('button', { name: 'OK', exact: true }).click(); // delete markdown block await page.getByLabel('block-item-Markdown.Void-general-markdown').hover(); - await page.getByLabel('designer-schema-settings-Markdown.Void-Markdown.Void.Designer-general').hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-Markdown.Void-blockSettings:markdown-general' }) + .hover(); await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.getByRole('button', { name: 'OK', exact: true }).click(); @@ -117,6 +124,7 @@ test.describe('where to open a popup and what can be added to it', () => { await expect(page.getByRole('menuitem', { name: 'Calendar' })).toBeVisible(); await page.getByText('Table').click(); + await page.mouse.move(300, 0); await expect(page.getByLabel('block-item-CardItem-users-table')).toBeVisible(); }); @@ -133,9 +141,10 @@ test.describe('where to open a popup and what can be added to it', () => { await expect(page.getByText('test1')).toBeVisible(); // add blocks - await page.getByLabel('schema-initializer-Grid-CreateFormBulkEditBlockInitializers-general').hover(); + await page.getByLabel('schema-initializer-Grid-BulkEditBlockInitializers-general').hover(); await page.getByText('Form').click(); await page.getByRole('menuitem', { name: 'Markdown' }).click(); + await page.mouse.move(300, 0); await expect(page.getByLabel('block-item-CardItem-general-form')).toBeVisible(); await expect(page.getByLabel('block-item-Markdown.Void-general-markdown')).toBeVisible(); @@ -165,35 +174,20 @@ test.describe('where to open a popup and what can be added to it', () => { await page.getByRole('menuitem', { name: 'Details' }).click(); await page.getByText('Form').first().click(); await page.getByRole('menuitem', { name: 'Markdown' }).click(); + await page.mouse.move(300, 0); await expect(page.getByText('GeneralConfigure actionsConfigure fields')).toBeVisible(); await expect(page.getByText('GeneralConfigure fieldsConfigure actions')).toBeVisible(); await expect(page.getByLabel('block-item-Markdown.Void-general-markdown')).toBeVisible(); - // 删除已创建的 blocks,腾出页面空间 - // delete details block - await page.getByText('GeneralConfigure actionsConfigure fields').hover(); - await page.getByLabel('designer-schema-settings-CardItem-FormV2.ReadPrettyDesigner-general').hover(); - await page.getByRole('menuitem', { name: 'Delete' }).click(); - await page.getByRole('button', { name: 'OK', exact: true }).click(); - // delete form block - await page.getByLabel('block-item-CardItem-general-form').hover(); - await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); - await page.getByRole('menuitem', { name: 'Delete' }).click(); - await page.getByRole('button', { name: 'OK', exact: true }).click(); - // delete markdown block - await page.getByLabel('block-item-Markdown.Void-general-markdown').hover(); - await page.getByLabel('designer-schema-settings-Markdown.Void-Markdown.Void.Designer-general').hover(); - await page.getByRole('menuitem', { name: 'Delete' }).click(); - await page.getByRole('button', { name: 'OK', exact: true }).click(); - // add relationship blocks await page.getByLabel('schema-initializer-Grid-RecordBlockInitializers-general').hover(); await page.getByRole('menuitem', { name: 'Many to one' }).hover(); await page.getByRole('menuitem', { name: 'Details' }).click(); - await page.mouse.move(300, 0); + await expect(page.getByLabel('block-item-CardItem-general-').nth(2)).toBeVisible(); + await page.getByLabel('schema-initializer-Grid-RecordBlockInitializers-general').hover(); await page.getByRole('menuitem', { name: 'One to many' }).hover(); @@ -206,7 +200,11 @@ test.describe('where to open a popup and what can be added to it', () => { await expect(page.getByRole('menuitem', { name: 'Calendar' })).toBeVisible(); await page.getByText('Table').click(); + await page.mouse.move(300, 0); + await expect(page.getByLabel('block-item-CardItem-users-table')).toBeVisible(); + // 屏幕上没有显示错误提示 + await expect(page.locator('.ant-notification-notice').first()).toBeHidden({ timeout: 1000 }); }); test('data picker', async ({ page, mockPage }) => { @@ -216,15 +214,20 @@ test.describe('where to open a popup and what can be added to it', () => { await page.getByTestId('select-data-picker').click(); // add blocks - await page.getByLabel('schema-initializer-Grid-TableSelectorInitializers-roles').hover(); - await page.getByRole('menuitem', { name: 'form Table' }).click(); - await page.getByText('Form').click(); - await page.getByRole('menuitem', { name: 'Collapse' }).click(); - await page.getByRole('menuitem', { name: 'Add text' }).click(); + await addBlock('form Table'); + await addBlock('form Form'); + await addBlock('Collapse'); + await addBlock('Add text'); await expect(page.getByLabel('block-item-CardItem-roles-table-selector')).toBeVisible(); await expect(page.getByLabel('block-item-CardItem-roles-filter-form')).toBeVisible(); await expect(page.getByLabel('block-item-CardItem-roles-filter-collapse')).toBeVisible(); await expect(page.getByLabel('block-item-Markdown.Void-roles-form')).toBeVisible(); + + async function addBlock(name: string) { + await page.getByLabel('schema-initializer-Grid-TableSelectorInitializers-roles').hover(); + await page.getByRole('menuitem', { name }).click(); + await page.mouse.move(300, 0); + } }); }); diff --git a/packages/core/client/src/modules/fields/component/CascadeSelect/cascadeSelectComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/CascadeSelect/cascadeSelectComponentFieldSettings.tsx new file mode 100644 index 000000000..b3f9bac57 --- /dev/null +++ b/packages/core/client/src/modules/fields/component/CascadeSelect/cascadeSelectComponentFieldSettings.tsx @@ -0,0 +1,94 @@ +import { Field } from '@formily/core'; +import { useField, useFieldSchema } from '@formily/react'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useFieldComponentName } from '../../../../common/useFieldComponentName'; +import { useDesignable, useFieldModeOptions, useIsAddNewForm } from '../../../../schema-component'; +import { isSubMode } from '../../../../schema-component/antd/association-field/util'; +import { useTitleFieldOptions } from '../../../../schema-component/antd/form-item/FormItem.Settings'; +import { useCollectionField } from '../utils'; + +const fieldComponent: any = { + name: 'fieldComponent', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const fieldModeOptions = useFieldModeOptions(); + const isAddNewForm = useIsAddNewForm(); + const fieldComponentName = useFieldComponentName(); + const { dn } = useDesignable(); + return { + title: t('Field component'), + options: fieldModeOptions, + value: fieldComponentName, + onChange(mode) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['mode'] = mode; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps = field.componentProps || {}; + field.componentProps.mode = mode; + + // 子表单状态不允许设置默认值 + if (isSubMode(fieldSchema) && isAddNewForm) { + // @ts-ignore + schema.default = null; + fieldSchema.default = null; + field.setInitialValue(null); + field.setValue(null); + } + + void dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, +}; + +const titleField: any = { + name: 'titleField', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn } = useDesignable(); + const options = useTitleFieldOptions(); + const collectionField = useCollectionField(); + + return { + title: t('Title field'), + options, + value: field?.componentProps?.fieldNames?.label, + onChange(label) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + const fieldNames = { + ...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'], + ...field.componentProps.fieldNames, + label, + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['fieldNames'] = fieldNames; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps.fieldNames = fieldSchema['x-component-props'].fieldNames; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, +}; + +export const cascadeSelectComponentFieldSettings = new SchemaSettings({ + name: 'fieldSettings:component:CascadeSelect', + items: [fieldComponent, titleField], +}); diff --git a/packages/core/client/src/modules/fields/component/DatePicker/datePickerComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/DatePicker/datePickerComponentFieldSettings.tsx new file mode 100644 index 000000000..bea3df021 --- /dev/null +++ b/packages/core/client/src/modules/fields/component/DatePicker/datePickerComponentFieldSettings.tsx @@ -0,0 +1,19 @@ +import { useFieldSchema } from '@formily/react'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { SchemaSettingsDateFormat } from '../../../../schema-settings/SchemaSettingsDateFormat'; + +export const datePickerComponentFieldSettings = new SchemaSettings({ + name: 'fieldSettings:component:DatePicker', + items: [ + { + name: 'dateDisplayFormat', + Component: SchemaSettingsDateFormat as any, + useComponentProps() { + const fieldSchema = useFieldSchema(); + return { + fieldSchema, + }; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/fields/component/FileManager/fileManagerComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/FileManager/fileManagerComponentFieldSettings.tsx new file mode 100644 index 000000000..1ee48795b --- /dev/null +++ b/packages/core/client/src/modules/fields/component/FileManager/fileManagerComponentFieldSettings.tsx @@ -0,0 +1,185 @@ +import { Field } from '@formily/core'; +import { useField, useFieldSchema, useForm } from '@formily/react'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useFieldComponentName } from '../../../../common/useFieldComponentName'; +import { useDesignable, useFieldModeOptions, useIsAddNewForm } from '../../../../schema-component'; +import { isSubMode } from '../../../../schema-component/antd/association-field/util'; +import { + useIsFieldReadPretty, + useIsAssociationField, +} from '../../../../schema-component/antd/form-item/FormItem.Settings'; +import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator'; +import { allowMultiple } from '../Select/selectComponentFieldSettings'; +import { useIsShowMultipleSwitch } from '../../../../schema-settings/hooks/useIsShowMultipleSwitch'; + +const fieldComponent: any = { + name: 'fieldComponent', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const isAddNewForm = useIsAddNewForm(); + const fieldComponentName = useFieldComponentName(); + const { fieldSchema: tableColumnSchema, collectionField } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const fieldModeOptions = useFieldModeOptions({ fieldSchema: tableColumnSchema, collectionField }); + const { dn } = useDesignable(); + + return { + title: t('Field component'), + options: fieldModeOptions, + value: fieldComponentName, + onChange(mode) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['mode'] = mode; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps = field.componentProps || {}; + field.componentProps.mode = mode; + + // 子表单状态不允许设置默认值 + if (isSubMode(fieldSchema) && isAddNewForm) { + // @ts-ignore + schema.default = null; + fieldSchema.default = null; + field.setInitialValue(null); + field.setValue(null); + } + + void dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, +}; + +export const fileManagerComponentFieldSettings = new SchemaSettings({ + name: 'fieldSettings:component:FileManager', + items: [ + fieldComponent, + { + name: 'quickUpload', + type: 'switch', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const { dn, refresh } = useDesignable(); + return { + title: t('Quick upload'), + checked: fieldSchema['x-component-props']?.quickUpload !== (false as boolean), + onChange(value) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + field.componentProps.quickUpload = value; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props'].quickUpload = value; + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + refresh(); + }, + }; + }, + useVisible() { + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + const field = useField(); + const form = useForm(); + const isReadPretty = tableColumnSchema?.['x-read-pretty'] || field.readPretty || form.readPretty; + return !isReadPretty; + }, + }, + { + name: 'selectFile', + type: 'switch', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const { dn, refresh } = useDesignable(); + return { + title: t('Select file'), + checked: fieldSchema['x-component-props']?.selectFile !== (false as boolean), + onChange(value) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + field.componentProps.selectFile = value; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props'].selectFile = value; + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + refresh(); + }, + }; + }, + useVisible() { + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + const field = useField(); + const form = useForm(); + const isReadPretty = tableColumnSchema?.['x-read-pretty'] || field.readPretty || form.readPretty; + return !isReadPretty; + }, + }, + { + name: 'size', + type: 'select', + useVisible() { + const readPretty = useIsFieldReadPretty(); + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + return readPretty && !tableColumnSchema; + }, + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn } = useDesignable(); + return { + title: t('Size'), + options: [ + { label: t('Large'), value: 'large' }, + { label: t('Default'), value: 'default' }, + { label: t('Small'), value: 'small' }, + ], + value: field?.componentProps?.size || 'default', + onChange(size) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['size'] = size; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps = field.componentProps || {}; + field.componentProps.size = size; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, + }, + { + ...allowMultiple, + useVisible() { + const isAssociationField = useIsAssociationField(); + const IsShowMultipleSwitch = useIsShowMultipleSwitch(); + return isAssociationField && IsShowMultipleSwitch(); + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/fields/component/FileManager/uploadAttachmentComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/FileManager/uploadAttachmentComponentFieldSettings.tsx new file mode 100644 index 000000000..25a6db2f8 --- /dev/null +++ b/packages/core/client/src/modules/fields/component/FileManager/uploadAttachmentComponentFieldSettings.tsx @@ -0,0 +1,49 @@ +import { Field } from '@formily/core'; +import { useField, useFieldSchema } from '@formily/react'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useDesignable } from '../../../../schema-component'; +import { useIsFieldReadPretty } from '../../../../schema-component/antd/form-item/FormItem.Settings'; + +export const uploadAttachmentComponentFieldSettings = new SchemaSettings({ + name: 'fieldSettings:component:Upload.Attachment', + items: [ + { + name: 'size', + type: 'select', + useVisible() { + const readPretty = useIsFieldReadPretty(); + return readPretty; + }, + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn } = useDesignable(); + return { + title: t('Size'), + options: [ + { label: t('Large'), value: 'large' }, + { label: t('Default'), value: 'default' }, + { label: t('Small'), value: 'small' }, + ], + value: field?.componentProps?.size || 'default', + onChange(size) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['size'] = size; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps = field.componentProps || {}; + field.componentProps.size = size; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/fields/component/Nester/subformComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/Nester/subformComponentFieldSettings.tsx new file mode 100644 index 000000000..6e5801303 --- /dev/null +++ b/packages/core/client/src/modules/fields/component/Nester/subformComponentFieldSettings.tsx @@ -0,0 +1,135 @@ +import { Field } from '@formily/core'; +import { useField, useFieldSchema } from '@formily/react'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useFieldComponentName } from '../../../../common/useFieldComponentName'; +import { useDesignable, useFieldModeOptions, useIsAddNewForm } from '../../../../schema-component'; +import { isSubMode } from '../../../../schema-component/antd/association-field/util'; +import { + useIsFieldReadPretty, + useIsFormReadPretty, +} from '../../../../schema-component/antd/form-item/FormItem.Settings'; +import { useCollectionField } from '../utils'; +import { useFormBlockType } from '../../../../block-provider'; + +const allowMultiple: any = { + name: 'allowMultiple', + type: 'switch', + useVisible() { + const isFieldReadPretty = useIsFieldReadPretty(); + const collectionField = useCollectionField(); + return !isFieldReadPretty && ['hasMany', 'belongsToMany'].includes(collectionField?.type); + }, + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn, refresh } = useDesignable(); + return { + title: t('Allow multiple'), + checked: + fieldSchema['x-component-props']?.multiple === undefined ? true : fieldSchema['x-component-props'].multiple, + onChange(value) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + field.componentProps = field.componentProps || {}; + + fieldSchema['x-component-props'].multiple = value; + field.componentProps.multiple = value; + + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + refresh(); + }, + }; + }, +}; + +const fieldComponent: any = { + name: 'fieldComponent', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const fieldModeOptions = useFieldModeOptions(); + const isAddNewForm = useIsAddNewForm(); + const fieldComponentName = useFieldComponentName(); + const { dn } = useDesignable(); + return { + title: t('Field component'), + options: fieldModeOptions, + value: fieldComponentName, + onChange(mode) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['mode'] = mode; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps = field.componentProps || {}; + field.componentProps.mode = mode; + + // 子表单状态不允许设置默认值 + if (isSubMode(fieldSchema) && isAddNewForm) { + // @ts-ignore + schema.default = null; + fieldSchema.default = null; + field.setInitialValue(null); + field.setValue(null); + } + + void dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, +}; + +export const subformComponentFieldSettings = new SchemaSettings({ + name: 'fieldSettings:component:Nester', + items: [ + fieldComponent, + allowMultiple, + { + name: 'allowDissociate', + type: 'switch', + useVisible() { + const { type } = useFormBlockType(); + return !useIsFormReadPretty() && type === 'update'; + }, + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn, refresh } = useDesignable(); + return { + title: t('Allow dissociate'), + checked: fieldSchema['x-component-props']?.allowDissociate !== false, + onChange(value) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + field.componentProps = field.componentProps || {}; + + fieldSchema['x-component-props'].allowDissociate = value; + field.componentProps.allowDissociate = value; + + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + refresh(); + }, + }; + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/TableSelectorInitializers.tsx b/packages/core/client/src/modules/fields/component/Picker/TableSelectorInitializers.tsx similarity index 72% rename from packages/core/client/src/schema-initializer/buttons/TableSelectorInitializers.tsx rename to packages/core/client/src/modules/fields/component/Picker/TableSelectorInitializers.tsx index b08f1b741..0ad6d3a85 100644 --- a/packages/core/client/src/schema-initializer/buttons/TableSelectorInitializers.tsx +++ b/packages/core/client/src/modules/fields/component/Picker/TableSelectorInitializers.tsx @@ -1,6 +1,6 @@ -import { useCollection } from '../..'; -import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; -import { gridRowColWrap } from '../utils'; +import { useCollection_deprecated } from '../../../..'; +import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer'; +import { gridRowColWrap } from '../../../../schema-initializer/utils'; export const tableSelectorInitializers = new SchemaInitializer({ name: 'TableSelectorInitializers', @@ -25,29 +25,33 @@ export const tableSelectorInitializers = new SchemaInitializer({ title: '{{t("Filter blocks")}}', name: 'filterBlocks', useChildren() { - const { name } = useCollection(); + const { name, dataSource } = useCollection_deprecated(); return [ { name: 'filterFormBlockInTableSelector', title: '{{t("Form")}}', Component: 'FilterFormBlockInitializer', componentProps: { - filterItems() { + filterMenuItemChildren() { return false; }, + onlyCurrentDataSource: true, }, collectionName: name, + dataSource, }, { name: 'filterCollapseBlockInTableSelector', title: '{{t("Collapse")}}', Component: 'FilterCollapseBlockInitializer', componentProps: { - filterItems() { + filterMenuItemChildren() { return false; }, + onlyCurrentDataSource: true, }, collectionName: name, + dataSource, }, ]; }, @@ -65,7 +69,9 @@ export const tableSelectorInitializers = new SchemaInitializer({ type: 'void', 'x-editable': false, 'x-decorator': 'BlockItem', - 'x-designer': 'Markdown.Void.Designer', + // 'x-designer': 'Markdown.Void.Designer', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:markdown', 'x-component': 'Markdown.Void', 'x-component-props': { content: '{{t("This is a demo text, **supports Markdown syntax**.")}}', diff --git a/packages/core/client/src/modules/fields/component/Picker/recordPickerComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/Picker/recordPickerComponentFieldSettings.tsx new file mode 100644 index 000000000..3ca9d45d1 --- /dev/null +++ b/packages/core/client/src/modules/fields/component/Picker/recordPickerComponentFieldSettings.tsx @@ -0,0 +1,185 @@ +import { Field } from '@formily/core'; +import { useField, useFieldSchema } from '@formily/react'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useFieldComponentName } from '../../../../common/useFieldComponentName'; +import { useDesignable, useFieldModeOptions, useIsAddNewForm } from '../../../../schema-component'; +import { isSubMode } from '../../../../schema-component/antd/association-field/util'; +import { + useIsFieldReadPretty, + useTitleFieldOptions, +} from '../../../../schema-component/antd/form-item/FormItem.Settings'; +import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator'; +import { useCollectionField } from '../utils'; + +export const titleField: any = { + name: 'titleField', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const { uiSchema, fieldSchema: tableColumnSchema, collectionField: tableColumnField } = useColumnSchema(); + const options = useTitleFieldOptions(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const targetCollectionField = useCollectionField(); + const collectionField = tableColumnField || targetCollectionField; + const { dn } = useDesignable(); + const fieldNames = + field?.componentProps?.fieldNames || + fieldSchema?.['x-component-props']?.['fieldNames'] || + uiSchema?.['x-component-props']?.['fieldNames']; + return { + title: t('Title field'), + options, + value: fieldNames?.label, + onChange(label) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + const fieldNames = { + ...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'], + ...field.componentProps.fieldNames, + label, + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['fieldNames'] = fieldNames; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps.fieldNames = fieldSchema['x-component-props'].fieldNames; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, +}; + +const allowMultiple: any = { + name: 'allowMultiple', + type: 'switch', + useVisible() { + const isFieldReadPretty = useIsFieldReadPretty(); + const collectionField = useCollectionField(); + return !isFieldReadPretty && ['hasMany', 'belongsToMany'].includes(collectionField?.type); + }, + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn, refresh } = useDesignable(); + return { + title: t('Allow multiple'), + checked: + fieldSchema['x-component-props']?.multiple === undefined ? true : fieldSchema['x-component-props'].multiple, + onChange(value) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + field.componentProps = field.componentProps || {}; + + fieldSchema['x-component-props'].multiple = value; + field.componentProps.multiple = value; + + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + refresh(); + }, + }; + }, +}; + +const fieldComponent: any = { + name: 'fieldComponent', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const isAddNewForm = useIsAddNewForm(); + const fieldComponentName = useFieldComponentName(); + const { fieldSchema: tableColumnSchema, collectionField } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const fieldModeOptions = useFieldModeOptions({ fieldSchema: tableColumnSchema, collectionField }); + const { dn } = useDesignable(); + return { + title: t('Field component'), + options: fieldModeOptions, + value: fieldComponentName, + onChange(mode) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['mode'] = mode; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps = field.componentProps || {}; + field.componentProps.mode = mode; + + // 子表单状态不允许设置默认值 + if (isSubMode(fieldSchema) && isAddNewForm) { + // @ts-ignore + schema.default = null; + fieldSchema.default = null; + field?.setInitialValue?.(null); + field?.setValue?.(null); + } + + void dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, +}; + +export const recordPickerComponentFieldSettings = new SchemaSettings({ + name: 'fieldSettings:component:Picker', + items: [ + fieldComponent, + { + name: 'popupSize', + type: 'select', + useVisible() { + const isFieldReadPretty = useIsFieldReadPretty(); + return !isFieldReadPretty; + }, + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const { dn } = useDesignable(); + return { + title: t('Popup size'), + options: [ + { label: t('Small'), value: 'small' }, + { label: t('Middle'), value: 'middle' }, + { label: t('Large'), value: 'large' }, + ], + value: + fieldSchema?.['x-component-props']?.['openSize'] ?? + (fieldSchema?.['x-component-props']?.['openMode'] == 'modal' ? 'large' : 'middle'), + onChange: (value) => { + field.componentProps.openSize = value; + fieldSchema['x-component-props'] = { ...fieldSchema['x-component-props'], openSize: value }; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + 'x-component-props': fieldSchema['x-component-props'], + }, + }); + dn.refresh(); + }, + }; + }, + }, + allowMultiple, + titleField, + ], +}); diff --git a/packages/core/client/src/modules/fields/component/PopoverNester/subformPopoverComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/PopoverNester/subformPopoverComponentFieldSettings.tsx new file mode 100644 index 000000000..dd99735fc --- /dev/null +++ b/packages/core/client/src/modules/fields/component/PopoverNester/subformPopoverComponentFieldSettings.tsx @@ -0,0 +1,98 @@ +import { Field } from '@formily/core'; +import { useField, useFieldSchema } from '@formily/react'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useFieldComponentName } from '../../../../common/useFieldComponentName'; +import { useDesignable, useFieldModeOptions, useIsAddNewForm } from '../../../../schema-component'; +import { isSubMode } from '../../../../schema-component/antd/association-field/util'; +import { useIsFieldReadPretty } from '../../../../schema-component/antd/form-item/FormItem.Settings'; +import { useCollectionField } from '../utils'; +import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator'; +import { titleField } from '../Picker/recordPickerComponentFieldSettings'; + +const allowMultiple: any = { + name: 'allowMultiple', + type: 'switch', + useVisible() { + const isFieldReadPretty = useIsFieldReadPretty(); + const collectionField = useCollectionField(); + return !isFieldReadPretty && ['hasMany', 'belongsToMany'].includes(collectionField?.type); + }, + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn, refresh } = useDesignable(); + return { + title: t('Allow multiple'), + checked: + fieldSchema['x-component-props']?.multiple === undefined ? true : fieldSchema['x-component-props'].multiple, + onChange(value) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + field.componentProps = field.componentProps || {}; + + fieldSchema['x-component-props'].multiple = value; + field.componentProps.multiple = value; + + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + refresh(); + }, + }; + }, +}; + +const fieldComponent: any = { + name: 'fieldComponent', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const { fieldSchema: tableColumnSchema, collectionField } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const fieldModeOptions = useFieldModeOptions({ fieldSchema: tableColumnSchema, collectionField }); + const { dn } = useDesignable(); + const isAddNewForm = useIsAddNewForm(); + const fieldMode = useFieldComponentName(); + return { + title: t('Field component'), + options: fieldModeOptions, + value: fieldMode, + onChange(mode) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['mode'] = mode; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps = field.componentProps || {}; + field.componentProps.mode = mode; + + // 子表单状态不允许设置默认值 + if (isSubMode(fieldSchema) && isAddNewForm) { + // @ts-ignore + schema.default = null; + fieldSchema.default = null; + field.setInitialValue(null); + field.setValue(null); + } + + void dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, +}; + +export const subformPopoverComponentFieldSettings = new SchemaSettings({ + name: 'fieldSettings:component:PopoverNester', + items: [fieldComponent, allowMultiple, titleField], +}); diff --git a/packages/core/client/src/modules/fields/component/Select/selectComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/Select/selectComponentFieldSettings.tsx new file mode 100644 index 000000000..ef6c479de --- /dev/null +++ b/packages/core/client/src/modules/fields/component/Select/selectComponentFieldSettings.tsx @@ -0,0 +1,370 @@ +import { Field } from '@formily/core'; +import { useField, useFieldSchema } from '@formily/react'; +import _ from 'lodash'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useFormBlockContext } from '../../../../block-provider'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../../collection-manager'; +import { useRecord } from '../../../../record-provider'; +import { removeNullCondition, useDesignable, useFieldModeOptions, useIsAddNewForm } from '../../../../schema-component'; +import { isSubMode } from '../../../../schema-component/antd/association-field/util'; +import { DynamicComponentProps } from '../../../../schema-component/antd/filter/DynamicComponent'; +import { + useIsAssociationField, + useIsFieldReadPretty, + useIsSelectFieldMode, + useTitleFieldOptions, +} from '../../../../schema-component/antd/form-item/FormItem.Settings'; +import { VariableInput, getShouldChange } from '../../../../schema-settings'; +import { useIsShowMultipleSwitch } from '../../../../schema-settings/hooks/useIsShowMultipleSwitch'; +import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator'; +import { useLocalVariables, useVariables } from '../../../../variables'; +import { useCollectionField } from '../utils'; +import { useFieldComponentName } from '../../../../common/useFieldComponentName'; +import { SchemaSettingsSortingRule } from '../../../../schema-settings/SchemaSettingsSortingRule'; +import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope'; + +const enableLink = { + name: 'enableLink', + type: 'switch', + useVisible() { + const field = useField(); + console.log(field); + console.log(useFieldSchema()); + return field.readPretty; + }, + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const { dn } = useDesignable(); + return { + title: t('Enable link'), + checked: fieldSchema['x-component-props']?.enableLink !== false, + onChange(flag) { + fieldSchema['x-component-props'] = { + ...fieldSchema?.['x-component-props'], + enableLink: flag, + }; + field.componentProps['enableLink'] = flag; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + 'x-component-props': { + ...fieldSchema?.['x-component-props'], + }, + }, + }); + dn.refresh(); + }, + }; + }, +}; + +const titleField: any = { + name: 'titleField', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const { dn } = useDesignable(); + const options = useTitleFieldOptions(); + const { uiSchema, fieldSchema: tableColumnSchema, collectionField: tableColumnField } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const targetCollectionField = useCollectionField(); + const collectionField = tableColumnField || targetCollectionField; + const fieldNames = + field?.componentProps?.fieldNames || + fieldSchema?.['x-component-props']?.['fieldNames'] || + uiSchema?.['x-component-props']?.['fieldNames']; + return { + title: t('Title field'), + options, + value: fieldNames?.label, + onChange(label) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + const fieldNames = { + ...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'], + ...field.componentProps.fieldNames, + label, + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['fieldNames'] = fieldNames; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps.fieldNames = fieldSchema['x-component-props'].fieldNames; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, +}; + +export const allowMultiple: any = { + name: 'allowMultiple', + type: 'switch', + useVisible() { + const isFieldReadPretty = useIsFieldReadPretty(); + const collectionField = useCollectionField(); + return !isFieldReadPretty && ['hasMany', 'belongsToMany'].includes(collectionField?.type); + }, + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const { dn, refresh } = useDesignable(); + return { + title: t('Allow multiple'), + checked: + fieldSchema['x-component-props']?.multiple === undefined ? true : fieldSchema['x-component-props'].multiple, + onChange(value) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + field.componentProps = field.componentProps || {}; + + fieldSchema['x-component-props'].multiple = value; + field.componentProps.multiple = value; + + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + refresh(); + }, + }; + }, +}; + +const quickCreate: any = { + name: 'quickCreate', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn, insertAdjacent } = useDesignable(); + return { + title: t('Quick create'), + options: [ + { label: t('None'), value: 'none' }, + { label: t('Dropdown'), value: 'quickAdd' }, + { label: t('Pop-up'), value: 'modalAdd' }, + ], + value: field.componentProps?.addMode || 'none', + onChange(mode) { + if (mode === 'modalAdd') { + const hasAddNew = fieldSchema.reduceProperties((buf, schema) => { + if (schema['x-component'] === 'Action') { + return schema; + } + return buf; + }, null); + + if (!hasAddNew) { + const addNewActionschema = { + 'x-action': 'create', + 'x-acl-action': 'create', + title: "{{t('Add new')}}", + // 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:addNew', + 'x-component': 'Action', + 'x-decorator': 'ACLActionProvider', + 'x-component-props': { + openMode: 'drawer', + type: 'default', + component: 'CreateRecordAction', + }, + }; + insertAdjacent('afterBegin', addNewActionschema); + } + } + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['addMode'] = mode; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps = field.componentProps || {}; + field.componentProps.addMode = mode; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, +}; + +const setDefaultSortingRules = { + name: 'setDefaultSortingRules', + Component: SchemaSettingsSortingRule, +}; + +const setTheDataScope: any = { + name: 'setTheDataScope', + Component: SchemaSettingsDataScope, + useComponentProps() { + const { getCollectionJoinField, getAllCollectionsInheritChain } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); + const { form } = useFormBlockContext(); + const record = useRecord(); + const field = useField(); + const { fieldSchema: tableColumnSchema, collectionField: tableColumnField } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const collectionField = + tableColumnField || getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + const variables = useVariables(); + const localVariables = useLocalVariables(); + const { dn } = useDesignable(); + return { + collectionName: collectionField?.target, + defaultFilter: fieldSchema?.['x-component-props']?.service?.params?.filter || {}, + form, + dynamicComponent: (props: DynamicComponentProps) => { + return ( + + ); + }, + onSubmit: ({ filter }) => { + filter = removeNullCondition(filter); + _.set(field.componentProps, 'service.params.filter', filter); + fieldSchema['x-component-props'] = field.componentProps; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-component-props': field.componentProps, + }, + }); + }, + }; + }, +}; + +const fieldComponent: any = { + name: 'fieldComponent', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const { fieldSchema: tableColumnSchema, collectionField } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const fieldModeOptions = useFieldModeOptions({ fieldSchema: tableColumnSchema, collectionField }); + const isAddNewForm = useIsAddNewForm(); + const fieldMode = useFieldComponentName(); + const { dn } = useDesignable(); + return { + title: t('Field component'), + options: fieldModeOptions, + value: fieldMode, + onChange(mode) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['mode'] = mode; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps = field.componentProps || {}; + field.componentProps.mode = mode; + + // 子表单状态不允许设置默认值 + if (isSubMode(fieldSchema) && isAddNewForm) { + // @ts-ignore + schema.default = null; + fieldSchema.default = null; + field?.setInitialValue?.(null); + field?.setValue?.(null); + } + + void dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, +}; + +export const selectComponentFieldSettings = new SchemaSettings({ + name: 'fieldSettings:component:Select', + items: [ + { + ...fieldComponent, + useVisible: useIsAssociationField, + }, + { + ...setTheDataScope, + useVisible() { + const isSelectFieldMode = useIsSelectFieldMode(); + const isFieldReadPretty = useIsFieldReadPretty(); + return isSelectFieldMode && !isFieldReadPretty; + }, + }, + { + ...setDefaultSortingRules, + useComponentProps() { + const { fieldSchema } = useColumnSchema(); + return { + fieldSchema, + }; + }, + useVisible() { + const isSelectFieldMode = useIsSelectFieldMode(); + const isFieldReadPretty = useIsFieldReadPretty(); + return isSelectFieldMode && !isFieldReadPretty; + }, + }, + { + ...quickCreate, + useVisible() { + const isAssociationField = useIsAssociationField(); + const readPretty = useIsFieldReadPretty(); + const { fieldSchema } = useColumnSchema(); + return isAssociationField && !fieldSchema && !readPretty; + }, + }, + { + ...allowMultiple, + useVisible() { + const isAssociationField = useIsAssociationField(); + const IsShowMultipleSwitch = useIsShowMultipleSwitch(); + return isAssociationField && IsShowMultipleSwitch(); + }, + }, + { + ...titleField, + useVisible: useIsAssociationField, + }, + { + ...enableLink, + useVisible() { + const readPretty = useIsFieldReadPretty(); + return useIsAssociationField() && readPretty; + }, + }, + ], +}); diff --git a/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx new file mode 100644 index 000000000..50ab3ebf6 --- /dev/null +++ b/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx @@ -0,0 +1,56 @@ +import { Field } from '@formily/core'; +import { useField, useFieldSchema } from '@formily/react'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useFieldComponentName } from '../../../../common/useFieldComponentName'; +import { useDesignable, useFieldModeOptions, useIsAddNewForm } from '../../../../schema-component'; +import { isSubMode } from '../../../../schema-component/antd/association-field/util'; + +const fieldComponent: any = { + name: 'fieldComponent', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const fieldModeOptions = useFieldModeOptions(); + const isAddNewForm = useIsAddNewForm(); + const fieldComponentName = useFieldComponentName(); + const { dn } = useDesignable(); + + return { + title: t('Field component'), + options: fieldModeOptions, + value: fieldComponentName, + onChange(mode) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['mode'] = mode; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps = field.componentProps || {}; + field.componentProps.mode = mode; + + // 子表单状态不允许设置默认值 + if (isSubMode(fieldSchema) && isAddNewForm) { + // @ts-ignore + schema.default = null; + fieldSchema.default = null; + field.setInitialValue(null); + field.setValue(null); + } + + void dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, +}; + +export const subTablePopoverComponentFieldSettings = new SchemaSettings({ + name: 'fieldSettings:component:SubTable', + items: [fieldComponent], +}); diff --git a/packages/core/client/src/modules/fields/component/Tag/tagComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/Tag/tagComponentFieldSettings.tsx new file mode 100644 index 000000000..3bc7db00a --- /dev/null +++ b/packages/core/client/src/modules/fields/component/Tag/tagComponentFieldSettings.tsx @@ -0,0 +1,171 @@ +import { Field } from '@formily/core'; +import { useField, useFieldSchema } from '@formily/react'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useFieldComponentName } from '../../../../common/useFieldComponentName'; +import { useDesignable, useFieldModeOptions, useIsAddNewForm } from '../../../../schema-component'; +import { isSubMode } from '../../../../schema-component/antd/association-field/util'; +import { useTitleFieldOptions } from '../../../../schema-component/antd/form-item/FormItem.Settings'; +import { useColorFields } from '../../../../schema-component/antd/table-v2/Table.Column.Designer'; +import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator'; +import { useCollectionField } from '../utils'; + +const enableLink = { + name: 'enableLink', + type: 'switch', + useVisible() { + const field = useField(); + return field.readPretty; + }, + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn } = useDesignable(); + return { + title: t('Enable link'), + checked: fieldSchema['x-component-props']?.enableLink !== false, + onChange(flag) { + fieldSchema['x-component-props'] = { + ...fieldSchema?.['x-component-props'], + enableLink: flag, + }; + field.componentProps['enableLink'] = flag; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + 'x-component-props': { + ...fieldSchema?.['x-component-props'], + }, + }, + }); + dn.refresh(); + }, + }; + }, +}; + +const titleField: any = { + name: 'titleField', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const { fieldSchema: tableColumnSchema, collectionField: tableColumnField } = useColumnSchema(); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const options = useTitleFieldOptions(); + const targetCollectionField = useCollectionField(); + const collectionField = tableColumnField || targetCollectionField; + const { dn } = useDesignable(); + return { + title: t('Title field'), + options, + value: field?.componentProps?.fieldNames?.label, + onChange(label) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + const fieldNames = { + ...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'], + ...field.componentProps.fieldNames, + label, + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['fieldNames'] = fieldNames; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps.fieldNames = fieldSchema['x-component-props'].fieldNames; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, +}; + +const fieldComponent: any = { + name: 'fieldComponent', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const isAddNewForm = useIsAddNewForm(); + const { fieldSchema: tableColumnSchema, collectionField } = useColumnSchema(); + const fieldModeOptions = useFieldModeOptions({ fieldSchema: tableColumnSchema, collectionField }); + const schema = useFieldSchema(); + const fieldSchema = tableColumnSchema || schema; + const fieldComponentName = useFieldComponentName(); + const { dn } = useDesignable(); + + return { + title: t('Field component'), + options: fieldModeOptions, + value: fieldComponentName, + onChange(mode) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['mode'] = mode; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps = field.componentProps || {}; + field.componentProps.mode = mode; + + // 子表单状态不允许设置默认值 + if (isSubMode(fieldSchema) && isAddNewForm) { + // @ts-ignore + schema.default = null; + fieldSchema.default = null; + field.setInitialValue(null); + field.setValue(null); + } + + void dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, +}; + +export const tagComponentFieldSettings = new SchemaSettings({ + name: 'fieldSettings:component:Tag', + items: [ + fieldComponent, + { + name: 'tagColorField', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn } = useDesignable(); + const collectionField = useCollectionField(); + const colorFieldOptions = useColorFields(collectionField?.target ?? collectionField?.targetCollection); + return { + title: t('Tag color field'), + options: colorFieldOptions, + value: field?.componentProps?.tagColorField, + onChange(tagColorField) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['tagColorField'] = tagColorField; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps.tagColorField = tagColorField; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, + }, + titleField, + enableLink, + ], +}); diff --git a/packages/core/client/src/modules/fields/component/utils.tsx b/packages/core/client/src/modules/fields/component/utils.tsx new file mode 100644 index 000000000..3c47f1758 --- /dev/null +++ b/packages/core/client/src/modules/fields/component/utils.tsx @@ -0,0 +1,14 @@ +import { useFieldSchema } from '@formily/react'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; + +/** + * 获取字段相关的配置信息 + * @returns + */ +export function useCollectionField() { + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); + const fieldSchema = useFieldSchema(); + const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + return collectionField; +} diff --git a/packages/core/client/src/schema-initializer/items/CollectionFieldInitializer.tsx b/packages/core/client/src/modules/fields/initializer/CollectionFieldInitializer.tsx similarity index 66% rename from packages/core/client/src/schema-initializer/items/CollectionFieldInitializer.tsx rename to packages/core/client/src/modules/fields/initializer/CollectionFieldInitializer.tsx index 3336d3f93..15316f56d 100644 --- a/packages/core/client/src/schema-initializer/items/CollectionFieldInitializer.tsx +++ b/packages/core/client/src/modules/fields/initializer/CollectionFieldInitializer.tsx @@ -1,8 +1,8 @@ import { ISchema } from '@formily/react'; import React from 'react'; -import { InitializerWithSwitch } from './InitializerWithSwitch'; -import { useSchemaInitializerItem } from '../../application'; +import { InitializerWithSwitch } from '../../../schema-initializer/items/InitializerWithSwitch'; +import { useSchemaInitializerItem } from '../../../application'; export const CollectionFieldInitializer = () => { const schema: ISchema = {}; diff --git a/packages/core/client/src/schema-initializer/items/TableCollectionFieldInitializer.tsx b/packages/core/client/src/modules/fields/initializer/TableCollectionFieldInitializer.tsx similarity index 66% rename from packages/core/client/src/schema-initializer/items/TableCollectionFieldInitializer.tsx rename to packages/core/client/src/modules/fields/initializer/TableCollectionFieldInitializer.tsx index 9bc08ff9e..a4886d380 100644 --- a/packages/core/client/src/schema-initializer/items/TableCollectionFieldInitializer.tsx +++ b/packages/core/client/src/modules/fields/initializer/TableCollectionFieldInitializer.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { ISchema } from '@formily/react'; -import { InitializerWithSwitch } from './InitializerWithSwitch'; -import { useSchemaInitializerItem } from '../../application'; +import { InitializerWithSwitch } from '../../../schema-initializer/items/InitializerWithSwitch'; +import { useSchemaInitializerItem } from '../../../application'; export const TableCollectionFieldInitializer = () => { const schema: ISchema = {}; diff --git a/packages/core/client/src/modules/menu/GroupItem.tsx b/packages/core/client/src/modules/menu/GroupItem.tsx new file mode 100644 index 000000000..98775e850 --- /dev/null +++ b/packages/core/client/src/modules/menu/GroupItem.tsx @@ -0,0 +1,71 @@ +import { FormLayout } from '@formily/antd-v5'; +import { SchemaOptionsContext } from '@formily/react'; +import React, { useCallback, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FormDialog, SchemaComponent, SchemaComponentOptions } from '../../schema-component'; +import { SchemaInitializerItem, useSchemaInitializer } from '../../application'; +import { useGlobalTheme } from '../../global-theme'; +import { useStyles } from '../../schema-component/antd/menu/MenuItemInitializers'; + +export const GroupItem = () => { + const { insert } = useSchemaInitializer(); + const { t } = useTranslation(); + const options = useContext(SchemaOptionsContext); + const { theme } = useGlobalTheme(); + const { styles } = useStyles(); + + const handleClick = useCallback(async () => { + const values = await FormDialog( + t('Add group'), + () => { + return ( + + + + + + ); + }, + theme, + ).open({ + initialValues: {}, + }); + const { title, icon } = values; + insert({ + type: 'void', + title, + 'x-component': 'Menu.SubMenu', + 'x-decorator': 'ACLMenuItemProvider', + 'x-component-props': { + icon, + }, + 'x-server-hooks': [ + { + type: 'onSelfCreate', + method: 'bindMenuToRole', + }, + { + type: 'onSelfSave', + method: 'extractTextToLocale', + }, + ], + }); + }, [insert, options.components, options.scope, t, theme]); + return ; +}; diff --git a/packages/core/client/src/modules/menu/LinkMenuItem.tsx b/packages/core/client/src/modules/menu/LinkMenuItem.tsx new file mode 100644 index 000000000..b9fc2a707 --- /dev/null +++ b/packages/core/client/src/modules/menu/LinkMenuItem.tsx @@ -0,0 +1,78 @@ +import { FormLayout } from '@formily/antd-v5'; +import { SchemaOptionsContext } from '@formily/react'; +import React, { useCallback, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FormDialog, SchemaComponent, SchemaComponentOptions } from '../../schema-component'; +import { SchemaInitializerItem, useSchemaInitializer } from '../../application'; +import { useGlobalTheme } from '../../global-theme'; +import { useStyles } from '../../schema-component/antd/menu/MenuItemInitializers'; + +export const LinkMenuItem = () => { + const { insert } = useSchemaInitializer(); + const { t } = useTranslation(); + const options = useContext(SchemaOptionsContext); + const { theme } = useGlobalTheme(); + const { styles } = useStyles(); + + const handleClick = useCallback(async () => { + const values = await FormDialog( + t('Add link'), + () => { + return ( + + + + + + ); + }, + theme, + ).open({ + initialValues: {}, + }); + const { title, href, icon } = values; + insert({ + type: 'void', + title, + 'x-component': 'Menu.URL', + 'x-decorator': 'ACLMenuItemProvider', + 'x-component-props': { + icon, + href, + }, + 'x-server-hooks': [ + { + type: 'onSelfCreate', + method: 'bindMenuToRole', + }, + { + type: 'onSelfSave', + method: 'extractTextToLocale', + }, + ], + }); + }, [insert, options.components, options.scope, t, theme]); + + return ; +}; diff --git a/packages/core/client/src/modules/menu/PageMenuItem.tsx b/packages/core/client/src/modules/menu/PageMenuItem.tsx new file mode 100644 index 000000000..0c2096d55 --- /dev/null +++ b/packages/core/client/src/modules/menu/PageMenuItem.tsx @@ -0,0 +1,87 @@ +import { FormLayout } from '@formily/antd-v5'; +import { SchemaOptionsContext } from '@formily/react'; +import { uid } from '@formily/shared'; +import React, { useCallback, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FormDialog, SchemaComponent, SchemaComponentOptions } from '../../schema-component'; +import { SchemaInitializerItem, useSchemaInitializer } from '../../application'; +import { useGlobalTheme } from '../../global-theme'; +import { useStyles } from '../../schema-component/antd/menu/MenuItemInitializers'; + +export const PageMenuItem = () => { + const { insert } = useSchemaInitializer(); + const { t } = useTranslation(); + const options = useContext(SchemaOptionsContext); + const { theme } = useGlobalTheme(); + const { styles } = useStyles(); + + const handleClick = useCallback(async () => { + const values = await FormDialog( + t('Add page'), + () => { + return ( + + + + + + ); + }, + theme, + ).open({ + initialValues: {}, + }); + const { title, icon } = values; + insert({ + type: 'void', + title, + 'x-component': 'Menu.Item', + 'x-decorator': 'ACLMenuItemProvider', + 'x-component-props': { + icon, + }, + 'x-server-hooks': [ + { + type: 'onSelfCreate', + method: 'bindMenuToRole', + }, + { + type: 'onSelfSave', + method: 'extractTextToLocale', + }, + ], + properties: { + page: { + type: 'void', + 'x-component': 'Page', + 'x-async': true, + properties: { + [uid()]: { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'BlockInitializers', + properties: {}, + }, + }, + }, + }, + }); + }, [insert, options.components, options.scope, t, theme]); + return ; +}; diff --git a/packages/core/client/src/modules/menu/menuItemInitializer.tsx b/packages/core/client/src/modules/menu/menuItemInitializer.tsx new file mode 100644 index 000000000..4c146fce8 --- /dev/null +++ b/packages/core/client/src/modules/menu/menuItemInitializer.tsx @@ -0,0 +1,25 @@ +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; +import { LinkMenuItem } from './LinkMenuItem'; +import { PageMenuItem } from './PageMenuItem'; +import { GroupItem } from './GroupItem'; + +export const menuItemInitializer = new SchemaInitializer({ + name: 'MenuItemInitializers', + insertPosition: 'beforeEnd', + icon: 'PlusOutlined', + title: '{{t("Add menu item")}}', + items: [ + { + name: 'group', + Component: GroupItem, + }, + { + name: 'page', + Component: PageMenuItem, + }, + { + name: 'link', + Component: LinkMenuItem, + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx b/packages/core/client/src/modules/page/BlockInitializers.tsx similarity index 96% rename from packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx rename to packages/core/client/src/modules/page/BlockInitializers.tsx index ecc56dda5..e5611365c 100644 --- a/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx +++ b/packages/core/client/src/modules/page/BlockInitializers.tsx @@ -1,5 +1,5 @@ import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; -import { gridRowColWrap } from '../utils'; +import { gridRowColWrap } from '../../schema-initializer/utils'; export const blockInitializers = new SchemaInitializer({ name: 'BlockInitializers', diff --git a/packages/core/client/src/nocobase-buildin-plugin/index.tsx b/packages/core/client/src/nocobase-buildin-plugin/index.tsx index c979d6c48..8f9e533d1 100644 --- a/packages/core/client/src/nocobase-buildin-plugin/index.tsx +++ b/packages/core/client/src/nocobase-buildin-plugin/index.tsx @@ -14,13 +14,15 @@ import { RemoteDocumentTitlePlugin } from '../document-title'; import { PinnedListPlugin } from '../plugin-manager'; import { PMPlugin } from '../pm'; import { AdminLayoutPlugin, RouteSchemaComponent } from '../route-switch'; -import { AntdSchemaComponentPlugin, SchemaComponentPlugin, menuItemInitializer } from '../schema-component'; +import { AntdSchemaComponentPlugin, SchemaComponentPlugin } from '../schema-component'; import { ErrorFallback } from '../schema-component/antd/error-fallback'; import { AssociationFilterPlugin, SchemaInitializerPlugin } from '../schema-initializer'; +import { SchemaSettingsPlugin } from '../schema-settings'; import { BlockTemplateDetails, BlockTemplatePage } from '../schema-templates'; import { SystemSettingsPlugin } from '../system-settings'; import { CurrentUserProvider, CurrentUserSettingsMenuProvider } from '../user'; import { LocalePlugin } from './plugins/LocalePlugin'; +import { CollectionPlugin } from '../collection-manager'; const AppSpin = () => { return ( @@ -261,8 +263,6 @@ export class NocoBaseBuildInPlugin extends Plugin { this.app.use(CurrentUserProvider); this.app.use(CurrentUserSettingsMenuProvider); - - this.app.schemaInitializerManager.add(menuItemInitializer); } addRoutes() { @@ -311,10 +311,12 @@ export class NocoBaseBuildInPlugin extends Plugin { }); await this.app.pm.add(SchemaComponentPlugin, { name: 'schema-component' }); await this.app.pm.add(SchemaInitializerPlugin, { name: 'schema-initializer' }); + await this.app.pm.add(SchemaSettingsPlugin, { name: 'schema-settings' }); await this.app.pm.add(BlockSchemaComponentPlugin, { name: 'block-schema-component' }); await this.app.pm.add(AntdSchemaComponentPlugin, { name: 'antd-schema-component' }); await this.app.pm.add(ACLPlugin, { name: 'builtin-acl' }); await this.app.pm.add(RemoteDocumentTitlePlugin, { name: 'remote-document-title' }); await this.app.pm.add(PMPlugin, { name: 'builtin-pm' }); + await this.app.pm.add(CollectionPlugin, { name: 'builtin-collection' }); } } diff --git a/packages/core/client/src/pm/PluginCard.tsx b/packages/core/client/src/pm/PluginCard.tsx index 3dbc20d35..0618c1a1b 100644 --- a/packages/core/client/src/pm/PluginCard.tsx +++ b/packages/core/client/src/pm/PluginCard.tsx @@ -28,7 +28,6 @@ function PluginInfo(props: IPluginInfo) { const api = useAPIClient(); const { modal } = App.useApp(); const [showUploadForm, setShowUploadForm] = useState(false); - const [enabledVal, setEnabledVal] = useState(enabled); const reload = () => window.location.reload(); const title = displayName || name || packageName; return ( @@ -173,7 +172,7 @@ function PluginInfo(props: IPluginInfo) { }); } }} - checked={enabledVal} + checked={!!enabled} >, ].filter(Boolean)} > diff --git a/packages/core/client/src/pm/PluginManagerLink.tsx b/packages/core/client/src/pm/PluginManagerLink.tsx index 216f017cb..78eb53535 100644 --- a/packages/core/client/src/pm/PluginManagerLink.tsx +++ b/packages/core/client/src/pm/PluginManagerLink.tsx @@ -1,9 +1,9 @@ import { ApiOutlined, SettingOutlined } from '@ant-design/icons'; import { css } from '@emotion/css'; -import { Button, Card, Popover, Tooltip } from 'antd'; +import { Button, Card, Dropdown, Popover, Tooltip } from 'antd'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import { useApp } from '../application'; import { ActionContextProvider, useCompile } from '../schema-component'; import { useToken } from '../style'; @@ -35,6 +35,31 @@ export const SettingsCenterDropdown = () => { const app = useApp(); const settings = app.pluginSettingsManager.getList(); const [open, setOpen] = useState(false); + return ( + v.isTopLevel !== false) + .map((setting) => { + return { + key: setting.name, + icon: setting.icon, + label: {compile(setting.title)}, + }; + }), + }} + > +
+ +
+ field.editable && ( + <> + {field.componentProps?.allowAddnew !== false && ( + + )} + {field.componentProps?.allowSelectExistingRecord && ( + + )} + + ) } - .ant-formily-editable { - vertical-align: sub; - } - .ant-table-footer { - display: flex; - } - `} - bordered - size={'small'} - field={field} - showIndex - dragSort={field.editable} - showDel={field.editable} - pagination={false} - rowSelection={{ type: 'none', hideSelectAll: true }} - footer={() => - field.editable && ( - <> - {field.componentProps?.allowAddnew !== false && ( - - )} - {field.componentProps?.allowSelectExistingRecord && ( - - )} - - ) - } - isSubTable={true} - /> - + isSubTable={true} + /> + + - + - + diff --git a/packages/core/client/src/schema-component/antd/association-field/components/CreateRecordAction.tsx b/packages/core/client/src/schema-component/antd/association-field/components/CreateRecordAction.tsx index d39e6d46d..a5aa6f2ef 100644 --- a/packages/core/client/src/schema-component/antd/association-field/components/CreateRecordAction.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/components/CreateRecordAction.tsx @@ -1,6 +1,6 @@ import { RecursionField, observer, useField, useFieldSchema } from '@formily/react'; import React, { useState } from 'react'; -import { CollectionProvider } from '../../../../collection-manager'; +import { CollectionProvider_deprecated, useCollectionManager_deprecated } from '../../../../collection-manager'; import { CreateAction } from '../../../../schema-initializer/components'; import { ActionContextProvider, useActionContext } from '../../action'; import { useAssociationFieldContext, useInsertSchema } from '../hooks'; @@ -11,20 +11,24 @@ export const CreateRecordAction = observer( const field: any = useField(); const fieldSchema = useFieldSchema(); const ctx = useActionContext(); + const { getCollection } = useCollectionManager_deprecated(); const insertAddNewer = useInsertSchema('AddNewer'); const { options: collectionField } = useAssociationFieldContext(); const [visibleAddNewer, setVisibleAddNewer] = useState(false); - const [currentCollection, setCurrentCollection] = useState(collectionField?.target); - const addbuttonClick = (name) => { + const targetCollection = getCollection(collectionField?.target); + const [currentCollection, setCurrentCollection] = useState(targetCollection?.name); + const [currentDataSource, setCurrentDataSource] = useState(targetCollection?.dataSource); + const addbuttonClick = (collectionData) => { insertAddNewer(schema.AddNewer); setVisibleAddNewer(true); - setCurrentCollection(name); + setCurrentCollection(collectionData.name); + setCurrentDataSource(collectionData.dataSource); }; return ( - + addbuttonClick(arg)} /> - + - + - + ); }, { displayName: 'CreateRecordAction' }, diff --git a/packages/core/client/src/schema-component/antd/association-field/hooks.ts b/packages/core/client/src/schema-component/antd/association-field/hooks.ts index 1c26ee10b..3cfb024e4 100644 --- a/packages/core/client/src/schema-component/antd/association-field/hooks.ts +++ b/packages/core/client/src/schema-component/antd/association-field/hooks.ts @@ -5,7 +5,7 @@ import { flatten, getValuesByPath } from '@nocobase/utils/client'; import _, { isString } from 'lodash'; import cloneDeep from 'lodash/cloneDeep'; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { useCollection, useCollectionManager } from '../../../collection-manager'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; import { isInFilterFormBlock } from '../../../filter-provider'; import { mergeFilter } from '../../../filter-provider/utils'; import { useRecord } from '../../../record-provider'; @@ -51,8 +51,8 @@ export default function useServiceOptions(props) { const { action = 'list', service, useOriginalFilter } = props; const fieldSchema = useFieldSchema(); const field = useField(); - const { getField } = useCollection(); - const { getCollectionJoinField } = useCollectionManager(); + const { getField } = useCollection_deprecated(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); const record = useRecord(); const { parseFilter, findVariable } = useParseDataScopeFilter(); const [fieldServiceFilter, setFieldServiceFilter] = useState(null); @@ -186,7 +186,7 @@ export const SubFormProvider = SubFormContext.Provider; * * ## 为什么要有这个方法? * 1. 目前使用 useForm 方法获取到的是普通表单区块的 form 对象,无法通过简单的方法获取到子表单对应的 form 对象; - * 2. 虽然现在 useRecord 也可以获取到相同值的对象,但是这个对象不是响应式的(因其内部 copy 过一次),字段值变更时无法监听到; + * 2. 虽然现在 useRecord 也可以获取到相同值的对象,但是这个对象不是响应式的(因其内部 copy 过一次),字段值变更时无法监听到; * 3. 可能更好的方式是在 useForm 返回的 form 对象添加一个 parent 属性,但可能会影响其它部分的代码,所以暂时不做修改; * @returns */ diff --git a/packages/core/client/src/schema-component/antd/association-field/schema.ts b/packages/core/client/src/schema-component/antd/association-field/schema.ts index f0ae6c8e7..76331ff1e 100644 --- a/packages/core/client/src/schema-component/antd/association-field/schema.ts +++ b/packages/core/client/src/schema-component/antd/association-field/schema.ts @@ -71,7 +71,9 @@ export default { title: '{{ t("Submit") }}', 'x-action': 'submit', 'x-component': 'Action', - 'x-designer': 'Action.Designer', + // 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:updateSubmit', 'x-component-props': { type: 'primary', htmlType: 'submit', diff --git a/packages/core/client/src/schema-component/antd/association-field/util.ts b/packages/core/client/src/schema-component/antd/association-field/util.ts index 694a071d0..9ed66183f 100644 --- a/packages/core/client/src/schema-component/antd/association-field/util.ts +++ b/packages/core/client/src/schema-component/antd/association-field/util.ts @@ -4,14 +4,26 @@ import { getDefaultFormat, str2moment } from '@nocobase/utils/client'; import { Tag } from 'antd'; import dayjs from 'dayjs'; import React from 'react'; -import { CollectionFieldOptions, useCollectionManager } from '../../../collection-manager'; +import { CollectionFieldOptions_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; + +export const useLabelUiSchemaV2 = () => { + const { getCollectionJoinField } = useCollectionManager_deprecated(); + + return (collectionName: string, label: string): ISchema => { + if (!collectionName) { + return; + } + const labelField = getCollectionJoinField(`${collectionName}.${label}`) as CollectionFieldOptions_deprecated; + return labelField?.uiSchema; + }; +}; export const useLabelUiSchema = (collectionName: string, label: string): ISchema => { - const { getCollectionJoinField } = useCollectionManager(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); if (!collectionName) { return; } - const labelField = getCollectionJoinField(`${collectionName}.${label}`) as CollectionFieldOptions; + const labelField = getCollectionJoinField(`${collectionName}.${label}`) as CollectionFieldOptions_deprecated; return labelField?.uiSchema; }; diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.BlockDesigner.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.BlockDesigner.tsx index bf6053dc0..387d31e92 100644 --- a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.BlockDesigner.tsx +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.BlockDesigner.tsx @@ -1,7 +1,7 @@ import { useFieldSchema } from '@formily/react'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useCollection } from '../../../collection-manager'; +import { useCollection_deprecated } from '../../../collection-manager'; import { FilterBlockType } from '../../../filter-provider/utils'; import { GeneralSchemaDesigner, @@ -14,7 +14,7 @@ import { import { useSchemaTemplate } from '../../../schema-templates'; export const AssociationFilterBlockDesigner = () => { - const { name, title } = useCollection(); + const { name, title } = useCollection_deprecated(); const template = useSchemaTemplate(); const fieldSchema = useFieldSchema(); const { t } = useTranslation(); diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.FilterBlockInitializer.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.FilterBlockInitializer.tsx deleted file mode 100644 index fa2a37108..000000000 --- a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.FilterBlockInitializer.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useOptionalFieldList } from '../../../block-provider/hooks'; -import { useAssociatedFields } from '../../../filter-provider/utils'; -import { SchemaInitializer } from '../../../application/schema-initializer/SchemaInitializer'; - -export const associationFilterFilterBlockInitializer = new SchemaInitializer({ - name: 'AssociationFilter.FilterBlockInitializer', - style: { marginTop: 16 }, - icon: 'SettingOutlined', - title: '{{t("Configure fields")}}', - items: [ - { - type: 'itemGroup', - name: 'associationFields', - title: '{{t("Association fields")}}', - useChildren() { - const associatedFields = useAssociatedFields(); - const useProps = '{{useAssociationFilterBlockProps}}'; - const children = associatedFields.map((field) => ({ - name: field.key, - title: field.uiSchema?.title, - Component: 'AssociationFilterDesignerDisplayField', - schema: { - name: field.name, - title: field.uiSchema?.title, - type: 'void', - 'x-designer': 'AssociationFilter.Item.Designer', - 'x-component': 'AssociationFilter.Item', - 'x-component-props': { - fieldNames: { - label: field.targetKey || 'id', - }, - useProps, - }, - properties: {}, - }, - })); - return children; - }, - }, - { - name: 'choicesFields', - type: 'itemGroup', - title: '{{t("Choices fields")}}', - checkChildrenLength: true, - useChildren() { - const optionalList = useOptionalFieldList(); - const useProps = '{{useAssociationFilterBlockProps}}'; - const optionalChildren = optionalList.map((field) => ({ - name: field.key, - title: field.uiSchema.title, - Component: 'AssociationFilterDesignerDisplayField', - schema: { - name: field.name, - title: field.uiSchema.title, - interface: field.interface, - type: 'void', - 'x-designer': 'AssociationFilter.Item.Designer', - 'x-component': 'AssociationFilter.Item', - 'x-component-props': { - fieldNames: { - label: field.name, - }, - useProps, - }, - properties: {}, - }, - })); - - return optionalChildren; - }, - }, - ], -}); diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Initializer.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Initializer.tsx index 975a104e3..ba5e77ca2 100644 --- a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Initializer.tsx +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Initializer.tsx @@ -1,6 +1,6 @@ import { SchemaInitializerItemType } from '../../../application'; -import { useAssociatedFields } from '../../../filter-provider/utils'; import { SchemaInitializer } from '../../../application/schema-initializer/SchemaInitializer'; +import { useAssociatedFields } from '../../../filter-provider/utils'; export const associationFilterInitializer = new SchemaInitializer({ name: 'AssociationFilter.Initializer', @@ -26,7 +26,9 @@ export const associationFilterInitializer = new SchemaInitializer({ name: field.name, title: field.uiSchema?.title, type: 'void', - 'x-designer': 'AssociationFilter.Item.Designer', + // 'x-designer': 'AssociationFilter.Item.Designer', + 'x-toolbar': 'CollapseItemSchemaToolbar', + 'x-settings': 'fieldSettings:FilterCollapseItem', 'x-component': 'AssociationFilter.Item', 'x-component-props': { fieldNames: { diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.Designer.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.Designer.tsx index cc542a293..e8c11d935 100644 --- a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.Designer.tsx @@ -3,10 +3,9 @@ import _ from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useFormBlockContext } from '../../../block-provider'; -import { useCollection, useCollectionManager } from '../../../collection-manager'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; import { GeneralSchemaDesigner, - SchemaSettingsDataScope, SchemaSettingsDefaultSortingRules, SchemaSettingsModalItem, SchemaSettingsRemove, @@ -14,6 +13,7 @@ import { SchemaSettingsSwitchItem, } from '../../../schema-settings'; import { useCompile, useDesignable } from '../../hooks'; +import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope'; export const AssociationFilterItemDesigner = (props) => { const fieldSchema = useFieldSchema(); @@ -21,12 +21,12 @@ export const AssociationFilterItemDesigner = (props) => { const field = useField(); const { t } = useTranslation(); - const { getCollectionJoinField } = useCollectionManager(); - const { getField } = useCollection(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(); const compile = useCompile(); const { dn } = useDesignable(); diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.tsx index cd721caec..ba122283e 100644 --- a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.tsx +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { useFieldSchema } from '@formily/react'; import cls from 'classnames'; import React from 'react'; -import { useCollection } from '../../../collection-manager'; +import { useCollection_deprecated } from '../../../collection-manager'; import { DndContext, SortableItem } from '../../common'; import { useDesigner } from '../../hooks'; import { useToken } from '../__builtins__'; @@ -12,7 +12,10 @@ import { AssociationFilterItemDesigner } from './AssociationFilter.Item.Designer import { AssociationFilterProvider } from './AssociationFilterProvider'; import { useSchemaInitializerRender } from '../../../application'; import { Plugin } from '../../../application/Plugin'; -import { associationFilterFilterBlockInitializer } from './AssociationFilter.FilterBlockInitializer'; +import { + associationFilterFilterBlockInitializer, + filterCollapseItemInitializer, +} from '../../../modules/blocks/filter-blocks/collapse/filterCollapseItemInitializer'; import { associationFilterInitializer } from './AssociationFilter.Initializer'; export const AssociationFilter = (props) => { @@ -94,13 +97,14 @@ AssociationFilter.BlockDesigner = AssociationFilterBlockDesigner; AssociationFilter.useAssociationField = () => { const fieldSchema = useFieldSchema(); - const { getField } = useCollection(); + const { getField } = useCollection_deprecated(); return React.useMemo(() => getField(fieldSchema.name as any), [fieldSchema.name]); }; export class AssociationFilterPlugin extends Plugin { async load() { this.app.schemaInitializerManager.add(associationFilterFilterBlockInitializer); + this.app.schemaInitializerManager.add(filterCollapseItemInitializer); this.app.schemaInitializerManager.add(associationFilterInitializer); } } diff --git a/packages/core/client/src/schema-component/antd/association-filter/utilts.ts b/packages/core/client/src/schema-component/antd/association-filter/utilts.ts index 0950c2d3e..268ccc47d 100644 --- a/packages/core/client/src/schema-component/antd/association-filter/utilts.ts +++ b/packages/core/client/src/schema-component/antd/association-filter/utilts.ts @@ -1,5 +1,5 @@ -import { CollectionFieldOptions } from '../../../collection-manager'; +import { CollectionFieldOptions_deprecated } from '../../../collection-manager'; -export const getTargetKey = (field?: CollectionFieldOptions) => { +export const getTargetKey = (field?: CollectionFieldOptions_deprecated) => { return field?.targetKey || 'id'; }; diff --git a/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx b/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx index 937a25fc3..238e83989 100644 --- a/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx +++ b/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx @@ -7,19 +7,18 @@ import _ from 'lodash'; import React, { useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useFilterByTk, useFormBlockContext } from '../../../block-provider'; -import { useCollection, useCollectionManager, useSortFields } from '../../../collection-manager'; +import { useCollection_deprecated, useCollectionManager_deprecated, useSortFields } from '../../../collection-manager'; import { GeneralSchemaItems } from '../../../schema-items'; import { - GeneralSchemaDesigner, - SchemaSettingsDataScope, SchemaSettingsDivider, SchemaSettingsModalItem, SchemaSettingsRemove, SchemaSettingsSelectItem, SchemaSettingsSwitchItem, - isPatternDisabled, -} from '../../../schema-settings'; -import useIsAllowToSetDefaultValue from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; +} from '../../../schema-settings/SchemaSettings'; +import { isPatternDisabled } from '../../../schema-settings/isPatternDisabled'; +import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope'; +import { useIsAllowToSetDefaultValue } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; import { useIsShowMultipleSwitch } from '../../../schema-settings/hooks/useIsShowMultipleSwitch'; import { useCompile, useDesignable, useFieldComponentOptions, useFieldTitle } from '../../hooks'; import { removeNullCondition } from '../filter'; @@ -27,6 +26,7 @@ import { RemoteSelect, RemoteSelectProps } from '../remote-select'; import { defaultFieldNames } from '../select'; import { ReadPretty } from './ReadPretty'; import useServiceOptions from './useServiceOptions'; +import { GeneralSchemaDesigner } from '../../../schema-settings/GeneralSchemaDesigner'; export type AssociationSelectProps

= RemoteSelectProps

& { action?: string; @@ -99,8 +99,8 @@ export const AssociationSelect = InternalAssociationSelect as unknown as Associa AssociationSelect.Designer = function Designer() { const { getCollectionFields, getInterface, getCollectionJoinField, getCollection, isTitleField } = - useCollectionManager(); - const { getField } = useCollection(); + useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); const { form } = useFormBlockContext(); const field = useField(); const fieldSchema = useFieldSchema(); @@ -348,7 +348,9 @@ AssociationSelect.Designer = function Designer() { description: fieldSchema['description'], default: fieldSchema['default'], 'x-decorator': 'FormItem', - 'x-designer': 'FormItem.Designer', + // 'x-designer': 'FormItem.Designer', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', 'x-component': type, 'x-validator': fieldSchema['x-validator'], 'x-collection-field': fieldSchema['x-collection-field'], @@ -612,8 +614,8 @@ AssociationSelect.Designer = function Designer() { * @returns */ AssociationSelect.FilterDesigner = function FilterDesigner() { - const { getCollectionFields, getInterface, getCollectionJoinField } = useCollectionManager(); - const { getField } = useCollection(); + const { getCollectionFields, getInterface, getCollectionJoinField } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); const { form } = useFormBlockContext(); const field = useField(); const fieldSchema = useFieldSchema(); diff --git a/packages/core/client/src/schema-component/antd/association-select/useServiceOptions.ts b/packages/core/client/src/schema-component/antd/association-select/useServiceOptions.ts index 91b1c2038..ecb4a7ed4 100644 --- a/packages/core/client/src/schema-component/antd/association-select/useServiceOptions.ts +++ b/packages/core/client/src/schema-component/antd/association-select/useServiceOptions.ts @@ -1,6 +1,6 @@ import { useFieldSchema } from '@formily/react'; import { useCallback, useMemo } from 'react'; -import { useCollection, useCollectionManager } from '../../../collection-manager'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; import { isInFilterFormBlock } from '../../../filter-provider'; import { mergeFilter } from '../../../filter-provider/utils'; import { useRecord } from '../../../record-provider'; @@ -9,8 +9,8 @@ export default function useServiceOptions(props) { const { action = 'list', service, fieldNames } = props; const params = service?.params || {}; const fieldSchema = useFieldSchema(); - const { getField } = useCollection(); - const { getCollectionFields } = useCollectionManager(); + const { getField } = useCollection_deprecated(); + const { getCollectionFields } = useCollectionManager_deprecated(); const record = useRecord(); const normalizeValues = useCallback( diff --git a/packages/core/client/src/schema-component/antd/block-item/BlockItem.tsx b/packages/core/client/src/schema-component/antd/block-item/BlockItem.tsx index 16b714f7d..63a9b9a3e 100644 --- a/packages/core/client/src/schema-component/antd/block-item/BlockItem.tsx +++ b/packages/core/client/src/schema-component/antd/block-item/BlockItem.tsx @@ -4,10 +4,12 @@ import React from 'react'; import { SortableItem } from '../../common'; import { useDesigner, useProps } from '../../hooks'; import { useGetAriaLabelOfBlockItem } from './hooks/useGetAriaLabelOfBlockItem'; +import { useFieldSchema } from '@formily/react'; export const BlockItem: React.FC = (props) => { const { className, children } = useProps(props); const Designer = useDesigner(); + const fieldSchema = useFieldSchema(); const { getAriaLabel } = useGetAriaLabelOfBlockItem(props.name); return ( @@ -63,7 +65,7 @@ export const BlockItem: React.FC = (props) => { `, )} > - + {children} ); diff --git a/packages/core/client/src/schema-component/antd/block-item/hooks/useGetAriaLabelOfBlockItem.ts b/packages/core/client/src/schema-component/antd/block-item/hooks/useGetAriaLabelOfBlockItem.ts index 590bc24a2..91dce780d 100644 --- a/packages/core/client/src/schema-component/antd/block-item/hooks/useGetAriaLabelOfBlockItem.ts +++ b/packages/core/client/src/schema-component/antd/block-item/hooks/useGetAriaLabelOfBlockItem.ts @@ -2,7 +2,7 @@ import { useFieldSchema } from '@formily/react'; import _ from 'lodash'; import { useCallback } from 'react'; import { useBlockContext } from '../../../../block-provider/BlockProvider'; -import { useCollection } from '../../../../collection-manager'; +import { useCollection_deprecated } from '../../../../collection-manager'; import { useCompile } from '../../../hooks'; /** @@ -18,7 +18,7 @@ export const useGetAriaLabelOfBlockItem = (name?: string) => { const collectionField = compile(fieldSchema['x-collection-field']); let { name: blockName } = useBlockContext() || {}; // eslint-disable-next-line prefer-const - let { name: collectionName, getField } = useCollection(); + let { name: collectionName, getField } = useCollection_deprecated(); blockName = name || blockName; const title = compile(fieldSchema['title']) || compile(getField(fieldSchema.name)?.uiSchema?.title); diff --git a/packages/core/client/src/schema-component/antd/card-item/CardItem.tsx b/packages/core/client/src/schema-component/antd/card-item/CardItem.tsx index 39264df58..f6b29d5bb 100644 --- a/packages/core/client/src/schema-component/antd/card-item/CardItem.tsx +++ b/packages/core/client/src/schema-component/antd/card-item/CardItem.tsx @@ -16,9 +16,8 @@ export const CardItem = (props: Props) => { const { children, name, ...restProps } = props; const template = useSchemaTemplate(); const fieldSchema = useFieldSchema(); - const templateKey = fieldSchema['x-template-key']; + const templateKey = fieldSchema?.['x-template-key']; const { wrapSSR, componentCls, hashId } = useStyles(); - return wrapSSR( templateKey && !template ? null : ( diff --git a/packages/core/client/src/schema-component/antd/checkbox/Checkbox.tsx b/packages/core/client/src/schema-component/antd/checkbox/Checkbox.tsx index 69ee0fa86..567b4f53b 100644 --- a/packages/core/client/src/schema-component/antd/checkbox/Checkbox.tsx +++ b/packages/core/client/src/schema-component/antd/checkbox/Checkbox.tsx @@ -5,7 +5,7 @@ import { Checkbox as AntdCheckbox, Tag } from 'antd'; import type { CheckboxGroupProps, CheckboxProps } from 'antd/es/checkbox'; import uniq from 'lodash/uniq'; import React from 'react'; -import { useCollectionField } from '../../../collection-manager'; +import { useCollectionField } from '../../../data-source/collection-field/CollectionFieldProvider'; import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip'; type ComposedCheckbox = React.ForwardRefExoticComponent< @@ -51,8 +51,8 @@ Checkbox.Group = connect( return null; } const field = useField(); - const { uiSchema } = useCollectionField(); - const dataSource = field.dataSource || uiSchema.enum || []; + const collectionField = useCollectionField(); + const dataSource = field.dataSource || collectionField?.uiSchema.enum || []; const value = uniq(field.value ? field.value : []); return ( diff --git a/packages/core/client/src/schema-component/antd/collection-select/CollectionSelect.tsx b/packages/core/client/src/schema-component/antd/collection-select/CollectionSelect.tsx index 84cdc4cc6..fab0388f7 100644 --- a/packages/core/client/src/schema-component/antd/collection-select/CollectionSelect.tsx +++ b/packages/core/client/src/schema-component/antd/collection-select/CollectionSelect.tsx @@ -3,7 +3,7 @@ import { Select, SelectProps, Tag } from 'antd'; import React, { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelfAndChildrenCollections } from '../../../collection-manager/action-hooks'; -import { useCollection, useCollectionManager } from '../../../collection-manager/hooks'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager/hooks'; import { useCompile } from '../../hooks'; import { FilterContext } from '../filter/context'; @@ -16,10 +16,10 @@ function useOptions({ filter, isTableOid }: CollectionSelectProps) { const compile = useCompile(); const field: any = useField(); const ctx: any = useContext(FilterContext); - const collection = useCollection(); + const collection = useCollection_deprecated(); const targetCollection = isTableOid && (ctx?.field?.collectionName || ctx?.collectionName || collection.name); const inheritCollections = useSelfAndChildrenCollections(targetCollection); - const { collections = [] } = useCollectionManager(); + const { collections = [] } = useCollectionManager_deprecated(); const currentCollections = field?.dataSource ? collections.filter((v) => { return field?.dataSource.find((i) => i.value === v.name) || field?.dataSource.includes(v.name); diff --git a/packages/core/client/src/schema-component/antd/collection-select/demos/demo1.tsx b/packages/core/client/src/schema-component/antd/collection-select/demos/demo1.tsx index 39af35a20..b112df39e 100644 --- a/packages/core/client/src/schema-component/antd/collection-select/demos/demo1.tsx +++ b/packages/core/client/src/schema-component/antd/collection-select/demos/demo1.tsx @@ -1,5 +1,5 @@ import { FormItem } from '@formily/antd-v5'; -import { CollectionManagerProvider, CollectionSelect, FormProvider, SchemaComponent } from '@nocobase/client'; +import { ExtendCollectionsProvider, CollectionSelect, FormProvider, SchemaComponent } from '@nocobase/client'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -22,9 +22,9 @@ export default () => { return ( - + - + ); }; diff --git a/packages/core/client/src/schema-component/antd/cron/CronSet.tsx b/packages/core/client/src/schema-component/antd/cron/CronSet.tsx index 79446077b..e01279878 100644 --- a/packages/core/client/src/schema-component/antd/cron/CronSet.tsx +++ b/packages/core/client/src/schema-component/antd/cron/CronSet.tsx @@ -1,7 +1,7 @@ import { connect, mapReadPretty, useFieldSchema } from '@formily/react'; import { Select, SelectProps } from 'antd'; import React, { useEffect, useMemo, useState } from 'react'; -import { useCollection } from '../../../collection-manager'; +import { useCollection_deprecated } from '../../../collection-manager'; import { useCompile } from '../../hooks'; import { EllipsisWithTooltip } from '../input'; import Cron from './Cron'; @@ -12,7 +12,7 @@ interface CronSetProps extends SelectProps { const CronSetInternal = (props: CronSetProps) => { const { onChange, value } = props; - const { getField } = useCollection(); + const { getField } = useCollection_deprecated(); const fieldSchema = useFieldSchema(); const uiSchemaOptions = getField(fieldSchema?.name)?.uiSchema.enum; const compile = useCompile(); @@ -87,7 +87,7 @@ const ReadPretty = (props: CronSetProps) => { const { value } = props; const compile = useCompile(); const fieldSchema = useFieldSchema(); - const { getField } = useCollection(); + const { getField } = useCollection_deprecated(); const uiSchemaOptions = getField(fieldSchema?.name)?.uiSchema.enum; const options = useMemo(() => { diff --git a/packages/core/client/src/schema-component/antd/expand-action/Expand.Action.Design.tsx b/packages/core/client/src/schema-component/antd/expand-action/Expand.Action.Design.tsx index b089332a3..8ba9a60bf 100644 --- a/packages/core/client/src/schema-component/antd/expand-action/Expand.Action.Design.tsx +++ b/packages/core/client/src/schema-component/antd/expand-action/Expand.Action.Design.tsx @@ -9,6 +9,11 @@ import { SchemaSettingsRemove, } from '../../../schema-settings'; +/** + * + * @deprecated + * @returns + */ export const ExpandActionDesign = (props) => { const { t } = useTranslation(); const fieldSchema = useFieldSchema(); diff --git a/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx b/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx index d26d99ebb..cb08edcda 100644 --- a/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx +++ b/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx @@ -2,7 +2,7 @@ import { createForm, onFieldValueChange } from '@formily/core'; import { FieldContext, FormContext } from '@formily/react'; import { merge } from '@formily/shared'; import React, { useCallback, useContext, useMemo } from 'react'; -import { CollectionFieldOptions } from '../../../collection-manager'; +import { CollectionFieldOptions_deprecated } from '../../../collection-manager'; import { SchemaComponent } from '../../core'; import { useComponent } from '../../hooks'; import { FilterContext } from './context'; @@ -12,7 +12,7 @@ export interface DynamicComponentProps { /** * `Filter` 组件左侧选择的字段 */ - collectionField: CollectionFieldOptions; + collectionField: CollectionFieldOptions_deprecated; onChange: (value: any) => void; renderSchemaComponent: () => React.JSX.Element; } @@ -20,7 +20,7 @@ export interface DynamicComponentProps { interface Props { schema: any; value: any; - collectionField: CollectionFieldOptions; + collectionField: CollectionFieldOptions_deprecated; onChange: (value: any) => void; } diff --git a/packages/core/client/src/schema-component/antd/filter/Filter.Action.Designer.tsx b/packages/core/client/src/schema-component/antd/filter/Filter.Action.Designer.tsx index 605b6b1d1..5a95ee5a7 100644 --- a/packages/core/client/src/schema-component/antd/filter/Filter.Action.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/filter/Filter.Action.Designer.tsx @@ -2,7 +2,7 @@ import { ISchema, useField, useFieldSchema } from '@formily/react'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useDesignable } from '../..'; -import { useCollection, useCollectionManager } from '../../../collection-manager'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; import { GeneralSchemaDesigner, SchemaSettingsDivider, @@ -14,7 +14,7 @@ import { import { useCompile } from '../../hooks'; export const useFilterableFields = (collectionName: string) => { - const { getCollectionFields, getInterface } = useCollectionManager(); + const { getCollectionFields, getInterface } = useCollectionManager_deprecated(); const fields = getCollectionFields(collectionName); return fields?.filter?.((field) => { if (!field.interface) { @@ -28,49 +28,60 @@ export const useFilterableFields = (collectionName: string) => { }); }; -export const FilterActionDesigner = (props) => { - const field = useField(); +export const FilterableFieldsSchemaSettingsItem = () => { const fieldSchema = useFieldSchema(); const { dn } = useDesignable(); - const { name } = useCollection(); + const { name } = useCollection_deprecated(); const fields = useFilterableFields(name); const compile = useCompile(); const { t } = useTranslation(); const nonfilterable = fieldSchema?.['x-component-props']?.nonfilterable || []; + + return ( + + {fields.map((field) => { + const checked = !nonfilterable.includes(field.name); + return ( + { + fieldSchema['x-component-props'] = fieldSchema?.['x-component-props'] || {}; + const nonfilterable = fieldSchema?.['x-component-props']?.nonfilterable || []; + if (!value) { + nonfilterable.push(field.name); + } else { + const index = nonfilterable.indexOf(field.name); + nonfilterable.splice(index, 1); + } + fieldSchema['x-component-props'].nonfilterable = nonfilterable; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-component-props': { + ...fieldSchema['x-component-props'], + }, + }, + }); + dn.refresh(); + }} + /> + ); + })} + + ); +}; + +export const FilterActionDesigner = (props) => { + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn } = useDesignable(); + const { t } = useTranslation(); + return ( - - {fields.map((field) => { - const checked = !nonfilterable.includes(field.name); - return ( - { - fieldSchema['x-component-props'] = fieldSchema?.['x-component-props'] || {}; - const nonfilterable = fieldSchema?.['x-component-props']?.nonfilterable || []; - if (!value) { - nonfilterable.push(field.name); - } else { - const index = nonfilterable.indexOf(field.name); - nonfilterable.splice(index, 1); - } - fieldSchema['x-component-props'].nonfilterable = nonfilterable; - dn.emit('patch', { - schema: { - ['x-uid']: fieldSchema['x-uid'], - 'x-component-props': { - ...fieldSchema['x-component-props'], - }, - }, - }); - dn.refresh(); - }} - /> - ); - })} - + { it('dynamic options', async () => { render(); + await waitFor(() => { + expect(screen.getByText(/test1/i)).toBeInTheDocument(); + }); + const addBtn = screen.getByText(/^Add condition$/i); const addGroupBtn = screen.getByText(/^Add condition group$/i); - expect(screen.getByText(/test1/i)).toBeInTheDocument(); expect(addBtn).toBeInTheDocument(); expect(addGroupBtn).toBeInTheDocument(); diff --git a/packages/core/client/src/schema-component/antd/filter/demos/demo6.tsx b/packages/core/client/src/schema-component/antd/filter/demos/demo6.tsx index 145f1de64..3156148be 100644 --- a/packages/core/client/src/schema-component/antd/filter/demos/demo6.tsx +++ b/packages/core/client/src/schema-component/antd/filter/demos/demo6.tsx @@ -1,13 +1,16 @@ import { AntdSchemaComponentProvider, - CollectionManagerProvider, - CollectionProvider, + Application, + CollectionPlugin, + CollectionProvider_deprecated, Filter, Input, + LocalDataSource, SchemaComponent, - SchemaComponentProvider, - useCollection, - useCollectionManager, + useCollection_deprecated, + Plugin, + DEFAULT_DATA_SOURCE_KEY, + DEFAULT_DATA_SOURCE_TITLE, } from '@nocobase/client'; import { Select } from 'antd'; import React, { useState } from 'react'; @@ -16,6 +19,7 @@ import { useFilterOptions } from '../useFilterActionProps'; const collections = [ { name: 'test1', + template: 'general', fields: [ { type: 'string', @@ -32,6 +36,7 @@ const collections = [ }, { name: 'test2', + template: 'general', fields: [ { type: 'string', @@ -57,7 +62,7 @@ const schema: any = { 'x-component': 'Filter', 'x-component-props': { useProps: () => { - const { name } = useCollection(); + const { name } = useCollection_deprecated(); const options = useFilterOptions(name); return { options, @@ -69,8 +74,7 @@ const schema: any = { }; const SwitchCollection = (props) => { - const [collection, setCollection] = useState(collections[0]); - const { getCollection } = useCollectionManager(); + const [collection, setCollection] = useState(collections[0].name); return (

: props.suffixIcon, }; diff --git a/packages/core/client/src/schema-component/antd/select/ReadPretty.tsx b/packages/core/client/src/schema-component/antd/select/ReadPretty.tsx index 97f2592be..2cb894014 100644 --- a/packages/core/client/src/schema-component/antd/select/ReadPretty.tsx +++ b/packages/core/client/src/schema-component/antd/select/ReadPretty.tsx @@ -5,7 +5,7 @@ import { Tag } from 'antd'; import React from 'react'; import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip'; import { defaultFieldNames, getCurrentOptions } from './utils'; -import { useCollectionField } from '../../../collection-manager'; +import { useCollectionField } from '../../../data-source/collection-field/CollectionFieldProvider'; export const ReadPretty = observer( (props: any) => { @@ -18,8 +18,8 @@ export const ReadPretty = observer( if (isArrayField(field) && field?.value?.length === 0) { return
; } - const { uiSchema } = useCollectionField(); - const dataSource = field.dataSource || props.options || uiSchema.enum || []; + const collectionField = useCollectionField(); + const dataSource = field.dataSource || props.options || collectionField?.uiSchema.enum || []; const currentOptions = getCurrentOptions(field.value, dataSource, fieldNames); return ( diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.ActionBar.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.ActionBar.tsx index 2a991ca7f..e4a5de134 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.ActionBar.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.ActionBar.tsx @@ -4,7 +4,9 @@ import React from 'react'; import { SortableItem, useDesigner } from '../..'; export const designerCss = css` - position: relative; + margin: -18px -16px; + padding: 18px 16px; + &:hover { > .general-schema-designer { display: block; @@ -20,10 +22,6 @@ export const designerCss = css` display: none; background: var(--colorBgSettingsHover) !important; border: 0 !important; - top: -16px !important; - bottom: -16px !important; - left: -16px !important; - right: -16px !important; pointer-events: none; > .general-schema-designer-icons { position: absolute; diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Decorator.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Decorator.tsx index d1ddeae4b..ccf589c9f 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Decorator.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Decorator.tsx @@ -1,14 +1,21 @@ import { useField, useFieldSchema } from '@formily/react'; import React, { useLayoutEffect } from 'react'; -import { SortableItem, useCollection, useCollectionManager, useCompile, useDesigner } from '../../../'; +import { + SortableItem, + useCollection_deprecated, + useCollectionManager_deprecated, + useCompile, + useDesigner, + CollectionFieldContext, +} from '../../../'; import { designerCss } from './Table.Column.ActionBar'; import { isCollectionFieldComponent } from './utils'; export const useColumnSchema = () => { - const { getField } = useCollection(); + const { getField } = useCollection_deprecated(); const compile = useCompile(); const columnSchema = useFieldSchema(); - const { getCollectionJoinField } = useCollectionManager(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); const fieldSchema = columnSchema.reduceProperties((buf, s) => { if (isCollectionFieldComponent(s)) { return s; @@ -41,10 +48,11 @@ export const TableColumnDecorator = (props) => { }, [uiSchema?.title]); return ( - - {/* */} -
{field?.title || compile(uiSchema?.title)}
- {/*
+ + {/* */} +
{field?.title || compile(uiSchema?.title)}
+ {/*
{ field.title = uid(); // columnSchema.title = field.title = field.title; @@ -56,6 +64,7 @@ export const TableColumnDecorator = (props) => { > Edit
*/} + ); }; diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx index 7bcb76982..2a9f42a8f 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx @@ -3,29 +3,29 @@ import { set } from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useFormBlockContext } from '../../../block-provider'; -import { useCollectionManager } from '../../../collection-manager'; +import { useCollectionManager_deprecated } from '../../../collection-manager'; import { GeneralSchemaDesigner, - SchemaSettingsDateFormat, - SchemaSettingsDataScope, - SchemaSettingsDefaultValue, SchemaSettingsDivider, SchemaSettingsModalItem, SchemaSettingsRemove, SchemaSettingsSelectItem, - SchemaSettingsSortingRule, SchemaSettingsSwitchItem, - isPatternDisabled, } from '../../../schema-settings'; -import useIsAllowToSetDefaultValue from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; +import { useIsAllowToSetDefaultValue } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; import { useCompile, useDesignable, useFieldModeOptions } from '../../hooks'; import { useAssociationFieldContext } from '../association-field/hooks'; import { removeNullCondition } from '../filter'; +import { SchemaSettingsDefaultValue } from '../../../schema-settings/SchemaSettingsDefaultValue'; +import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope'; +import { isPatternDisabled } from '../../../schema-settings/isPatternDisabled'; +import { SchemaSettingsDateFormat } from '../../../schema-settings/SchemaSettingsDateFormat'; +import { SchemaSettingsSortingRule } from '../../../schema-settings/SchemaSettingsSortingRule'; -const useLabelFields = (collectionName?: any) => { +export const useLabelFields = (collectionName?: any) => { // 需要在组件顶层调用 const compile = useCompile(); - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(); if (!collectionName) { return []; } @@ -42,7 +42,7 @@ const useLabelFields = (collectionName?: any) => { export const useColorFields = (collectionName?: any) => { const compile = useCompile(); - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(); if (!collectionName) { return []; } @@ -59,7 +59,7 @@ export const useColorFields = (collectionName?: any) => { export const TableColumnDesigner = (props) => { const { uiSchema, fieldSchema, collectionField } = props; const { form } = useFormBlockContext(); - const { getInterface, getCollection } = useCollectionManager(); + const { getInterface, getCollection } = useCollectionManager_deprecated(); const field: any = useField(); const { t } = useTranslation(); const columnSchema = useFieldSchema(); diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.Designer.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.Designer.tsx index d7e4ab5cb..0fadf360b 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.Designer.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { useCollection } from '../../../collection-manager'; +import { useCollection_deprecated } from '../../../collection-manager'; import { GeneralSchemaDesigner, SchemaSettingsRemove } from '../../../schema-settings'; export const TableDesigner = () => { - const { name, title } = useCollection(); + const { name, title } = useCollection_deprecated(); return ( { const field = useField(); @@ -33,11 +37,13 @@ const useArrayField = (props) => { }; const useTableColumns = (props: { showDel?: boolean; isSubTable?: boolean }) => { + const { token } = useToken(); const field = useArrayField(props); const schema = useFieldSchema(); const { schemaInWhitelist } = useACLFieldWhitelist(); const { designable } = useDesignable(); const { exists, render } = useSchemaInitializerRender(schema['x-initializer'], schema['x-initializer-props']); + const parentRecordData = useCollectionParentRecordData(); const columns = schema .reduceProperties((buf, s) => { if (isColumnComponent(s) && schemaInWhitelist(Object.values(s.properties || {}).pop())) { @@ -64,9 +70,18 @@ const useTableColumns = (props: { showDel?: boolean; isSubTable?: boolean }) => return ( - + - + { - const newKeys = flag ? [...expandedKeys, record.id] : expandedKeys.filter((i) => record.id !== i); + const newKeys = flag + ? [...expandedKeys, record[collection.getPrimaryKey()]] + : expandedKeys.filter((i) => record[collection.getPrimaryKey()] !== i); setExpandesKeys(newKeys); onExpand?.(flag, record); }, diff --git a/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx b/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx index b6b43e93e..bb60f25ef 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx @@ -1,18 +1,18 @@ import { ArrayItems } from '@formily/antd-v5'; import { ISchema, useField, useFieldSchema } from '@formily/react'; +import { Field } from '@formily/core'; import React, { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useAPIClient } from '../../../api-client'; import { useFormBlockContext, useTableBlockContext } from '../../../block-provider'; -import { useCollection, useCollectionManager } from '../../../collection-manager'; +import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager'; import { useSortFields } from '../../../collection-manager/action-hooks'; import { FilterBlockType, mergeFilter } from '../../../filter-provider/utils'; -import { RecordProvider, useRecord } from '../../../record-provider'; +import { useRecord } from '../../../record-provider'; import { GeneralSchemaDesigner, SchemaSettingsBlockTitleItem, SchemaSettingsConnectDataBlocks, - SchemaSettingsDataScope, SchemaSettingsDivider, SchemaSettingsModalItem, SchemaSettingsRemove, @@ -23,11 +23,51 @@ import { import { useSchemaTemplate } from '../../../schema-templates'; import { useDesignable } from '../../hooks'; import { removeNullCondition } from '../filter'; -import { FixedBlockDesignerItem } from '../page'; +import { useCompile } from '../../'; +import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope'; +import { FixedBlockDesignerItem } from '../page/FixedBlockDesignerItem'; + +export const EditSortField = () => { + const { fields } = useCollection_deprecated(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + const compile = useCompile(); + const { service } = useTableBlockContext(); + + const options = fields + .filter((field) => !field?.target && field.interface === 'sort') + .map((field) => ({ + value: field?.name, + label: compile(field?.uiSchema?.title) || field?.name, + })); + + return ( + { + fieldSchema['x-decorator-props'].dragSortBy = dragSortBy; + service.run({ ...service.params?.[0], sort: dragSortBy }); + field.decoratorProps.dragSortBy = dragSortBy; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + dn.refresh(); + }} + /> + ); +}; export const TableBlockDesigner = () => { - const { name, title, sortable } = useCollection(); - const { getCollectionField, getCollection } = useCollectionManager(); + const { name, title, sortable } = useCollection_deprecated(); + const { getCollectionField, getCollection } = useCollectionManager_deprecated(); const field = useField(); const fieldSchema = useFieldSchema(); const { form } = useFormBlockContext(); @@ -52,7 +92,7 @@ export const TableBlockDesigner = () => { }; }); const template = useSchemaTemplate(); - const collection = useCollection(); + const collection = useCollection_deprecated(); const { dragSort, resource } = field.decoratorProps; const collectionField = resource && getCollectionField(resource); const treeCollection = resource?.includes('.') ? getCollection(collectionField?.target)?.tree : !!collection?.tree; @@ -79,195 +119,191 @@ export const TableBlockDesigner = () => { ); const api = useAPIClient(); return ( - // fix https://nocobase.height.app/T-2259 - - - - {collection?.tree && collectionField?.collectionName === collectionField?.target && ( - { - field.decoratorProps.treeTable = flag; - fieldSchema['x-decorator-props'].treeTable = flag; - const params = { - ...service.params?.[0], - tree: flag ? true : null, - }; - dn.emit('patch', { - schema: fieldSchema, - }); - dn.refresh(); - service.run(params); - }} - /> - )} - {sortable && ( - { - if (dragSort && collectionField) { - const { data } = await api.resource('collections.fields', collectionField.collectionName).update({ - filterByTk: collectionField.name, - values: { - sortable: true, - }, - }); - const sortBy = data?.data?.[0]?.sortBy; - fieldSchema['x-decorator-props'].dragSortBy = sortBy; - } - field.decoratorProps.dragSort = dragSort; - fieldSchema['x-decorator-props'].dragSort = dragSort; - service.run({ ...service.params?.[0], sort: fieldSchema['x-decorator-props'].dragSortBy }); - dn.emit('patch', { - schema: { - ['x-uid']: fieldSchema['x-uid'], - 'x-decorator-props': fieldSchema['x-decorator-props'], - }, - }); - }} - /> - )} - - + + {collection?.tree && collectionField?.collectionName === collectionField?.target && ( + { + field.decoratorProps.treeTable = flag; + fieldSchema['x-decorator-props'].treeTable = flag; + const params = { + ...service.params?.[0], + tree: flag ? true : null, + }; + dn.emit('patch', { + schema: fieldSchema, + }); + dn.refresh(); + service.run(params); + }} /> - {!dragSort && ( - { + if (dragSort && collectionField) { + const { data } = await api.resource('collections.fields', collectionField.collectionName).update({ + filterByTk: collectionField.name, + values: { + sortable: true, + }, + }); + // const sortBy = data?.data?.[0]?.sortBy; + // fieldSchema['x-decorator-props'].dragSortBy = sortBy; + } + field.decoratorProps.dragSort = dragSort; + fieldSchema['x-decorator-props'].dragSort = dragSort; + // service.run({ ...service.params?.[0], sort: fieldSchema['x-decorator-props'].dragSortBy }); + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }} + /> + {field.decoratorProps.dragSort && } + + + {!dragSort && ( + { - const sortArr = sort.map((item) => { - return item.direction === 'desc' ? `-${item.field}` : item.field; - }); - const params = field.decoratorProps.params || {}; - params.sort = sortArr; - field.decoratorProps.params = params; - fieldSchema['x-decorator-props']['params'] = params; - dn.emit('patch', { - schema: { - ['x-uid']: fieldSchema['x-uid'], - 'x-decorator-props': fieldSchema['x-decorator-props'], - }, - }); - service.run({ ...service.params?.[0], sort: sortArr }); - }} - /> - )} - { + }, + } as ISchema + } + onSubmit={({ sort }) => { + const sortArr = sort.map((item) => { + return item.direction === 'desc' ? `-${item.field}` : item.field; + }); const params = field.decoratorProps.params || {}; - params.pageSize = pageSize; + params.sort = sortArr; field.decoratorProps.params = params; fieldSchema['x-decorator-props']['params'] = params; - service.run({ ...service.params?.[0], pageSize, page: 1 }); dn.emit('patch', { schema: { ['x-uid']: fieldSchema['x-uid'], 'x-decorator-props': fieldSchema['x-decorator-props'], }, }); + service.run({ ...service.params?.[0], sort: sortArr }); }} /> - - {supportTemplate && } - {supportTemplate && ( - - )} - - - - + )} + { + const params = field.decoratorProps.params || {}; + params.pageSize = pageSize; + field.decoratorProps.params = params; + fieldSchema['x-decorator-props']['params'] = params; + service.run({ ...service.params?.[0], pageSize, page: 1 }); + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }} + /> + + {supportTemplate && } + {supportTemplate && ( + + )} + + + ); }; diff --git a/packages/core/client/src/schema-component/antd/table-v2/TableField.tsx b/packages/core/client/src/schema-component/antd/table-v2/TableField.tsx index 917afe1cc..b594cbbde 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/TableField.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/TableField.tsx @@ -2,14 +2,14 @@ import { Field } from '@formily/core'; import { observer, useField, useFieldSchema, useForm } from '@formily/react'; import React, { useEffect } from 'react'; import { useFormBlockContext } from '../../../block-provider'; -import { useCollection } from '../../../collection-manager'; +import { useCollection_deprecated } from '../../../collection-manager'; import { useCompile } from '../../hooks'; import { ActionBar } from '../action'; export const TableField: any = observer( (props) => { const fieldSchema = useFieldSchema(); - const { getField } = useCollection(); + const { getField } = useCollection_deprecated(); const field = useField(); const collectionField = getField(fieldSchema.name); const compile = useCompile(); diff --git a/packages/core/client/src/schema-component/antd/table-v2/TableSelectorDesigner.tsx b/packages/core/client/src/schema-component/antd/table-v2/TableSelectorDesigner.tsx index 774b23ec2..4daa33d22 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/TableSelectorDesigner.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/TableSelectorDesigner.tsx @@ -5,13 +5,12 @@ import React, { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { useFormBlockContext, useTableSelectorContext } from '../../../block-provider'; import { recursiveParent } from '../../../block-provider/TableSelectorProvider'; -import { useCollection, useCollectionManager } from '../../../collection-manager'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; import { useSortFields } from '../../../collection-manager/action-hooks'; import { useRecord } from '../../../record-provider'; import { useLocalVariables, useVariables } from '../../../variables'; import { GeneralSchemaDesigner, - SchemaSettingsDataScope, SchemaSettingsDivider, SchemaSettingsModalItem, SchemaSettingsRemove, @@ -23,10 +22,11 @@ import { useDesignable } from '../../hooks'; import { removeNullCondition } from '../filter'; import { VariableInput, getShouldChange } from '../../../schema-settings/VariableInput/VariableInput'; import { RecordPickerContext } from '../../antd/record-picker'; +import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope'; export const TableSelectorDesigner = () => { - const { name, title } = useCollection(); - const { getCollectionJoinField, getAllCollectionsInheritChain } = useCollectionManager(); + const { name, title } = useCollection_deprecated(); + const { getCollectionJoinField, getAllCollectionsInheritChain } = useCollectionManager_deprecated(); const field = useField(); const fieldSchema = useFieldSchema(); @@ -50,7 +50,7 @@ export const TableSelectorDesigner = () => { }; }); const template = useSchemaTemplate(); - const collection = useCollection(); + const collection = useCollection_deprecated(); const { dragSort } = field.decoratorProps; const record = useRecord(); const variables = useVariables(); diff --git a/packages/core/client/src/schema-component/antd/table-v2/components/ColumnFieldProvider.tsx b/packages/core/client/src/schema-component/antd/table-v2/components/ColumnFieldProvider.tsx index 67677602a..f41b8bdd2 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/components/ColumnFieldProvider.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/components/ColumnFieldProvider.tsx @@ -1,12 +1,12 @@ import { observer, RecursionField } from '@formily/react'; import React from 'react'; -import { useCollectionManager } from '../../../../collection-manager'; +import { useCollectionManager_deprecated } from '../../../../collection-manager'; import { useRecord } from '../../../../record-provider'; export const ColumnFieldProvider = observer( (props: { schema: any; basePath: any; children: any }) => { const { schema, basePath } = props; const record = useRecord(); - const { getCollectionJoinField } = useCollectionManager(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); const fieldSchema = schema.reduceProperties((buf, s) => { if (s['x-component'] === 'CollectionField') { return s; diff --git a/packages/core/client/src/schema-component/antd/table-v2/demos/demo1.tsx b/packages/core/client/src/schema-component/antd/table-v2/demos/demo1.tsx index 72faf549f..b1db206b5 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/demos/demo1.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/demos/demo1.tsx @@ -4,7 +4,7 @@ import { AntdSchemaComponentProvider, Application, BlockSchemaComponentProvider, - CollectionManagerProvider, + ExtendCollectionsProvider, SchemaComponent, SchemaComponentProvider, useFormBlockContext, @@ -211,13 +211,13 @@ const Root = () => { return ( - + - + ); diff --git a/packages/core/client/src/schema-component/antd/table/Table.Array.tsx b/packages/core/client/src/schema-component/antd/table/Table.Array.tsx index 6f8042895..2c3740719 100644 --- a/packages/core/client/src/schema-component/antd/table/Table.Array.tsx +++ b/packages/core/client/src/schema-component/antd/table/Table.Array.tsx @@ -15,7 +15,14 @@ import { default as classNames, default as cls } from 'classnames'; import React, { useContext, useState } from 'react'; import ReactDragListView from 'react-drag-listview'; import { DndContext } from '../..'; -import { RecordIndexProvider, RecordProvider, useRequest, useSchemaInitializerRender } from '../../../'; +import { + RecordIndexProvider, + RecordProvider, + useCollectionParentRecordData, + useCollectionRecordData, + useRequest, + useSchemaInitializerRender, +} from '../../../'; import { useToken } from '../__builtins__'; const isColumnComponent = (schema: Schema) => { @@ -27,6 +34,8 @@ const useTableColumns = () => { const schema = useFieldSchema(); const { exists, render } = useSchemaInitializerRender(schema['x-initializer'], schema['x-initializer-props']); const scope = useContext(SchemaExpressionScopeContext); + const parentRecordData = useCollectionParentRecordData(); + const recordData = useCollectionRecordData(); const columns = schema .reduceProperties((buf, s) => { @@ -42,10 +51,11 @@ const useTableColumns = () => { key: s.name, render: (v, record) => { const index = field.value?.indexOf(record); - // console.log((Date.now() - start) / 1000); return ( - + {/* fix https://nocobase.height.app/T-3232/description */} + {/* 如果作为关系表格区块,则 parentRecordData 应该有值;如果作为普通表格使用(如数据源管理页面的表格)则应该使用 recordData,且 parentRecordData 为空 */} + diff --git a/packages/core/client/src/schema-component/antd/table/Table.Column.Decorator.tsx b/packages/core/client/src/schema-component/antd/table/Table.Column.Decorator.tsx index 7a413ef96..5e89f46de 100644 --- a/packages/core/client/src/schema-component/antd/table/Table.Column.Decorator.tsx +++ b/packages/core/client/src/schema-component/antd/table/Table.Column.Decorator.tsx @@ -1,10 +1,10 @@ import { useField, useFieldSchema } from '@formily/react'; import React, { useLayoutEffect } from 'react'; -import { SortableItem, useCollection, useCompile, useDesignable, useDesigner } from '../../../'; +import { SortableItem, useCollection_deprecated, useCompile, useDesignable, useDesigner } from '../../../'; import { designerCss } from './Table.Column.ActionBar'; export const useColumnSchema = () => { - const { getField } = useCollection(); + const { getField } = useCollection_deprecated(); const compile = useCompile(); const columnSchema = useFieldSchema(); const fieldSchema = columnSchema.reduceProperties((buf, s) => { diff --git a/packages/core/client/src/schema-component/antd/table/Table.Column.Designer.tsx b/packages/core/client/src/schema-component/antd/table/Table.Column.Designer.tsx index f8b141a18..b5ba5908d 100644 --- a/packages/core/client/src/schema-component/antd/table/Table.Column.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/table/Table.Column.Designer.tsx @@ -1,6 +1,6 @@ import { ISchema, useField, useFieldSchema, useForm } from '@formily/react'; import React from 'react'; -import { useCollectionManager } from '../../../collection-manager'; +import { useCollectionManager_deprecated } from '../../../collection-manager'; import { GeneralSchemaDesigner, SchemaSettingsDivider, @@ -13,7 +13,7 @@ import { useActionContext } from '../action'; const useLabelFields = (collectionName?: any) => { const compile = useCompile(); - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(); if (!collectionName) { return []; } diff --git a/packages/core/client/src/schema-component/antd/table/Table.Designer.tsx b/packages/core/client/src/schema-component/antd/table/Table.Designer.tsx index d7e4ab5cb..0fadf360b 100644 --- a/packages/core/client/src/schema-component/antd/table/Table.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/table/Table.Designer.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { useCollection } from '../../../collection-manager'; +import { useCollection_deprecated } from '../../../collection-manager'; import { GeneralSchemaDesigner, SchemaSettingsRemove } from '../../../schema-settings'; export const TableDesigner = () => { - const { name, title } = useCollection(); + const { name, title } = useCollection_deprecated(); return ( { - const { name, title } = useCollection(); + const { name, title } = useCollection_deprecated(); const field = useField(); const fieldSchema = useFieldSchema(); const dataSource = useCollectionFilterOptions(name); diff --git a/packages/core/client/src/schema-component/hooks/useDesignable.tsx b/packages/core/client/src/schema-component/hooks/useDesignable.tsx index e55b7f5f6..d3918f246 100644 --- a/packages/core/client/src/schema-component/hooks/useDesignable.tsx +++ b/packages/core/client/src/schema-component/hooks/useDesignable.tsx @@ -5,7 +5,7 @@ import { message } from 'antd'; import cloneDeep from 'lodash/cloneDeep'; import get from 'lodash/get'; import set from 'lodash/set'; -import React, { ComponentType, useContext, useMemo } from 'react'; +import React, { ComponentType, useCallback, useContext, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { APIClient, useAPIClient } from '../../api-client'; import { SchemaComponentContext } from '../context'; @@ -704,15 +704,24 @@ export function useDesignable() { const { designable, setDesignable, refresh, reset } = useContext(SchemaComponentContext); const schemaOptions = useContext(SchemaOptionsContext); const components = useMemo(() => schemaOptions?.components || {}, [schemaOptions]); - const DesignableBar = () => { - return <>; - }; + const DesignableBar = useMemo( + () => () => { + return <>; + }, + [], + ); const field = useField(); const fieldSchema = useFieldSchema(); const api = useAPIClient(); const { t } = useTranslation(); - const dn = createDesignable({ t, api, refresh, current: fieldSchema, model: field }); - dn.loadAPIClientEvents(); + const dn = useMemo(() => { + return createDesignable({ t, api, refresh, current: fieldSchema, model: field }); + }, [t, api, refresh, fieldSchema, field]); + + useEffect(() => { + dn.loadAPIClientEvents(); + }, [dn]); + return { dn, designable, @@ -720,82 +729,112 @@ export function useDesignable() { refresh, setDesignable, DesignableBar, - findComponent(component: any) { - if (!component) { - return null; - } - if (typeof component !== 'string') { - return component; - } - return get(components, component); - }, + findComponent: useCallback( + (component: any) => { + if (!component) { + return null; + } + if (typeof component !== 'string') { + return component; + } + return get(components, component); + }, + [get], + ), on: dn.on.bind(dn), // TODO - patch: (key: ISchema | string, value?: any) => { - const update = (obj: any) => { - Object.keys(obj).forEach((k) => { - const val = obj[k]; - if (k === 'title') { - field.title = val; - fieldSchema['title'] = val; - } - if (k === 'x-decorator-props') { - if (!field.decoratorProps) { - field.decoratorProps = {}; + patch: useCallback( + (key: ISchema | string, value?: any) => { + const update = (obj: any) => { + Object.keys(obj).forEach((k) => { + const val = obj[k]; + if (k === 'title') { + field.title = val; + fieldSchema['title'] = val; } - if (!fieldSchema['x-decorator-props']) { - fieldSchema['x-decorator-props'] = {}; + if (k === 'x-decorator-props') { + if (!field.decoratorProps) { + field.decoratorProps = {}; + } + if (!fieldSchema['x-decorator-props']) { + fieldSchema['x-decorator-props'] = {}; + } + Object.keys(val).forEach((i) => { + field.decoratorProps[i] = val[i]; + fieldSchema['x-decorator-props'][i] = val[i]; + }); } - Object.keys(val).forEach((i) => { - field.decoratorProps[i] = val[i]; - fieldSchema['x-decorator-props'][i] = val[i]; - }); - } - if (k === 'x-component-props') { - if (!field.componentProps) { - field.componentProps = {}; + if (k === 'x-component-props') { + if (!field.componentProps) { + field.componentProps = {}; + } + if (!fieldSchema['x-component-props']) { + fieldSchema['x-component-props'] = {}; + } + Object.keys(val).forEach((i) => { + field.componentProps[i] = val[i]; + fieldSchema['x-component-props'][i] = val[i]; + }); } - if (!fieldSchema['x-component-props']) { - fieldSchema['x-component-props'] = {}; - } - Object.keys(val).forEach((i) => { - field.componentProps[i] = val[i]; - fieldSchema['x-component-props'][i] = val[i]; - }); - } - }); - }; - if (typeof key === 'string') { - const obj = {}; - set(obj, key, value); - return update(obj); - } - update(key); - refresh(); - }, - shallowMerge(schema: ISchema) { - dn.shallowMerge(schema); - }, - deepMerge(schema: ISchema) { - dn.deepMerge(schema); - }, - remove(schema?: any, options?: RemoveOptions) { - dn.remove(schema, options); - }, - insertAdjacent(position: Position, schema: ISchema, options?: InsertAdjacentOptions) { - dn.insertAdjacent(position, schema, options); - }, - insertBeforeBegin(schema: ISchema) { - dn.insertBeforeBegin(schema); - }, - insertAfterBegin(schema: ISchema) { - dn.insertAfterBegin(schema); - }, - insertBeforeEnd(schema: ISchema) { - dn.insertBeforeEnd(schema); - }, - insertAfterEnd(schema: ISchema) { - dn.insertAfterEnd(schema); - }, + }); + }; + if (typeof key === 'string') { + const obj = {}; + set(obj, key, value); + return update(obj); + } + update(key); + refresh(); + }, + [dn], + ), + shallowMerge: useCallback( + (schema: ISchema) => { + dn.shallowMerge(schema); + }, + [dn], + ), + deepMerge: useCallback( + (schema: ISchema) => { + dn.deepMerge(schema); + }, + [dn], + ), + remove: useCallback( + (schema?: any, options?: RemoveOptions) => { + dn.remove(schema, options); + }, + [dn], + ), + insertAdjacent: useCallback( + (position: Position, schema: ISchema, options?: InsertAdjacentOptions) => { + dn.insertAdjacent(position, schema, options); + }, + [dn], + ), + insertBeforeBegin: useCallback( + (schema: ISchema) => { + dn.insertBeforeBegin(schema); + }, + [dn], + ), + insertAfterBegin: useCallback( + (schema: ISchema) => { + dn.insertAfterBegin(schema); + }, + [dn], + ), + insertBeforeEnd: useCallback( + (schema: ISchema) => { + dn.insertBeforeEnd(schema); + }, + [dn], + ), + insertAfterEnd: useCallback( + (schema: ISchema) => { + dn.insertAfterEnd(schema); + }, + [dn], + ), }; } diff --git a/packages/core/client/src/schema-component/hooks/useFieldComponentOptions.ts b/packages/core/client/src/schema-component/hooks/useFieldComponentOptions.ts index a8c85a042..2eb328448 100644 --- a/packages/core/client/src/schema-component/hooks/useFieldComponentOptions.ts +++ b/packages/core/client/src/schema-component/hooks/useFieldComponentOptions.ts @@ -1,12 +1,12 @@ import { useFieldSchema } from '@formily/react'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useCollection, useCollectionManager } from '../../collection-manager'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../../collection-manager'; export const useFieldComponentOptions = () => { - const { getCollectionJoinField, getCollection } = useCollectionManager(); + const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated(); const fieldSchema = useFieldSchema(); - const { getField } = useCollection(); + const { getField } = useCollection_deprecated(); const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); const { t } = useTranslation(); const { label } = fieldSchema['x-component-props']?.fieldNames || {}; diff --git a/packages/core/client/src/schema-component/hooks/useFieldModeOptions.tsx b/packages/core/client/src/schema-component/hooks/useFieldModeOptions.tsx index f1410ee2d..5a79c4977 100644 --- a/packages/core/client/src/schema-component/hooks/useFieldModeOptions.tsx +++ b/packages/core/client/src/schema-component/hooks/useFieldModeOptions.tsx @@ -1,25 +1,33 @@ import { useField, useFieldSchema, useForm } from '@formily/react'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useCollection, useCollectionManager } from '../../collection-manager'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../../collection-manager'; export const useFieldModeOptions = (props?) => { - const { getCollectionJoinField, getCollection } = useCollectionManager(); + const { t } = useTranslation(); + const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated(); const currentFieldSchema = useFieldSchema(); const fieldSchema = props?.fieldSchema || currentFieldSchema; const field = useField(); const form = useForm(); - const isReadPretty = field.readPretty || form.readPretty; - const isSubTableField = props?.fieldSchema; - const { getField } = useCollection(); - const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); - const { t } = useTranslation(); + const isReadPretty = fieldSchema?.['x-read-pretty'] || field.readPretty || form.readPretty; + const isTableField = props?.fieldSchema; + const { getField } = useCollection_deprecated(); + const collectionField = + props?.collectionField || + getField(fieldSchema['name']) || + getCollectionJoinField(fieldSchema['x-collection-field']); const { label } = fieldSchema['x-component-props']?.fieldNames || {}; const fieldModeOptions = useMemo(() => { if (!collectionField || !collectionField?.interface) { return; } - if (!['o2o', 'oho', 'obo', 'o2m', 'linkTo', 'm2o', 'm2m'].includes(collectionField.interface)) return; + if ( + !['o2o', 'oho', 'obo', 'o2m', 'linkTo', 'm2o', 'm2m', 'updatedBy', 'createdBy'].includes( + collectionField.interface, + ) + ) + return; const collection = getCollection(collectionField.target); if (collection?.template === 'file') { return isReadPretty @@ -31,7 +39,7 @@ export const useFieldModeOptions = (props?) => { : [ { label: t('Select'), value: 'Select' }, { label: t('Record picker'), value: 'Picker' }, - !isSubTableField && { label: t('File manager'), value: 'FileManager' }, + { label: t('File manager'), value: 'FileManager' }, ]; } if (collection?.template === 'tree' && ['m2m', 'o2m', 'm2o'].includes(collectionField.interface)) { @@ -45,7 +53,7 @@ export const useFieldModeOptions = (props?) => { { label: t('Record picker'), value: 'Picker' }, ['m2m', 'o2m'].includes(collectionField.interface) && { label: t('Sub-table'), value: 'SubTable' }, { label: t('Cascade Select'), value: 'CascadeSelect' }, - !isSubTableField && { label: t('Sub-form'), value: 'Nester' }, + !isTableField && { label: t('Sub-form'), value: 'Nester' }, { label: t('Sub-form(Popover)'), value: 'PopoverNester' }, ]; } @@ -55,29 +63,29 @@ export const useFieldModeOptions = (props?) => { ? [ { label: t('Title'), value: 'Select' }, { label: t('Tag'), value: 'Tag' }, - { label: t('Sub-table'), value: 'SubTable' }, - { label: t('Sub-details'), value: 'Nester' }, + !isTableField && { label: t('Sub-table'), value: 'SubTable' }, + !isTableField && { label: t('Sub-details'), value: 'Nester' }, ] : [ { label: t('Select'), value: 'Select' }, { label: t('Record picker'), value: 'Picker' }, - !isSubTableField && { label: t('Sub-form'), value: 'Nester' }, + !isTableField && { label: t('Sub-form'), value: 'Nester' }, { label: t('Sub-form(Popover)'), value: 'PopoverNester' }, - !isSubTableField && { label: t('Sub-table'), value: 'SubTable' }, + !isTableField && { label: t('Sub-table'), value: 'SubTable' }, ]; case 'm2m': return isReadPretty ? [ { label: t('Title'), value: 'Select' }, { label: t('Tag'), value: 'Tag' }, - { label: t('Sub-details'), value: 'Nester' }, - { label: t('Sub-table'), value: 'SubTable' }, + !isTableField && { label: t('Sub-details'), value: 'Nester' }, + !isTableField && { label: t('Sub-table'), value: 'SubTable' }, ] : [ { label: t('Select'), value: 'Select' }, { label: t('Record picker'), value: 'Picker' }, - !isSubTableField && { label: t('Sub-table'), value: 'SubTable' }, - !isSubTableField && { label: t('Sub-form'), value: 'Nester' }, + !isTableField && { label: t('Sub-table'), value: 'SubTable' }, + !isTableField && { label: t('Sub-form'), value: 'Nester' }, { label: t('Sub-form(Popover)'), value: 'PopoverNester' }, ]; case 'm2o': @@ -86,12 +94,12 @@ export const useFieldModeOptions = (props?) => { ? [ { label: t('Title'), value: 'Select' }, { label: t('Tag'), value: 'Tag' }, - { label: t('Sub-details'), value: 'Nester' }, + !isTableField && { label: t('Sub-details'), value: 'Nester' }, ] : [ { label: t('Select'), value: 'Select' }, { label: t('Record picker'), value: 'Picker' }, - !isSubTableField && { label: t('Sub-form'), value: 'Nester' }, + !isTableField && { label: t('Sub-form'), value: 'Nester' }, { label: t('Sub-form(Popover)'), value: 'PopoverNester' }, ]; @@ -100,12 +108,12 @@ export const useFieldModeOptions = (props?) => { ? [ { label: t('Title'), value: 'Select' }, { label: t('Tag'), value: 'Tag' }, - { label: t('Sub-details'), value: 'Nester' }, + !isTableField && { label: t('Sub-details'), value: 'Nester' }, ] : [ { label: t('Select'), value: 'Select' }, { label: t('Record picker'), value: 'Picker' }, - !isSubTableField && { label: t('Sub-form'), value: 'Nester' }, + !isTableField && { label: t('Sub-form'), value: 'Nester' }, { label: t('Sub-form(Popover)'), value: 'PopoverNester' }, ]; } diff --git a/packages/core/client/src/schema-component/hooks/useFieldTitle.ts b/packages/core/client/src/schema-component/hooks/useFieldTitle.ts index ad2629f1d..e18bcdd10 100644 --- a/packages/core/client/src/schema-component/hooks/useFieldTitle.ts +++ b/packages/core/client/src/schema-component/hooks/useFieldTitle.ts @@ -2,13 +2,13 @@ import { Field } from '@formily/core'; import { useField, useFieldSchema } from '@formily/react'; import { useEffect } from 'react'; import { useCompile } from './useCompile'; -import { useCollection, useCollectionManager } from '../../collection-manager'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../../collection-manager'; export const useFieldTitle = () => { const field = useField(); const fieldSchema = useFieldSchema(); - const { getField } = useCollection(); - const { getCollectionJoinField } = useCollectionManager(); + const { getField } = useCollection_deprecated(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); const compile = useCompile(); useEffect(() => { diff --git a/packages/core/client/src/schema-component/index.tsx b/packages/core/client/src/schema-component/index.tsx index d9785b81c..00b957220 100644 --- a/packages/core/client/src/schema-component/index.tsx +++ b/packages/core/client/src/schema-component/index.tsx @@ -1,5 +1,4 @@ export * from './antd'; -export * from './antd/AntdSchemaComponentProvider'; export * from './common'; export * from './context'; export * from './core'; diff --git a/packages/core/client/src/schema-initializer/buttons/FormActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/FormActionInitializers.tsx deleted file mode 100644 index f0eaca578..000000000 --- a/packages/core/client/src/schema-initializer/buttons/FormActionInitializers.tsx +++ /dev/null @@ -1,284 +0,0 @@ -import { SchemaInitializerItemType } from '../../application'; -import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; - -// TODO(refactor): should be moved to workflow plugin -const formTriggerWorkflowActionInitializerV2: SchemaInitializerItemType = { - name: 'submitToWorkflow', - title: '{{t("Submit to workflow", { ns: "workflow" })}}', - Component: 'CustomizeActionInitializer', - schema: { - title: '{{t("Submit to workflow", { ns: "workflow" })}}', - 'x-component': 'Action', - 'x-component-props': { - useProps: '{{ useTriggerWorkflowsActionProps }}', - }, - 'x-designer': 'Action.Designer', - 'x-action-settings': { - assignedValues: {}, - skipValidator: false, - onSuccess: { - manualClose: true, - redirecting: false, - successMessage: '{{t("Submitted successfully")}}', - }, - triggerWorkflows: [], - }, - 'x-action': 'customize:triggerWorkflows', - }, -}; - -// 表单的操作配置 -export const formActionInitializers = new SchemaInitializer({ - name: 'FormActionInitializers', - title: '{{t("Configure actions")}}', - icon: 'SettingOutlined', - items: [ - { - type: 'itemGroup', - name: 'enableActions', - title: '{{t("Enable actions")}}', - children: [ - { - name: 'submit', - title: '{{t("Submit")}}', - Component: 'CreateSubmitActionInitializer', - schema: { - 'x-action-settings': {}, - }, - }, - ], - }, - { - name: 'divider', - type: 'divider', - }, - { - name: 'custom', - type: 'subMenu', - title: '{{t("Customize")}}', - children: [ - { - name: 'saveRecord', - title: '{{t("Save record")}}', - Component: 'CustomizeActionInitializer', - schema: { - title: '{{ t("Save record") }}', - 'x-action': 'customize:save', - 'x-component': 'Action', - 'x-designer': 'Action.Designer', - 'x-designer-props': { - modalTip: - '{{ t("When the button is clicked, the following fields will be assigned and saved together with the fields in the form. If there are overlapping fields, the value here will overwrite the value in the form.") }}', - }, - 'x-action-settings': { - assignedValues: {}, - skipValidator: false, - onSuccess: { - manualClose: true, - redirecting: false, - successMessage: '{{t("Submitted successfully")}}', - }, - triggerWorkflows: [], - }, - 'x-component-props': { - useProps: '{{ useCreateActionProps }}', - }, - }, - }, - formTriggerWorkflowActionInitializerV2, - { - name: 'customRequest', - title: '{{t("Custom request")}}', - Component: 'CustomRequestInitializer', - }, - ], - }, - ], -}); - -export const createFormActionInitializers = new SchemaInitializer({ - name: 'CreateFormActionInitializers', - title: '{{t("Configure actions")}}', - icon: 'SettingOutlined', - items: [ - { - type: 'itemGroup', - title: '{{t("Enable actions")}}', - name: 'enableActions', - children: [ - { - name: 'submit', - title: '{{t("Submit")}}', - Component: 'CreateSubmitActionInitializer', - schema: { - 'x-action-settings': {}, - }, - }, - ], - }, - { - name: 'divider', - type: 'divider', - }, - { - type: 'subMenu', - title: '{{t("Customize")}}', - name: 'customize', - children: [ - { - name: 'saveRecord', - title: '{{t("Save record")}}', - Component: 'CustomizeActionInitializer', - schema: { - title: '{{ t("Save record") }}', - 'x-action': 'customize:save', - 'x-component': 'Action', - 'x-designer': 'Action.Designer', - 'x-designer-props': { - modalTip: - '{{ t("When the button is clicked, the following fields will be assigned and saved together with the fields in the form. If there are overlapping fields, the value here will overwrite the value in the form.") }}', - }, - 'x-action-settings': { - assignedValues: {}, - skipValidator: false, - onSuccess: { - manualClose: true, - redirecting: false, - successMessage: '{{t("Submitted successfully")}}', - }, - triggerWorkflows: [], - }, - 'x-component-props': { - useProps: '{{ useCreateActionProps }}', - }, - }, - }, - formTriggerWorkflowActionInitializerV2, - { - name: 'customRequest', - title: '{{t("Custom request")}}', - Component: 'CustomRequestInitializer', - }, - ], - }, - ], -}); - -export const updateFormActionInitializers = new SchemaInitializer({ - name: 'UpdateFormActionInitializers', - title: '{{t("Configure actions")}}', - icon: 'SettingOutlined', - items: [ - { - type: 'itemGroup', - title: '{{t("Enable actions")}}', - name: 'enableActions', - children: [ - { - name: 'submit', - title: '{{t("Submit")}}', - Component: 'UpdateSubmitActionInitializer', - schema: { - 'x-action-settings': {}, - }, - }, - ], - }, - { - name: 'divider', - type: 'divider', - }, - { - type: 'subMenu', - title: '{{t("Customize")}}', - name: 'customize', - children: [ - { - name: 'popup', - title: '{{t("Popup")}}', - Component: 'CustomizeActionInitializer', - schema: { - type: 'void', - title: '{{ t("Popup") }}', - 'x-action': 'customize:popup', - 'x-designer': 'Action.Designer', - 'x-component': 'Action', - 'x-component-props': { - openMode: 'drawer', - }, - properties: { - drawer: { - type: 'void', - title: '{{ t("Popup") }}', - 'x-component': 'Action.Container', - 'x-component-props': { - className: 'nb-action-popup', - }, - properties: { - tabs: { - type: 'void', - 'x-component': 'Tabs', - 'x-component-props': {}, - 'x-initializer': 'TabPaneInitializers', - properties: { - tab1: { - type: 'void', - title: '{{t("Details")}}', - 'x-component': 'Tabs.TabPane', - 'x-designer': 'Tabs.Designer', - 'x-component-props': {}, - properties: { - grid: { - type: 'void', - 'x-component': 'Grid', - 'x-initializer': 'RecordBlockInitializers', - properties: {}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: 'saveRecord', - title: '{{t("Save record")}}', - Component: 'CustomizeActionInitializer', - schema: { - title: '{{ t("Save") }}', - 'x-component': 'Action', - 'x-action': 'customize:save', - 'x-designer': 'Action.Designer', - 'x-designer-props': { - modalTip: - '{{ t("When the button is clicked, the following fields will be assigned and saved together with the fields in the form. If there are overlapping fields, the value here will overwrite the value in the form.") }}', - }, - 'x-action-settings': { - assignedValues: {}, - skipValidator: false, - onSuccess: { - manualClose: true, - redirecting: false, - successMessage: '{{t("Submitted successfully")}}', - }, - triggerWorkflows: [], - }, - 'x-component-props': { - useProps: '{{ useUpdateActionProps }}', - }, - }, - }, - formTriggerWorkflowActionInitializerV2, - { - type: 'item', - name: 'customRequest', - title: '{{t("Custom request")}}', - Component: 'CustomRequestInitializer', - }, - ], - }, - ], -}); diff --git a/packages/core/client/src/schema-initializer/buttons/FormItemInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/FormItemInitializers.tsx index 089f01972..93183b9ca 100644 --- a/packages/core/client/src/schema-initializer/buttons/FormItemInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/FormItemInitializers.tsx @@ -1,19 +1,15 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { SchemaInitializerChildren } from '../../application'; -import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; import { useCompile } from '../../schema-component'; import { - gridRowColWrap, useAssociatedFormItemInitializerFields, useFilterAssociatedFormItemInitializerFields, - useFilterFormItemInitializerFields, useFilterInheritsFormItemInitializerFields, - useFormItemInitializerFields, useInheritsFormItemInitializerFields, } from '../utils'; -const ParentCollectionFields = () => { +export const ParentCollectionFields = () => { const inheritFields = useInheritsFormItemInitializerFields(); const { t } = useTranslation(); const compile = useCompile(); @@ -31,7 +27,7 @@ const ParentCollectionFields = () => { return {res}; }; -const AssociatedFields = () => { +export const AssociatedFields = () => { const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form', @@ -48,49 +44,6 @@ const AssociatedFields = () => { return {schema}; }; -// 表单里配置字段 -export const formItemInitializers = new SchemaInitializer({ - name: 'FormItemInitializers', - wrap: gridRowColWrap, - icon: 'SettingOutlined', - title: '{{t("Configure fields")}}', - items: [ - { - type: 'itemGroup', - name: 'displayFields', - title: '{{t("Display fields")}}', - useChildren: useFormItemInitializerFields, - }, - { - name: 'parentCollectionFields', - Component: ParentCollectionFields, - }, - { - name: 'associationFields', - Component: AssociatedFields, - }, - { - name: 'divider', - type: 'divider', - }, - { - name: 'addText', - title: '{{t("Add text")}}', - Component: 'BlockItemInitializer', - schema: { - type: 'void', - 'x-editable': false, - 'x-decorator': 'FormItem', - 'x-designer': 'Markdown.Void.Designer', - 'x-component': 'Markdown.Void', - 'x-component-props': { - content: '{{t("This is a demo text, **supports Markdown syntax**.")}}', - }, - }, - }, - ], -}); - export const FilterParentCollectionFields = () => { const inheritFields = useFilterInheritsFormItemInitializerFields(); const { t } = useTranslation(); @@ -123,45 +76,3 @@ export const FilterAssociatedFields = () => { ]; return {res}; }; - -export const filterFormItemInitializers = new SchemaInitializer({ - name: 'FilterFormItemInitializers', - wrap: gridRowColWrap, - icon: 'SettingOutlined', - title: '{{t("Configure fields")}}', - items: [ - { - type: 'itemGroup', - name: 'displayFields', - title: '{{t("Display fields")}}', - useChildren: useFilterFormItemInitializerFields, - }, - { - name: 'parentCollectionFields', - Component: FilterParentCollectionFields, - }, - { - name: 'associationFields', - Component: FilterAssociatedFields, - }, - { - name: 'divider', - type: 'divider', - }, - { - title: '{{t("Add text")}}', - Component: 'BlockItemInitializer', - name: 'addText', - schema: { - type: 'void', - 'x-editable': false, - 'x-decorator': 'FormItem', - 'x-designer': 'Markdown.Void.Designer', - 'x-component': 'Markdown.Void', - 'x-component-props': { - content: '{{t("This is a demo text, **supports Markdown syntax**.")}}', - }, - }, - }, - ], -}); diff --git a/packages/core/client/src/schema-initializer/buttons/GridCardActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/GridCardActionInitializers.tsx deleted file mode 100644 index 328b68022..000000000 --- a/packages/core/client/src/schema-initializer/buttons/GridCardActionInitializers.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; -import { useCollection } from '../../collection-manager'; - -// 表单的操作配置 -export const gridCardActionInitializers = new SchemaInitializer({ - name: 'GridCardActionInitializers', - title: "{{t('Configure actions')}}", - icon: 'SettingOutlined', - style: { - marginLeft: 8, - }, - items: [ - { - type: 'itemGroup', - title: "{{t('Enable actions')}}", - name: 'enableActions', - children: [ - { - name: 'filter', - title: "{{t('Filter')}}", - Component: 'FilterActionInitializer', - schema: { - 'x-align': 'left', - }, - }, - { - name: 'addNew', - title: "{{t('Add new')}}", - Component: 'CreateActionInitializer', - schema: { - 'x-align': 'right', - 'x-decorator': 'ACLActionProvider', - 'x-acl-action-props': { - skipScopeCheck: true, - }, - }, - useVisible() { - const collection = useCollection(); - return !['view', 'file', 'sql'].includes(collection.template) || collection?.writableView; - }, - }, - { - name: 'refresh', - title: "{{t('Refresh')}}", - Component: 'RefreshActionInitializer', - schema: { - 'x-align': 'right', - }, - }, - { - name: 'import', - title: "{{t('Import')}}", - Component: 'ImportActionInitializer', - schema: { - 'x-align': 'right', - 'x-acl-action': 'importXlsx', - 'x-decorator': 'ACLActionProvider', - 'x-acl-action-props': { - skipScopeCheck: true, - }, - }, - useVisible() { - const collection = useCollection(); - return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; - }, - }, - { - name: 'export', - title: "{{t('Export')}}", - Component: 'ExportActionInitializer', - schema: { - 'x-align': 'right', - 'x-decorator': 'ACLActionProvider', - 'x-acl-action-props': { - skipScopeCheck: true, - }, - }, - }, - ], - }, - ], -}); - -export const gridCardItemActionInitializers = new SchemaInitializer({ - name: 'GridCardItemActionInitializers', - title: '{{t("Configure actions")}}', - icon: 'SettingOutlined', - items: [ - { - type: 'itemGroup', - title: '{{t("Enable actions")}}', - name: 'enable-actions', - children: [ - { - name: 'view', - title: '{{t("View")}}', - Component: 'ViewActionInitializer', - schema: { - 'x-component': 'Action.Link', - 'x-action': 'view', - 'x-decorator': 'ACLActionProvider', - 'x-align': 'left', - }, - }, - { - name: 'edit', - title: '{{t("Edit")}}', - Component: 'UpdateActionInitializer', - schema: { - 'x-component': 'Action.Link', - 'x-action': 'update', - 'x-decorator': 'ACLActionProvider', - 'x-align': 'left', - }, - useVisible() { - const collection = useCollection(); - return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; - }, - }, - { - name: 'delete', - title: '{{t("Delete")}}', - Component: 'DestroyActionInitializer', - schema: { - 'x-component': 'Action.Link', - 'x-action': 'destroy', - 'x-decorator': 'ACLActionProvider', - 'x-align': 'left', - }, - useVisible() { - const collection = useCollection(); - return collection.template !== 'sql'; - }, - }, - ], - }, - { - name: 'divider', - type: 'divider', - }, - { - type: 'subMenu', - title: '{{t("Customize")}}', - name: 'customize', - children: [ - { - name: 'popup', - title: '{{t("Popup")}}', - Component: 'CustomizeActionInitializer', - schema: { - type: 'void', - title: '{{ t("Popup") }}', - 'x-action': 'customize:popup', - 'x-designer': 'Action.Designer', - 'x-component': 'Action.Link', - 'x-component-props': { - openMode: 'drawer', - }, - properties: { - drawer: { - type: 'void', - title: '{{ t("Popup") }}', - 'x-component': 'Action.Container', - 'x-component-props': { - className: 'nb-action-popup', - }, - properties: { - tabs: { - type: 'void', - 'x-component': 'Tabs', - 'x-component-props': {}, - 'x-initializer': 'TabPaneInitializers', - properties: { - tab1: { - type: 'void', - title: '{{t("Details")}}', - 'x-component': 'Tabs.TabPane', - 'x-designer': 'Tabs.Designer', - 'x-component-props': {}, - properties: { - grid: { - type: 'void', - 'x-component': 'Grid', - 'x-initializer': 'RecordBlockInitializers', - properties: {}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: 'update-record', - title: '{{t("Update record")}}', - Component: 'CustomizeActionInitializer', - schema: { - title: '{{ t("Update record") }}', - 'x-component': 'Action.Link', - 'x-action': 'customize:update', - 'x-decorator': 'ACLActionProvider', - 'x-acl-action': 'update', - 'x-designer': 'Action.Designer', - 'x-action-settings': { - assignedValues: {}, - onSuccess: { - manualClose: true, - redirecting: false, - successMessage: '{{t("Updated successfully")}}', - }, - }, - 'x-component-props': { - useProps: '{{ useCustomizeUpdateActionProps }}', - }, - }, - useVisible() { - const collection = useCollection(); - return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; - }, - }, - { - name: 'customRequest', - title: '{{t("Custom request")}}', - Component: 'CustomRequestInitializer', - schema: { - 'x-action': 'customize:table:request', - }, - useVisible() { - const collection = useCollection(); - return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; - }, - }, - ], - }, - ], -}); diff --git a/packages/core/client/src/schema-initializer/buttons/ListActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/ListActionInitializers.tsx deleted file mode 100644 index 70219da82..000000000 --- a/packages/core/client/src/schema-initializer/buttons/ListActionInitializers.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; -import { useCollection } from '../../collection-manager'; - -// 表单的操作配置 -export const listActionInitializers = new SchemaInitializer({ - name: 'ListActionInitializers', - title: "{{t('Configure actions')}}", - icon: 'SettingOutlined', - style: { - marginLeft: 8, - }, - items: [ - { - type: 'itemGroup', - name: 'enableActions', - title: "{{t('Enable actions')}}", - children: [ - { - name: 'filter', - title: "{{t('Filter')}}", - Component: 'FilterActionInitializer', - schema: { - 'x-align': 'left', - }, - }, - { - name: 'addNew', - title: "{{t('Add new')}}", - Component: 'CreateActionInitializer', - schema: { - 'x-align': 'right', - 'x-decorator': 'ACLActionProvider', - 'x-acl-action-props': { - skipScopeCheck: true, - }, - }, - useVisible() { - const collection = useCollection(); - return ( - (collection.template !== 'view' || collection?.writableView) && - collection.template !== 'file' && - collection.template !== 'sql' - ); - }, - }, - { - name: 'refresh', - title: "{{t('Refresh')}}", - Component: 'RefreshActionInitializer', - schema: { - 'x-align': 'right', - }, - }, - { - name: 'import', - title: "{{t('Import')}}", - Component: 'ImportActionInitializer', - schema: { - 'x-align': 'right', - 'x-acl-action': 'importXlsx', - 'x-decorator': 'ACLActionProvider', - 'x-acl-action-props': { - skipScopeCheck: true, - }, - }, - useVisible() { - const collection = useCollection(); - return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; - }, - }, - { - name: 'export', - title: "{{t('Export')}}", - Component: 'ExportActionInitializer', - schema: { - 'x-align': 'right', - 'x-decorator': 'ACLActionProvider', - 'x-acl-action-props': { - skipScopeCheck: true, - }, - }, - }, - ], - }, - ], -}); - -export const listItemActionInitializers = new SchemaInitializer({ - name: 'ListItemActionInitializers', - title: '{{t("Configure actions")}}', - icon: 'SettingOutlined', - items: [ - { - type: 'itemGroup', - name: 'enableActions', - title: '{{t("Enable actions")}}', - children: [ - { - name: 'view', - title: '{{t("View")}}', - Component: 'ViewActionInitializer', - schema: { - 'x-component': 'Action.Link', - 'x-action': 'view', - 'x-decorator': 'ACLActionProvider', - 'x-align': 'left', - }, - }, - { - name: 'edit', - title: '{{t("Edit")}}', - Component: 'UpdateActionInitializer', - schema: { - 'x-component': 'Action.Link', - 'x-action': 'update', - 'x-decorator': 'ACLActionProvider', - 'x-align': 'left', - }, - useVisible() { - const collection = useCollection(); - return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; - }, - }, - { - name: 'delete', - title: '{{t("Delete")}}', - Component: 'DestroyActionInitializer', - schema: { - 'x-component': 'Action.Link', - 'x-action': 'destroy', - 'x-decorator': 'ACLActionProvider', - 'x-align': 'left', - }, - useVisible() { - const collection = useCollection(); - return collection.template !== 'sql'; - }, - }, - ], - }, - { - name: 'divider', - type: 'divider', - }, - { - type: 'subMenu', - title: '{{t("Customize")}}', - name: 'customize', - children: [ - { - name: 'popup', - title: '{{t("Popup")}}', - Component: 'CustomizeActionInitializer', - schema: { - type: 'void', - title: '{{ t("Popup") }}', - 'x-action': 'customize:popup', - 'x-designer': 'Action.Designer', - 'x-component': 'Action.Link', - 'x-component-props': { - openMode: 'drawer', - }, - properties: { - drawer: { - type: 'void', - title: '{{ t("Popup") }}', - 'x-component': 'Action.Container', - 'x-component-props': { - className: 'nb-action-popup', - }, - properties: { - tabs: { - type: 'void', - 'x-component': 'Tabs', - 'x-component-props': {}, - 'x-initializer': 'TabPaneInitializers', - properties: { - tab1: { - type: 'void', - title: '{{t("Details")}}', - 'x-component': 'Tabs.TabPane', - 'x-designer': 'Tabs.Designer', - 'x-component-props': {}, - properties: { - grid: { - type: 'void', - 'x-component': 'Grid', - 'x-initializer': 'RecordBlockInitializers', - properties: {}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: 'updateRecord', - title: '{{t("Update record")}}', - Component: 'CustomizeActionInitializer', - schema: { - title: '{{ t("Update record") }}', - 'x-component': 'Action.Link', - 'x-action': 'customize:update', - 'x-decorator': 'ACLActionProvider', - 'x-acl-action': 'update', - 'x-designer': 'Action.Designer', - 'x-action-settings': { - assignedValues: {}, - onSuccess: { - manualClose: true, - redirecting: false, - successMessage: '{{t("Updated successfully")}}', - }, - }, - 'x-component-props': { - useProps: '{{ useCustomizeUpdateActionProps }}', - }, - }, - useVisible() { - const collection = useCollection(); - return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; - }, - }, - { - name: 'customRequest', - title: '{{t("Custom request")}}', - Component: 'CustomRequestInitializer', - schema: { - 'x-action': 'customize:table:request', - }, - useVisible() { - const collection = useCollection(); - return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; - }, - }, - ], - }, - ], -}); diff --git a/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx deleted file mode 100644 index feb09b07e..000000000 --- a/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { useCollection } from '../..'; -import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; - -const useVisibleCollection = () => { - const collection = useCollection(); - return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; -}; -// 表单的操作配置 -export const readPrettyFormActionInitializers = new SchemaInitializer({ - name: 'ReadPrettyFormActionInitializers', - title: '{{t("Configure actions")}}', - icon: 'SettingOutlined', - style: { - marginLeft: 8, - }, - items: [ - { - type: 'itemGroup', - name: 'enableActions', - title: '{{t("Enable actions")}}', - children: [ - { - title: '{{t("Edit")}}', - name: 'edit', - Component: 'UpdateActionInitializer', - schema: { - 'x-component': 'Action', - 'x-decorator': 'ACLActionProvider', - 'x-component-props': { - type: 'primary', - }, - }, - useVisible: useVisibleCollection, - }, - { - title: '{{t("Delete")}}', - name: 'delete', - Component: 'DestroyActionInitializer', - schema: { - 'x-component': 'Action', - 'x-decorator': 'ACLActionProvider', - }, - useVisible: useVisibleCollection, - }, - ], - }, - { - name: 'divider', - type: 'divider', - }, - { - type: 'subMenu', - name: 'customize', - title: '{{t("Customize")}}', - children: [ - { - name: 'popup', - title: '{{t("Popup")}}', - Component: 'CustomizeActionInitializer', - schema: { - type: 'void', - title: '{{ t("Popup") }}', - 'x-action': 'customize:popup', - 'x-designer': 'Action.Designer', - 'x-component': 'Action', - 'x-component-props': { - openMode: 'drawer', - }, - properties: { - drawer: { - type: 'void', - title: '{{ t("Popup") }}', - 'x-component': 'Action.Container', - 'x-component-props': { - className: 'nb-action-popup', - }, - properties: { - tabs: { - type: 'void', - 'x-component': 'Tabs', - 'x-component-props': {}, - 'x-initializer': 'TabPaneInitializers', - properties: { - tab1: { - type: 'void', - title: '{{t("Details")}}', - 'x-component': 'Tabs.TabPane', - 'x-designer': 'Tabs.Designer', - 'x-component-props': {}, - properties: { - grid: { - type: 'void', - 'x-component': 'Grid', - 'x-initializer': 'RecordBlockInitializers', - properties: {}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: 'updateRecord', - title: '{{t("Update record")}}', - Component: 'CustomizeActionInitializer', - schema: { - title: '{{ t("Update record") }}', - 'x-component': 'Action', - 'x-designer': 'Action.Designer', - 'x-action': 'customize:update', - 'x-decorator': 'ACLActionProvider', - 'x-acl-action': 'update', - 'x-action-settings': { - assignedValues: {}, - onSuccess: { - manualClose: true, - redirecting: false, - successMessage: '{{t("Updated successfully")}}', - }, - }, - 'x-component-props': { - useProps: '{{ useCustomizeUpdateActionProps }}', - }, - }, - visible: useVisibleCollection, - }, - { - name: 'customRequest', - title: '{{t("Custom request")}}', - Component: 'CustomRequestInitializer', - useVisible: useVisibleCollection, - }, - ], - }, - ], -}); diff --git a/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx index a7b1637a4..9c66efa23 100644 --- a/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx @@ -1,6 +1,6 @@ import { Schema, useFieldSchema } from '@formily/react'; import { useMemo } from 'react'; -import { useCollection, useCollectionManager } from '../..'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../..'; import { SchemaInitializerItemType, useSchemaInitializer } from '../../application'; import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; import { gridRowColWrap } from '../utils'; @@ -17,8 +17,8 @@ const recursiveParent = (schema: Schema) => { const useRelationFields = () => { const fieldSchema = useFieldSchema(); - const { getCollectionFields } = useCollectionManager(); - const collection = useCollection(); + const { getCollectionFields } = useCollectionManager_deprecated(); + const collection = useCollection_deprecated(); let fields = []; if (fieldSchema['x-initializer']) { @@ -182,9 +182,9 @@ const useFormCollections = (props) => { function useRecordBlocks() { const { options } = useSchemaInitializer(); const { actionInitializers } = options; - const collection = useCollection(); - const { getChildrenCollections } = useCollectionManager(); - const formChildrenCollections = getChildrenCollections(collection.name); + const collection = useCollection_deprecated(); + const { getChildrenCollections } = useCollectionManager_deprecated(); + const formChildrenCollections = getChildrenCollections(collection.name, collection.dataSource); const hasFormChildCollection = formChildrenCollections?.length > 0; const detailChildrenCollections = getChildrenCollections(collection.name, true); const hasDetailChildCollection = detailChildrenCollections?.length > 0; @@ -252,22 +252,27 @@ export const recordBlockInitializers = new SchemaInitializer({ name: 'filterBlocks', title: '{{t("Filter blocks")}}', type: 'itemGroup', + useVisible() { + const collection = useCollection_deprecated(); + return collection.fields.some((field) => ['hasMany', 'belongsToMany'].includes(field.type)); + }, children: [ { name: 'filterForm', title: '{{t("Form")}}', Component: 'FilterFormBlockInitializer', useComponentProps() { - const collection = useCollection(); + const collection = useCollection_deprecated(); const toManyField = useMemo( () => collection.fields.filter((field) => ['hasMany', 'belongsToMany'].includes(field.type)), [collection.fields], ); return { - filterItems(item) { - return toManyField.some((field) => field.target === item.name); + filterMenuItemChildren(collection) { + return toManyField.some((field) => field.target === collection.name); }, + onlyCurrentDataSource: true, }; }, }, @@ -276,16 +281,17 @@ export const recordBlockInitializers = new SchemaInitializer({ title: '{{t("Collapse")}}', Component: 'FilterCollapseBlockInitializer', useComponentProps() { - const collection = useCollection(); + const collection = useCollection_deprecated(); const toManyField = useMemo( () => collection.fields.filter((field) => ['hasMany', 'belongsToMany'].includes(field.type)), [collection.fields], ); return { - filterItems(item) { - return toManyField.some((field) => field.target === item.name); + filterMenuItemChildren(collection) { + return toManyField.some((field) => field.target === collection.name); }, + onlyCurrentDataSource: true, }; }, }, diff --git a/packages/core/client/src/schema-initializer/buttons/TabPaneInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/TabPaneInitializers.tsx index ebd6688e9..2fa3422c6 100644 --- a/packages/core/client/src/schema-initializer/buttons/TabPaneInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/TabPaneInitializers.tsx @@ -20,7 +20,7 @@ export const TabPaneInitializers = (props?: any) => { if (isCreate || !record) { initializer = 'CreateFormBlockInitializers'; } else if (isBulkEdit) { - initializer = 'CreateFormBulkEditBlockInitializers'; + initializer = 'BulkEditBlockInitializers'; } } return { diff --git a/packages/core/client/src/schema-initializer/buttons/index.ts b/packages/core/client/src/schema-initializer/buttons/index.ts index 10700873f..23171b79e 100644 --- a/packages/core/client/src/schema-initializer/buttons/index.ts +++ b/packages/core/client/src/schema-initializer/buttons/index.ts @@ -1,22 +1,7 @@ -export * from './BlockInitializers'; -export * from './CreateFormBlockInitializers'; -export * from './CusomeizeCreateFormBlockInitializers'; export * from './CustomFormItemInitializers'; -export * from './DetailsActionInitializers'; -export * from './FilterFormActionInitializers'; -export * from './FormActionInitializers'; export * from './FormItemInitializers'; -export * from './GridCardActionInitializers'; -export * from './ListActionInitializers'; -export * from './ReadPrettyFormActionInitializers'; -export * from './ReadPrettyFormItemInitializers'; export * from './RecordBlockInitializers'; -export * from './RecordFormBlockInitializers'; export * from './SubTableActionInitializers'; export * from './TabPaneInitializers'; -export * from './TableActionColumnInitializers'; -export * from './TableActionInitializers'; -export * from './TableColumnInitializers'; -export * from './TableSelectorInitializers'; // association filter export * from '../../schema-component/antd/association-filter/AssociationFilter'; diff --git a/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx index 68ec67327..e072b4dd0 100644 --- a/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx +++ b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx @@ -5,7 +5,11 @@ import { Button, Dropdown, MenuProps } from 'antd'; import React, { useEffect, useMemo, useState } from 'react'; import { useDesignable } from '../../'; import { useACLRolesCheck, useRecordPkValue } from '../../acl/ACLProvider'; -import { CollectionProvider, useCollection, useCollectionManager } from '../../collection-manager'; +import { + CollectionProvider_deprecated, + useCollection_deprecated, + useCollectionManager_deprecated, +} from '../../collection-manager'; import { useRecord } from '../../record-provider'; import { ActionContextProvider, useActionContext, useCompile } from '../../schema-component'; import { linkageAction } from '../../schema-component/antd/action/utils'; @@ -52,50 +56,59 @@ export const actionDesignerCss = css` } `; -const actionAclCheck = function useAclCheck(actionPath) { +export function useAclCheck(actionPath) { + const aclCheck = useAclCheckFn(); + return aclCheck(actionPath); +} + +function useAclCheckFn() { const { data, inResources, getResourceActionParams, getStrategyActionParams } = useACLRolesCheck(); const recordPkValue = useRecordPkValue(); - const collection = useCollection(); - const resource = collection.resource; - const parseAction = (actionPath: string, options: any = {}) => { - const [resourceName] = actionPath.split(':'); - if (data?.allowAll) { - return {}; + const collection = useCollection_deprecated(); + function actionAclCheck(actionPath: string) { + const resource = collection.resource; + const parseAction = (actionPath: string, options: any = {}) => { + const [resourceName] = actionPath.split(':'); + if (data?.allowAll) { + return {}; + } + if (inResources(resourceName)) { + return getResourceActionParams(actionPath); + } + return getStrategyActionParams(actionPath); + }; + if (!actionPath && resource) { + actionPath = `${resource}:create}`; } - if (inResources(resourceName)) { - return getResourceActionParams(actionPath); + if (!actionPath?.includes(':')) { + actionPath = `${resource}:${actionPath}`; + } + if (!actionPath) { + return true; + } + const params = parseAction(actionPath, { recordPkValue }); + if (!params) { + return false; } - return getStrategyActionParams(actionPath); - }; - if (!actionPath && resource) { - actionPath = `${resource}:create}`; - } - if (!actionPath?.includes(':')) { - actionPath = `${resource}:${actionPath}`; - } - if (!actionPath) { return true; } - const params = parseAction(actionPath, { recordPkValue }); - if (!params) { - return false; - } - return true; -}; + + return actionAclCheck; +} export const CreateRecordAction = observer( (props: any) => { const [visible, setVisible] = useState(false); - const collection = useCollection(); + const collection = useCollection_deprecated(); const fieldSchema = useFieldSchema(); const field: any = useField(); const [currentCollection, setCurrentCollection] = useState(collection.name); + const [currentCollectionDataSource, setCurrentCollectionDataSource] = useState(collection.dataSource); const linkageRules: any[] = fieldSchema?.['x-linkage-rules'] || []; const values = useRecord(); const ctx = useActionContext(); const variables = useVariables(); const localVariables = useLocalVariables({ currentForm: { values } as any }); - useEffect(() => { field.stateOfLinkageRules = {}; linkageRules @@ -117,14 +130,15 @@ export const CreateRecordAction = observer( { + onClick={(collectionData) => { setVisible(true); - setCurrentCollection(name); + setCurrentCollection(collectionData.name); + setCurrentCollectionDataSource(collectionData.dataSource); }} /> - + - +
); @@ -148,18 +162,19 @@ function getLinkageCollection(str, form, field) { export const CreateAction = observer( (props: any) => { const { onClick } = props; - const collection = useCollection(); + const collection = useCollection_deprecated(); const fieldSchema = useFieldSchema(); const field: any = useField(); const form = useForm(); const variables = useVariables(); + const aclCheck = useAclCheckFn(); const enableChildren = fieldSchema['x-enable-children'] || []; const allowAddToCurrent = fieldSchema?.['x-allow-add-to-current']; const linkageFromForm = fieldSchema?.['x-component-props']?.['linkageFromForm']; // antd v5 danger type is deprecated const componentType = field.componentProps.type === 'danger' ? undefined : field.componentProps.type || 'primary'; - const { getChildrenCollections } = useCollectionManager(); + const { getChildrenCollections } = useCollectionManager_deprecated(); const totalChildCollections = getChildrenCollections(collection.name); const inheritsCollections = useMemo(() => { return enableChildren @@ -172,12 +187,12 @@ export const CreateAction = observer( return; } return { - ...childCollection, + ...childCollection.getOptions(), title: k.title || childCollection.title, }; }) .filter((v) => { - return v && actionAclCheck(`${v.name}:create`); + return v && aclCheck(`${v.name}:create`); }); }, [enableChildren, totalChildCollections]); const linkageRules: any[] = fieldSchema?.['x-linkage-rules'] || []; @@ -190,7 +205,7 @@ export const CreateAction = observer( return inheritsCollections.map((option) => ({ key: option.name, label: compile(option.title), - onClick: () => onClick?.(option.name), + onClick: () => onClick?.(option), })); }, [inheritsCollections, onClick]); @@ -267,6 +282,7 @@ function FinallyButton({ form; designable: boolean; }) { + const { getCollection } = useCollectionManager_deprecated(); if (inheritsCollections?.length > 0) { if (!linkageFromForm) { return allowAddToCurrent === undefined || allowAddToCurrent ? ( @@ -281,7 +297,7 @@ function FinallyButton({ ]} menu={menu} onClick={(info) => { - onClick?.(collection.name); + onClick?.(collection); }} > {icon} @@ -306,9 +322,10 @@ function FinallyButton({ icon={icon} onClick={(info) => { const collectionName = getLinkageCollection(linkageFromForm, form, field); - const targetCollection = inheritsCollections.find((v) => v.name === collectionName) + const targetCollectionName = inheritsCollections.find((v) => v.name === collectionName) ? collectionName : collection.name; + const targetCollection = getCollection(targetCollectionName); onClick?.(targetCollection); }} style={{ @@ -329,7 +346,7 @@ function FinallyButton({ danger={props.danger} icon={icon} onClick={(info) => { - onClick?.(collection.name); + onClick?.(collection); }} style={{ display: !designable && field?.data?.hidden && 'none', diff --git a/packages/core/client/src/schema-initializer/components/assigned-field/AssignedField.tsx b/packages/core/client/src/schema-initializer/components/assigned-field/AssignedField.tsx index a17efde04..cd854a087 100644 --- a/packages/core/client/src/schema-initializer/components/assigned-field/AssignedField.tsx +++ b/packages/core/client/src/schema-initializer/components/assigned-field/AssignedField.tsx @@ -5,11 +5,10 @@ import _ from 'lodash'; import React, { useCallback, useEffect, useMemo } from 'react'; import { useFormBlockContext } from '../../../block-provider'; import { - CollectionFieldProvider, - useCollection, - useCollectionField, + useCollection_deprecated, + useCollectionField_deprecated, useCollectionFilterOptions, - useCollectionManager, + useCollectionManager_deprecated, } from '../../../collection-manager'; import { useRecord } from '../../../record-provider'; import { useCompile, useComponent } from '../../../schema-component'; @@ -17,7 +16,7 @@ import { VariableInput, getShouldChange } from '../../../schema-settings/Variabl import { Option } from '../../../schema-settings/VariableInput/type'; import { formatVariableScop } from '../../../schema-settings/VariableInput/utils/formatVariableScop'; import { useLocalVariables, useVariables } from '../../../variables'; -import { DeletedField } from '../DeletedField'; +import { CollectionFieldProvider } from '../../../data-source'; interface AssignedFieldProps { value: any; @@ -28,7 +27,7 @@ interface AssignedFieldProps { const InternalField: React.FC = (props) => { const field = useField(); const fieldSchema = useFieldSchema(); - const { uiSchema } = useCollectionField(); + const { uiSchema } = useCollectionField_deprecated(); const component = useComponent(uiSchema?.['x-component']); const compile = useCompile(); const setFieldProps = (key, value) => { @@ -82,7 +81,7 @@ const InternalField: React.FC = (props) => { const CollectionField = (props) => { const fieldSchema = useFieldSchema(); return ( - }> + ); @@ -95,8 +94,8 @@ export enum AssignedFieldValueType { export const AssignedField = (props: AssignedFieldProps) => { const { value, onChange } = props; - const { getCollectionFields, getAllCollectionsInheritChain } = useCollectionManager(); - const collection = useCollection(); + const { getCollectionFields, getAllCollectionsInheritChain } = useCollectionManager_deprecated(); + const collection = useCollection_deprecated(); const { form } = useFormBlockContext(); const fieldSchema = useFieldSchema(); const record = useRecord(); diff --git a/packages/core/client/src/schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer.ts b/packages/core/client/src/schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer.ts index 1b35c1f09..d73cdf072 100644 --- a/packages/core/client/src/schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer.ts +++ b/packages/core/client/src/schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer.ts @@ -1,6 +1,6 @@ import { useFieldSchema } from '@formily/react'; import { useCallback } from 'react'; -import { useCollection } from '../../collection-manager'; +import { useCollection_deprecated } from '../../collection-manager'; /** * label = 'schema-initializer' + x-component + [x-initializer] + [collectionName] + [postfix] @@ -9,7 +9,7 @@ import { useCollection } from '../../collection-manager'; export const useGetAriaLabelOfSchemaInitializer = () => { const fieldSchema = useFieldSchema(); - const { name } = useCollection(); + const { name } = useCollection_deprecated(); const getAriaLabel = useCallback( (postfix?: string) => { if (!fieldSchema) return ''; diff --git a/packages/core/client/src/schema-initializer/index.ts b/packages/core/client/src/schema-initializer/index.ts index e49b592b3..ee6d1f2ef 100644 --- a/packages/core/client/src/schema-initializer/index.ts +++ b/packages/core/client/src/schema-initializer/index.ts @@ -1,32 +1,63 @@ import { Plugin } from '../application/Plugin'; +import { CreateChildInitializer } from '../modules/actions/add-child/CreateChildInitializer'; +import { CreateActionInitializer } from '../modules/actions/add-new/CreateActionInitializer'; +import { createFormBlockInitializers } from '../modules/actions/add-new/createFormBlockInitializers'; +import { CustomizeAddRecordActionInitializer } from '../modules/actions/add-record/CustomizeAddRecordActionInitializer'; +import { customizeCreateFormBlockInitializers } from '../modules/actions/add-record/customizeCreateFormBlockInitializers'; +import { BulkDestroyActionInitializer } from '../modules/actions/bulk-destroy/BulkDestroyActionInitializer'; +import { DestroyActionInitializer } from '../modules/actions/delete/DestroyActionInitializer'; +import { ExpandableActionInitializer } from '../modules/actions/expand-collapse/ExpandableActionInitializer'; +import { FilterActionInitializer } from '../modules/actions/filter/FilterActionInitializer'; +import { RefreshActionInitializer } from '../modules/actions/refresh/RefreshActionInitializer'; +import { SaveRecordActionInitializer } from '../modules/actions/save-record/SaveRecordActionInitializer'; +import { CreateSubmitActionInitializer } from '../modules/actions/submit/CreateSubmitActionInitializer'; +import { UpdateSubmitActionInitializer } from '../modules/actions/submit/UpdateSubmitActionInitializer'; +import { UpdateRecordActionInitializer } from '../modules/actions/update-record/UpdateRecordActionInitializer'; +import { PopupActionInitializer } from '../modules/actions/view-edit-popup/PopupActionInitializer'; +import { recordFormBlockInitializers } from '../modules/actions/view-edit-popup/RecordFormBlockInitializers'; +import { UpdateActionInitializer } from '../modules/actions/view-edit-popup/UpdateActionInitializer'; +import { ViewActionInitializer } from '../modules/actions/view-edit-popup/ViewActionInitializer'; +import { detailsActionInitializers } from '../modules/blocks/data-blocks/details-multi/DetailsActionInitializers'; +import { DetailsBlockInitializer } from '../modules/blocks/data-blocks/details-multi/DetailsBlockInitializer'; +import { readPrettyFormActionInitializers } from '../modules/blocks/data-blocks/details-single/ReadPrettyFormActionInitializers'; +import { readPrettyFormItemInitializers } from '../modules/blocks/data-blocks/details-single/ReadPrettyFormItemInitializers'; +import { RecordReadPrettyFormBlockInitializer } from '../modules/blocks/data-blocks/details-single/RecordReadPrettyFormBlockInitializer'; +import { CreateFormBlockInitializer } from '../modules/blocks/data-blocks/form/CreateFormBlockInitializer'; +import { FormBlockInitializer } from '../modules/blocks/data-blocks/form/FormBlockInitializer'; +import { RecordFormBlockInitializer } from '../modules/blocks/data-blocks/form/RecordFormBlockInitializer'; +import { createFormActionInitializers } from '../modules/blocks/data-blocks/form/createFormActionInitializers'; +import { formActionInitializers } from '../modules/blocks/data-blocks/form/formActionInitializers'; +import { formItemInitializers } from '../modules/blocks/data-blocks/form/formItemInitializers'; +import { updateFormActionInitializers } from '../modules/blocks/data-blocks/form/updateFormActionInitializers'; +import { gridCardActionInitializers } from '../modules/blocks/data-blocks/grid-card/GridCardActionInitializers'; +import { GridCardBlockInitializer } from '../modules/blocks/data-blocks/grid-card/GridCardBlockInitializer'; +import { gridCardItemActionInitializers } from '../modules/blocks/data-blocks/grid-card/gridCardItemActionInitializers'; +import { listActionInitializers } from '../modules/blocks/data-blocks/list/ListActionInitializers'; +import { ListBlockInitializer } from '../modules/blocks/data-blocks/list/ListBlockInitializer'; +import { listItemActionInitializers } from '../modules/blocks/data-blocks/list/listItemActionInitializers'; +import { TableSelectorInitializer } from '../modules/blocks/data-blocks/table-selector/TableSelectorInitializer'; +import { tableActionColumnInitializers } from '../modules/blocks/data-blocks/table/TableActionColumnInitializers'; +import { tableActionInitializers } from '../modules/blocks/data-blocks/table/TableActionInitializers'; +import { TableBlockInitializer } from '../modules/blocks/data-blocks/table/TableBlockInitializer'; +import { tableColumnInitializers } from '../modules/blocks/data-blocks/table/TableColumnInitializers'; +import { FilterCollapseBlockInitializer } from '../modules/blocks/filter-blocks/collapse/FilterCollapseBlockInitializer'; +import { filterFormActionInitializers } from '../modules/blocks/filter-blocks/form/FilterFormActionInitializers'; +import { FilterFormBlockInitializer } from '../modules/blocks/filter-blocks/form/FilterFormBlockInitializer'; +import { filterFormItemInitializers } from '../modules/blocks/filter-blocks/form/filterFormItemInitializers'; +import { MarkdownBlockInitializer } from '../modules/blocks/other-blocks/markdown/MarkdownBlockInitializer'; +import { MarkdownFormItemInitializer } from '../modules/blocks/other-blocks/markdown/MarkdownFormItemInitializer'; +import { tableSelectorInitializers } from '../modules/fields/component/Picker/TableSelectorInitializers'; +import { CollectionFieldInitializer } from '../modules/fields/initializer/CollectionFieldInitializer'; +import { TableCollectionFieldInitializer } from '../modules/fields/initializer/TableCollectionFieldInitializer'; +import { menuItemInitializer } from '../modules/menu/menuItemInitializer'; +import { blockInitializers } from '../modules/page/BlockInitializers'; import { - blockInitializers, - createFormActionInitializers, - createFormBlockInitializers, - cusomeizeCreateFormBlockInitializers, customFormItemInitializers, - detailsActionInitializers, - filterFormActionInitializers, - filterFormItemInitializers, - formActionInitializers, - formItemInitializers, - gridCardActionInitializers, - gridCardItemActionInitializers, - listActionInitializers, - listItemActionInitializers, - readPrettyFormActionInitializers, - readPrettyFormItemInitializers, recordBlockInitializers, - recordFormBlockInitializers, subTableActionInitializers, tabPaneInitializers, tabPaneInitializersForBulkEditFormBlock, tabPaneInitializersForRecordBlock, - tableActionColumnInitializers, - tableActionInitializers, - tableColumnInitializers, - tableSelectorInitializers, - updateFormActionInitializers, } from './buttons'; import * as initializerComponents from './components'; import * as items from './items'; @@ -42,7 +73,6 @@ export { useAssociatedFormItemInitializerFields, useAssociatedTableColumnInitializerFields, useCollectionDataSourceItems, - useCollectionDataSourceItemsV2, useCurrentSchema, useFormItemInitializerFields, useInheritsTableColumnInitializerFields, @@ -56,6 +86,36 @@ export class SchemaInitializerPlugin extends Plugin { this.app.addComponents({ ...initializerComponents, ...items, + CreateFormBlockInitializer, + FormBlockInitializer, + RecordFormBlockInitializer, + TableBlockInitializer, + TableSelectorInitializer, + RecordReadPrettyFormBlockInitializer, + DetailsBlockInitializer, + ListBlockInitializer, + GridCardBlockInitializer, + FilterFormBlockInitializer, + FilterCollapseBlockInitializer, + MarkdownBlockInitializer, + MarkdownFormItemInitializer, + TableCollectionFieldInitializer, + CollectionFieldInitializer, + CreateActionInitializer, + CustomizeAddRecordActionInitializer, + CreateChildInitializer, + ViewActionInitializer, + UpdateActionInitializer, + PopupActionInitializer, + SaveRecordActionInitializer, + UpdateRecordActionInitializer, + CreateSubmitActionInitializer, + UpdateSubmitActionInitializer, + BulkDestroyActionInitializer, + ExpandableActionInitializer, + DestroyActionInitializer, + FilterActionInitializer, + RefreshActionInitializer, } as any); this.app.schemaInitializerManager.add(blockInitializers); @@ -68,7 +128,7 @@ export class SchemaInitializerPlugin extends Plugin { this.app.schemaInitializerManager.add(readPrettyFormItemInitializers); this.app.schemaInitializerManager.add(readPrettyFormActionInitializers); this.app.schemaInitializerManager.add(createFormBlockInitializers); - this.app.schemaInitializerManager.add(cusomeizeCreateFormBlockInitializers); + this.app.schemaInitializerManager.add(customizeCreateFormBlockInitializers); this.app.schemaInitializerManager.add(customFormItemInitializers); this.app.schemaInitializerManager.add(filterFormActionInitializers); this.app.schemaInitializerManager.add(createFormActionInitializers); @@ -86,5 +146,7 @@ export class SchemaInitializerPlugin extends Plugin { this.app.schemaInitializerManager.add(tabPaneInitializers); this.app.schemaInitializerManager.add(tabPaneInitializersForRecordBlock); this.app.schemaInitializerManager.add(tabPaneInitializersForBulkEditFormBlock); + this.app.schemaInitializerManager.add(createFormBlockInitializers); + this.app.schemaInitializerManager.add(menuItemInitializer); } } diff --git a/packages/core/client/src/schema-initializer/items/CustomizeActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/CustomizeActionInitializer.tsx index 3be3df60a..eb4113cfa 100644 --- a/packages/core/client/src/schema-initializer/items/CustomizeActionInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/CustomizeActionInitializer.tsx @@ -1,18 +1,9 @@ import React from 'react'; import { BlockInitializer } from '.'; -import { useCollection } from '../../collection-manager'; import { useSchemaInitializerItem } from '../../application'; export const CustomizeActionInitializer = () => { - const collection = useCollection(); const itemConfig = useSchemaInitializerItem(); - - const schema = {}; - if (collection && schema['x-acl-action']) { - schema['x-acl-action'] = `${collection.name}:${schema['x-acl-action']}`; - schema['x-decorator'] = 'ACLActionProvider'; - } - - return ; + return ; }; diff --git a/packages/core/client/src/schema-initializer/items/DataBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/DataBlockInitializer.tsx index ce98e755c..61263af92 100644 --- a/packages/core/client/src/schema-initializer/items/DataBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/DataBlockInitializer.tsx @@ -5,12 +5,13 @@ import { useTranslation } from 'react-i18next'; import { SchemaInitializerItem, SchemaInitializerMenu, + useGetSchemaInitializerMenuItems, useSchemaInitializer, - useSchemaInitializerMenuItems, } from '../../application'; +import { Collection } from '../../data-source/collection/Collection'; import { useCompile } from '../../schema-component'; import { useSchemaTemplateManager } from '../../schema-templates'; -import { useCollectionDataSourceItemsV2 } from '../utils'; +import { useCollectionDataSourceItems } from '../utils'; const MENU_ITEM_HEIGHT = 40; const STEP = 15; @@ -108,27 +109,41 @@ const LoadingItem = ({ loadMore, maxHeight }) => { ); }; -export function useMenuSearch(items: any[], isOpenSubMenu: boolean, showType?: boolean) { +export function useMenuSearch(data: any[], openKeys: string[], showType?: boolean) { const [searchValue, setSearchValue] = useState(''); const [count, setCount] = useState(STEP); + + const isMuliSource = useMemo(() => data.length > 1, [data]); + const openKey = useMemo(() => { + return isMuliSource ? openKeys?.[1] : openKeys?.length > 0; + }, [openKeys]); + useEffect(() => { - if (isOpenSubMenu) { + if (!openKey) { setSearchValue(''); } - }, [isOpenSubMenu]); + }, [openKey]); + + const currentItems = useMemo(() => { + if (isMuliSource) { + if (!openKey) return []; + return data.find((item) => (item.key || item.name) === openKey)?.children || []; + } + return data[0]?.children || []; + }, [data, isMuliSource, openKey]); // 根据搜索的值进行处理 const searchedItems = useMemo(() => { - if (!searchValue) return items; + if (!searchValue) return currentItems; const lowerSearchValue = searchValue.toLocaleLowerCase(); - return items.filter( + return currentItems.filter( (item) => (item.label || item.title) && String(item.label || item.title) .toLocaleLowerCase() .includes(lowerSearchValue), ); - }, [searchValue, items]); + }, [searchValue, currentItems]); const shouldLoadMore = useMemo(() => searchedItems.length > count, [count, searchedItems]); @@ -207,7 +222,23 @@ export function useMenuSearch(items: any[], isOpenSubMenu: boolean, showType?: b return res; }, [limitedSearchedItems, searchValue, shouldLoadMore, showType]); - return resultItems; + const res = useMemo(() => { + if (!isMuliSource) return resultItems; + return data.map((item) => { + if (openKey && item.key === openKey) { + return { + ...item, + children: resultItems, + }; + } else { + return { + ...item, + children: [], + }; + } + }); + }, [data, openKey, resultItems]); + return res; } export interface DataBlockInitializerProps { @@ -225,9 +256,9 @@ export interface DataBlockInitializerProps { icon?: string | React.ReactNode; name: string; title: string; - items?: any[]; - filterItems?: (item: any, index: number, items: any[]) => boolean; + filter?: (collection: Collection) => boolean; componentType: string; + onlyCurrentDataSource?: boolean; } export const DataBlockInitializer = (props: DataBlockInitializerProps) => { @@ -240,10 +271,10 @@ export const DataBlockInitializer = (props: DataBlockInitializerProps) => { icon = TableOutlined, name, title, - items, - filterItems, + filter, + onlyCurrentDataSource, } = props; - const { insert } = useSchemaInitializer(); + const { insert, setVisible } = useSchemaInitializer(); const compile = useCompile(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); const onClick = useCallback( @@ -255,23 +286,27 @@ export const DataBlockInitializer = (props: DataBlockInitializerProps) => { if (onCreateBlockSchema) { onCreateBlockSchema({ item }); } else if (createBlockSchema) { - insert(createBlockSchema({ collection: item.collectionName || item.name, isCusomeizeCreate })); + insert( + createBlockSchema({ + collection: item.collectionName || item.name, + dataSource: item.dataSource, + isCusomeizeCreate, + settings: 'blockSettings:createForm', + }), + ); } } + setVisible(false); }, [createBlockSchema, getTemplateSchemaByMode, insert, isCusomeizeCreate, onCreateBlockSchema, templateWrap], ); - const defaultItems = useCollectionDataSourceItemsV2(componentType); - const menuChildren = useMemo(() => { - const result = items || defaultItems; - if (filterItems) { - return result.filter(filterItems); - } - return result; - }, [items, defaultItems]); - const childItems = useSchemaInitializerMenuItems(menuChildren, name, onClick); - const [isOpenSubMenu, setIsOpenSubMenu] = useState(false); - const searchedChildren = useMenuSearch(childItems, isOpenSubMenu); + const items = useCollectionDataSourceItems(componentType, filter, onlyCurrentDataSource); + const getMenuItems = useGetSchemaInitializerMenuItems(onClick); + const childItems = useMemo(() => { + return getMenuItems(items, name); + }, [getMenuItems, items, name]); + const [openMenuKeys, setOpenMenuKeys] = useState([]); + const searchedChildren = useMenuSearch(childItems, openMenuKeys); const compiledMenuItems = useMemo( () => [ { @@ -285,14 +320,14 @@ export const DataBlockInitializer = (props: DataBlockInitializerProps) => { children: searchedChildren, }, ], - [name, compile, title, icon, searchedChildren, onClick, props], + [name, compile, title, icon, childItems, onClick, props], ); - if (menuChildren.length > 0) { + if (childItems.length > 1 || (childItems.length === 1 && childItems[0].children?.length > 0)) { return ( { - setIsOpenSubMenu(keys.length > 0); + setOpenMenuKeys(keys); }} items={compiledMenuItems} /> diff --git a/packages/core/client/src/schema-initializer/items/FilterFormBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/FilterFormBlockInitializer.tsx deleted file mode 100644 index e48303b1c..000000000 --- a/packages/core/client/src/schema-initializer/items/FilterFormBlockInitializer.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { FormOutlined } from '@ant-design/icons'; -import React from 'react'; -import { useSchemaInitializerItem } from '../../application'; -import { createFilterFormBlockSchema } from '../utils'; -import { FilterBlockInitializer } from './FilterBlockInitializer'; - -export const FilterFormBlockInitializer = ({ filterItems }) => { - const itemConfig = useSchemaInitializerItem(); - - return ( - } - componentType={'FilterFormItem'} - templateWrap={(templateSchema, { item }) => { - const s = createFilterFormBlockSchema({ - template: templateSchema, - collection: item.name || item.collectionName, - }); - if (item.template && item.mode === 'reference') { - s['x-template-key'] = item.template.key; - } - return s; - }} - createBlockSchema={createFilterFormBlockSchema} - filterItems={filterItems} - /> - ); -}; diff --git a/packages/core/client/src/schema-initializer/items/RecordAssociationBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordAssociationBlockInitializer.tsx index c3f771ddf..2f6ef29ee 100644 --- a/packages/core/client/src/schema-initializer/items/RecordAssociationBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordAssociationBlockInitializer.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { TableOutlined } from '@ant-design/icons'; -import { useCollectionManager } from '../../collection-manager'; +import { useCollectionManager_deprecated } from '../../collection-manager'; import { useSchemaTemplateManager } from '../../schema-templates'; import { createTableBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; @@ -11,7 +11,7 @@ export const RecordAssociationBlockInitializer = () => { const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); - const { getCollection } = useCollectionManager(); + const { getCollection } = useCollectionManager_deprecated(); const field = itemConfig.field; const collection = getCollection(field.target); const resource = `${field.collectionName}.${field.name}`; @@ -28,6 +28,7 @@ export const RecordAssociationBlockInitializer = () => { createTableBlockSchema({ rowKey: collection.filterTargetKey, collection: field.target, + dataSource: collection.dataSource, resource, association: resource, }), diff --git a/packages/core/client/src/schema-initializer/items/RecordAssociationDetailsBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordAssociationDetailsBlockInitializer.tsx index 5e66b5d5e..d4e5ee200 100644 --- a/packages/core/client/src/schema-initializer/items/RecordAssociationDetailsBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordAssociationDetailsBlockInitializer.tsx @@ -1,17 +1,17 @@ -import React from 'react'; import { FormOutlined } from '@ant-design/icons'; +import React from 'react'; -import { useCollectionManager } from '../../collection-manager'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import { useCollectionManager_deprecated } from '../../collection-manager'; import { useSchemaTemplateManager } from '../../schema-templates'; import { createDetailsBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; -import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; export const RecordAssociationDetailsBlockInitializer = () => { const itemConfig = useSchemaInitializerItem(); const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); - const { getCollection } = useCollectionManager(); + const { getCollection } = useCollectionManager_deprecated(); const field = itemConfig.field; const collection = getCollection(field.target); const resource = `${field.collectionName}.${field.name}`; @@ -28,8 +28,10 @@ export const RecordAssociationDetailsBlockInitializer = () => { createDetailsBlockSchema({ collection: field.target, resource, + dataSource: collection.dataSource, association: resource, rowKey: collection.filterTargetKey || 'id', + settings: 'blockSettings:multiDataDetails', }), ); } diff --git a/packages/core/client/src/schema-initializer/items/RecordAssociationFormBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordAssociationFormBlockInitializer.tsx index ed6bb4d51..0d22eec64 100644 --- a/packages/core/client/src/schema-initializer/items/RecordAssociationFormBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordAssociationFormBlockInitializer.tsx @@ -1,17 +1,20 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { FormOutlined } from '@ant-design/icons'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; import { useSchemaTemplateManager } from '../../schema-templates'; import { createFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; -import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import { useCollectionManager_deprecated } from '../../collection-manager'; export const RecordAssociationFormBlockInitializer = () => { const itemConfig = useSchemaInitializerItem(); const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; const { insert } = useSchemaInitializer(); + const { getCollection } = useCollectionManager_deprecated(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); const field = itemConfig.field; - const collection = field.target; + const collectionName = field.target; + const collection = useMemo(() => getCollection(collectionName), [collectionName]); const resource = `${field.collectionName}.${field.name}`; return ( { const s = await getTemplateSchemaByMode(item); if (item.template.componentName === 'FormItem') { const blockSchema = createFormBlockSchema({ - collection, + collection: collectionName, + dataSource: collection.dataSource, resource, association: resource, action, @@ -35,6 +39,7 @@ export const RecordAssociationFormBlockInitializer = () => { useParams: '{{ useParamsFromRecord }}', actionInitializers, template: s, + settings: 'blockSettings:createForm', }); if (item.mode === 'reference') { blockSchema['x-template-key'] = item.template.key; @@ -46,13 +51,15 @@ export const RecordAssociationFormBlockInitializer = () => { } else { insert( createFormBlockSchema({ - collection, + collection: collectionName, resource, + dataSource: collection.dataSource, association: resource, action, useSourceId: '{{ useSourceIdFromParentRecord }}', useParams: '{{ useParamsFromRecord }}', actionInitializers, + settings: 'blockSettings:createForm', }), ); } diff --git a/packages/core/client/src/schema-initializer/items/RecordAssociationGridCardBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordAssociationGridCardBlockInitializer.tsx index e0f7f53be..48942cc1b 100644 --- a/packages/core/client/src/schema-initializer/items/RecordAssociationGridCardBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordAssociationGridCardBlockInitializer.tsx @@ -1,17 +1,17 @@ -import React from 'react'; import { TableOutlined } from '@ant-design/icons'; +import React from 'react'; -import { useCollectionManager } from '../../collection-manager'; +import { useCollectionManager_deprecated } from '../../collection-manager'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; import { useSchemaTemplateManager } from '../../schema-templates'; import { createGridCardBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; -import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; export const RecordAssociationGridCardBlockInitializer = () => { const itemConfig = useSchemaInitializerItem(); const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); - const { getCollection } = useCollectionManager(); + const { getCollection } = useCollectionManager_deprecated(); const field = itemConfig.field; const collection = getCollection(field.target); const resource = `${field.collectionName}.${field.name}`; @@ -30,7 +30,9 @@ export const RecordAssociationGridCardBlockInitializer = () => { rowKey: collection.filterTargetKey, collection: field.target, resource, + dataSource: collection.dataSource, association: resource, + settings: 'blockSettings:gridCard', }), ); } diff --git a/packages/core/client/src/schema-initializer/items/RecordAssociationListBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordAssociationListBlockInitializer.tsx index dabb0a2e1..4dc50011e 100644 --- a/packages/core/client/src/schema-initializer/items/RecordAssociationListBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordAssociationListBlockInitializer.tsx @@ -1,17 +1,17 @@ -import React from 'react'; import { TableOutlined } from '@ant-design/icons'; +import React from 'react'; -import { useCollectionManager } from '../../collection-manager'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import { useCollectionManager_deprecated } from '../../collection-manager'; import { useSchemaTemplateManager } from '../../schema-templates'; import { createListBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; -import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; export const RecordAssociationListBlockInitializer = () => { const itemConfig = useSchemaInitializerItem(); const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); - const { getCollection } = useCollectionManager(); + const { getCollection } = useCollectionManager_deprecated(); const field = itemConfig.field; const collection = getCollection(field.target); const resource = `${field.collectionName}.${field.name}`; @@ -30,7 +30,9 @@ export const RecordAssociationListBlockInitializer = () => { rowKey: collection.filterTargetKey, collection: field.target, resource, + dataSource: collection.dataSource, association: resource, + settings: 'blockSettings:list', }), ); } diff --git a/packages/core/client/src/schema-initializer/items/RecordReadPrettyAssociationFormBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordReadPrettyAssociationFormBlockInitializer.tsx index 3a92bd872..503c1c7b2 100644 --- a/packages/core/client/src/schema-initializer/items/RecordReadPrettyAssociationFormBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordReadPrettyAssociationFormBlockInitializer.tsx @@ -1,19 +1,23 @@ -import React from 'react'; import { FormOutlined } from '@ant-design/icons'; +import React from 'react'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; import { useBlockRequestContext } from '../../block-provider'; import { useSchemaTemplateManager } from '../../schema-templates'; import { createReadPrettyFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; -import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import { useCollectionManager_deprecated } from '../../collection-manager'; export const RecordReadPrettyAssociationFormBlockInitializer = () => { const itemConfig = useSchemaInitializerItem(); const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; const { insert } = useSchemaInitializer(); + const { getCollection } = useCollectionManager_deprecated(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); const field = itemConfig.field; - const collection = field.target; + const collectionName = field.target; + const collection = getCollection(collectionName); + const resource = `${field.collectionName}.${field.name}`; const { block } = useBlockRequestContext(); const actionInitializers = block !== 'TableField' ? 'ReadPrettyFormActionInitializers' : null; @@ -28,13 +32,15 @@ export const RecordReadPrettyAssociationFormBlockInitializer = () => { if (item.template.componentName === 'ReadPrettyFormItem') { const blockSchema = createReadPrettyFormBlockSchema({ actionInitializers, - collection, + collection: collectionName, + dataSource: collection.dataSource, resource, association: resource, action: 'get', useSourceId: '{{ useSourceIdFromParentRecord }}', useParams: '{{ useParamsFromRecord }}', template: s, + settings: 'blockSettings:singleDataDetails', }); if (item.mode === 'reference') { blockSchema['x-template-key'] = item.template.key; @@ -47,12 +53,14 @@ export const RecordReadPrettyAssociationFormBlockInitializer = () => { insert( createReadPrettyFormBlockSchema({ actionInitializers, - collection, + collection: collectionName, resource, association: resource, + dataSource: collection.dataSource, action: 'get', useSourceId: '{{ useSourceIdFromParentRecord }}', useParams: '{{ useParamsFromRecord }}', + settings: 'blockSettings:singleDataDetails', }), ); } diff --git a/packages/core/client/src/schema-initializer/items/SelectActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/SelectActionInitializer.tsx index 49b8eb53e..3364d9fb4 100644 --- a/packages/core/client/src/schema-initializer/items/SelectActionInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/SelectActionInitializer.tsx @@ -39,7 +39,9 @@ export const SelectActionInitializer = (props) => { title: '{{ t("Submit") }}', 'x-action': 'submit', 'x-component': 'Action', - 'x-designer': 'Action.Designer', + // 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:updateSubmit', 'x-component-props': { type: 'primary', htmlType: 'submit', diff --git a/packages/core/client/src/schema-initializer/items/SubmitActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/SubmitActionInitializer.tsx index 9837061c4..f3f0afb84 100644 --- a/packages/core/client/src/schema-initializer/items/SubmitActionInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/SubmitActionInitializer.tsx @@ -7,7 +7,9 @@ export const SubmitActionInitializer = (props) => { title: '{{ t("Submit") }}', 'x-action': 'submit', 'x-component': 'Action', - 'x-designer': 'Action.Designer', + // 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:updateSubmit', 'x-component-props': { type: 'primary', htmlType: 'submit', diff --git a/packages/core/client/src/schema-initializer/items/TableActionColumnInitializer.tsx b/packages/core/client/src/schema-initializer/items/TableActionColumnInitializer.tsx index 35fae6ebd..604c77935 100644 --- a/packages/core/client/src/schema-initializer/items/TableActionColumnInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/TableActionColumnInitializer.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { uid } from '@formily/shared'; -import { InitializerWithSwitch } from './InitializerWithSwitch'; import { useSchemaInitializerItem } from '../../application'; +import { InitializerWithSwitch } from './InitializerWithSwitch'; export const TableActionColumnInitializer = () => { const schema = { diff --git a/packages/core/client/src/schema-initializer/items/index.tsx b/packages/core/client/src/schema-initializer/items/index.tsx index 0702c6567..e52a191cc 100644 --- a/packages/core/client/src/schema-initializer/items/index.tsx +++ b/packages/core/client/src/schema-initializer/items/index.tsx @@ -5,45 +5,19 @@ export * from '../../schema-component/antd/association-filter/AssociationFilterD export * from '../../schema-component/antd/association-filter/AssociationFilterDesignerDisplayField'; export * from './ActionInitializer'; export * from './BlockInitializer'; -export * from './BulkDestroyActionInitializer'; -export * from './CollectionFieldInitializer'; -export * from './CreateActionInitializer'; -export * from './CreateChildInitializer'; export * from './CreateFilterActionInitializer'; -export * from './CreateFormBlockInitializer'; export * from './CreateResetActionInitializer'; -export * from './CreateSubmitActionInitializer'; export * from './CustomizeActionInitializer'; -export * from './CustomizeAddRecordActionInitializer'; export * from './DataBlockInitializer'; export * from './DeleteEventActionInitializer'; -export * from './DestroyActionInitializer'; -export * from './DetailsBlockInitializer'; -export * from './ExpandActionInitializer'; -export * from './FilterActionInitializer'; -export * from './FilterCollapseBlockInitializer'; -export * from './FilterFormBlockInitializer'; -export * from './FormBlockInitializer'; export * from './G2PlotInitializer'; -export * from './GridCardBlockInitializer'; export * from './InitializerWithSwitch'; -export * from './ListBlockInitializer'; -export * from './MarkdownBlockInitializer'; export * from './RecordAssociationBlockInitializer'; -export * from './RecordAssociationGridCardBlockInitializer'; -export * from './RecordAssociationListBlockInitializer'; export * from './RecordAssociationDetailsBlockInitializer'; export * from './RecordAssociationFormBlockInitializer'; -export * from './RecordFormBlockInitializer'; +export * from './RecordAssociationGridCardBlockInitializer'; +export * from './RecordAssociationListBlockInitializer'; export * from './RecordReadPrettyAssociationFormBlockInitializer'; -export * from './RecordReadPrettyFormBlockInitializer'; -export * from './RefreshActionInitializer'; export * from './SelectActionInitializer'; export * from './SubmitActionInitializer'; export * from './TableActionColumnInitializer'; -export * from './TableBlockInitializer'; -export * from './TableCollectionFieldInitializer'; -export * from './TableSelectorInitializer'; -export * from './UpdateActionInitializer'; -export * from './UpdateSubmitActionInitializer'; -export * from './ViewActionInitializer'; diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts index ba516f42e..19bee2e57 100644 --- a/packages/core/client/src/schema-initializer/utils.ts +++ b/packages/core/client/src/schema-initializer/utils.ts @@ -3,11 +3,14 @@ import { ISchema, Schema, useFieldSchema, useForm } from '@formily/react'; import { uid } from '@formily/shared'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { SchemaInitializerItemType, useFormActiveFields, useFormBlockContext } from '../'; -import { CollectionFieldOptions, FieldOptions, useCollection, useCollectionManager } from '../collection-manager'; +import { SchemaInitializerItemType, useDataSourceKey, useFormActiveFields, useFormBlockContext } from '../'; +import { FieldOptions, useCollection_deprecated, useCollectionManager_deprecated } from '../collection-manager'; import { isAssocField } from '../filter-provider/utils'; import { useActionContext, useDesignable } from '../schema-component'; import { useSchemaTemplateManager } from '../schema-templates'; +import { Collection } from '../data-source/collection/Collection'; +import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider'; +import { DataSourceManager } from '../data-source/data-source/DataSourceManager'; export const itemsMerge = (items1) => { return items1; @@ -85,8 +88,8 @@ const quickEditField = [ ]; export const useTableColumnInitializerFields = () => { - const { name, currentFields = [] } = useCollection(); - const { getInterface, getCollection } = useCollectionManager(); + const { name, currentFields = [] } = useCollection_deprecated(); + const { getInterface, getCollection } = useCollectionManager_deprecated(); const fieldSchema = useFieldSchema(); const isSubTable = fieldSchema['x-component'] === 'AssociationField.SubTable'; const form = useForm(); @@ -146,8 +149,8 @@ export const useTableColumnInitializerFields = () => { }; export const useAssociatedTableColumnInitializerFields = () => { - const { name, fields } = useCollection(); - const { getInterface, getCollectionFields, getCollection } = useCollectionManager(); + const { name, fields } = useCollection_deprecated(); + const { getInterface, getCollectionFields, getCollection } = useCollectionManager_deprecated(); const groups = fields ?.filter((field) => { return ['o2o', 'oho', 'obo', 'm2o'].includes(field.interface); @@ -203,8 +206,9 @@ export const useAssociatedTableColumnInitializerFields = () => { }; export const useInheritsTableColumnInitializerFields = () => { - const { name } = useCollection(); - const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } = useCollectionManager(); + const { name } = useCollection_deprecated(); + const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } = + useCollectionManager_deprecated(); const fieldSchema = useFieldSchema(); const isSubTable = fieldSchema['x-component'] === 'AssociationField.SubTable'; const form = useForm(); @@ -269,26 +273,25 @@ export const useInheritsTableColumnInitializerFields = () => { }; export const useFormItemInitializerFields = (options?: any) => { - const { name, currentFields, template } = useCollection(); - const { getInterface, getCollection } = useCollectionManager(); + const { name, currentFields } = useCollection_deprecated(); + const { getInterface, getCollection } = useCollectionManager_deprecated(); const form = useForm(); const { readPretty = form.readPretty, block = 'Form' } = options || {}; - const { snapshot, fieldSchema } = useActionContext(); + const { fieldSchema } = useActionContext(); const action = fieldSchema?.['x-action']; + return currentFields ?.filter((field) => field?.interface && !field?.isForeignKey && !field?.treeChildren) ?.map((field) => { const interfaceConfig = getInterface(field.interface); const targetCollection = getCollection(field.target); const isFileCollection = field?.target && getCollection(field?.target)?.template === 'file'; - // const component = - // field.interface === 'o2m' && targetCollection?.template !== 'file' && !snapshot - // ? 'TableField' - // : 'CollectionField'; + const isAssociationField = targetCollection; const schema = { type: 'string', name: field.name, - 'x-designer': 'FormItem.Designer', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', 'x-component': 'CollectionField', 'x-decorator': 'FormItem', 'x-collection-field': `${name}.${field.name}`, @@ -299,10 +302,13 @@ export const useFormItemInitializerFields = (options?: any) => { value: 'id', }, } - : {}, + : isAssociationField + ? { + fieldNames: field?.uiSchema['x-component-props']['fieldNames'], + } + : {}, 'x-read-pretty': field?.uiSchema?.['x-read-pretty'], }; - // interfaceConfig?.schemaInitialize?.(schema, { field, block: 'Form', readPretty: form.readPretty }); const resultItem = { type: 'item', name: field.name, @@ -333,8 +339,8 @@ export const useFormItemInitializerFields = (options?: any) => { // 筛选表单相关 export const useFilterFormItemInitializerFields = (options?: any) => { - const { name, currentFields } = useCollection(); - const { getInterface, getCollection } = useCollectionManager(); + const { name, currentFields } = useCollection_deprecated(); + const { getInterface, getCollection } = useCollectionManager_deprecated(); const form = useForm(); const { readPretty = form.readPretty, block = 'FilterForm' } = options || {}; const { snapshot, fieldSchema } = useActionContext(); @@ -345,15 +351,13 @@ export const useFilterFormItemInitializerFields = (options?: any) => { ?.map((field) => { const interfaceConfig = getInterface(field.interface); const targetCollection = getCollection(field.target); - // const component = - // field.interface === 'o2m' && targetCollection?.template !== 'file' && !snapshot - // ? 'TableField' - // : 'CollectionField'; let schema = { type: 'string', name: field.name, required: false, - 'x-designer': 'FormItem.FilterFormDesigner', + // 'x-designer': 'FormItem.FilterFormDesigner', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FilterFormItem', 'x-component': 'CollectionField', 'x-decorator': 'FormItem', 'x-collection-field': `${name}.${field.name}`, @@ -364,7 +368,9 @@ export const useFilterFormItemInitializerFields = (options?: any) => { type: 'string', name: `${field.name}`, required: false, - 'x-designer': 'FormItem.FilterFormDesigner', + // 'x-designer': 'FormItem.FilterFormDesigner', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FilterFormItem', 'x-component': 'CollectionField', 'x-decorator': 'FormItem', 'x-collection-field': `${name}.${field.name}`, @@ -394,8 +400,8 @@ export const useFilterFormItemInitializerFields = (options?: any) => { }; export const useAssociatedFormItemInitializerFields = (options?: any) => { - const { name, fields } = useCollection(); - const { getInterface, getCollectionFields, getCollection } = useCollectionManager(); + const { name, fields } = useCollection_deprecated(); + const { getInterface, getCollectionFields, getCollection } = useCollectionManager_deprecated(); const form = useForm(); const { readPretty = form.readPretty, block = 'Form' } = options || {}; const interfaces = block === 'Form' ? ['m2o'] : ['o2o', 'oho', 'obo', 'm2o']; @@ -416,9 +422,10 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => { const schema = { type: 'string', name: `${field.name}.${subField.name}`, - 'x-designer': 'FormItem.Designer', + // 'x-designer': 'FormItem.Designer', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', 'x-component': 'CollectionField', - 'x-read-pretty': readPretty, 'x-component-props': { 'pattern-disable': block === 'Form' && readPretty, @@ -492,7 +499,9 @@ const getItem = ( const schema = { type: 'string', name: schemaName, - 'x-designer': 'FormItem.FilterFormDesigner', + // 'x-designer': 'FormItem.FilterFormDesigner', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FilterFormItem', 'x-designer-props': { // 在 useOperatorList 中使用,用于获取对应的操作符列表 interface: field.interface, @@ -515,8 +524,8 @@ const getItem = ( // 筛选表单相关 export const useFilterAssociatedFormItemInitializerFields = () => { - const { name, fields } = useCollection(); - const { getCollectionFields } = useCollectionManager(); + const { name, fields } = useCollection_deprecated(); + const { getCollectionFields } = useCollectionManager_deprecated(); const interfaces = ['m2o']; const groups = fields ?.filter((field) => { @@ -527,8 +536,9 @@ export const useFilterAssociatedFormItemInitializerFields = () => { }; export const useInheritsFormItemInitializerFields = (options?) => { - const { name } = useCollection(); - const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } = useCollectionManager(); + const { name } = useCollection_deprecated(); + const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } = + useCollectionManager_deprecated(); const inherits = getInheritCollections(name); const { snapshot } = useActionContext(); const form = useForm(); @@ -551,7 +561,9 @@ export const useInheritsFormItemInitializerFields = (options?) => { type: 'string', name: field.name, title: field?.uiSchema?.title || field.name, - 'x-designer': 'FormItem.Designer', + // 'x-designer': 'FormItem.Designer', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', 'x-component': component, 'x-decorator': 'FormItem', 'x-collection-field': `${name}.${field.name}`, @@ -581,8 +593,9 @@ export const useInheritsFormItemInitializerFields = (options?) => { // 筛选表单相关 export const useFilterInheritsFormItemInitializerFields = (options?) => { - const { name } = useCollection(); - const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } = useCollectionManager(); + const { name } = useCollection_deprecated(); + const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } = + useCollectionManager_deprecated(); const inherits = getInheritCollections(name); const { snapshot } = useActionContext(); const form = useForm(); @@ -606,7 +619,9 @@ export const useFilterInheritsFormItemInitializerFields = (options?) => { name: field.name, title: field?.uiSchema?.title || field.name, required: false, - 'x-designer': 'FormItem.FilterFormDesigner', + // 'x-designer': 'FormItem.FilterFormDesigner', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FilterFormItem', 'x-component': 'CollectionField', 'x-decorator': 'FormItem', 'x-collection-field': `${name}.${field.name}`, @@ -634,8 +649,8 @@ export const useFilterInheritsFormItemInitializerFields = (options?) => { }); }; export const useCustomFormItemInitializerFields = (options?: any) => { - const { name, currentFields } = useCollection(); - const { getInterface, getCollection } = useCollectionManager(); + const { name, currentFields } = useCollection_deprecated(); + const { getInterface, getCollection } = useCollectionManager_deprecated(); const form = useForm(); const { readPretty = form.readPretty, block = 'Form' } = options || {}; const remove = useRemoveGridFormItem(); @@ -654,7 +669,9 @@ export const useCustomFormItemInitializerFields = (options?: any) => { type: 'string', name: field.name, title: field?.uiSchema?.title || field.name, - 'x-designer': 'FormItem.Designer', + // 'x-designer': 'FormItem.Designer', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', 'x-component': 'AssignedField', 'x-decorator': 'FormItem', 'x-collection-field': `${name}.${field.name}`, @@ -678,57 +695,6 @@ export const useCustomFormItemInitializerFields = (options?: any) => { }); }; -export const useCustomBulkEditFormItemInitializerFields = (options?: any) => { - const { name, fields } = useCollection(); - const { getInterface, getCollection } = useCollectionManager(); - const form = useForm(); - const { readPretty = form.readPretty, block = 'Form' } = options || {}; - const remove = useRemoveGridFormItem(); - const filterFields = useMemo( - () => - fields - ?.filter((field) => { - return ( - field?.interface && - !field?.uiSchema?.['x-read-pretty'] && - field.interface !== 'snapshot' && - field.type !== 'sequence' - ); - }) - .map((field) => { - const interfaceConfig = getInterface(field.interface); - const schema = { - type: 'string', - name: field.name, - title: field?.uiSchema?.title || field.name, - 'x-designer': 'FormItem.Designer', - 'x-component': 'BulkEditField', - 'x-decorator': 'FormItem', - 'x-collection-field': `${name}.${field.name}`, - }; - return { - name: field?.uiSchema?.title || field.name, - type: 'item', - title: field?.uiSchema?.title || field.name, - Component: 'CollectionFieldInitializer', - remove: remove, - schemaInitialize: (s) => { - interfaceConfig?.schemaInitialize?.(s, { - field, - block, - readPretty, - targetCollection: getCollection(field.target), - }); - }, - schema, - } as SchemaInitializerItemType; - }), - [fields], - ); - - return filterFields; -}; - const findSchema = (schema: Schema, key: string, action: string) => { if (!Schema.isSchemaInstance(schema)) return null; return schema.reduceProperties((buf, s) => { @@ -792,9 +758,9 @@ export const useRecordCollectionDataSourceItems = ( resourceName = null, ) => { const { t } = useTranslation(); - const collection = useCollection(); + const collection = useCollection_deprecated(); const { getTemplatesByCollection } = useSchemaTemplateManager(); - const templates = getTemplatesByCollection(collectionName || collection.name) + const templates = getTemplatesByCollection(collection.dataSource, collectionName || collection.name) .filter((template) => { return componentName && template.componentName === componentName; }) @@ -857,45 +823,35 @@ export const useRecordCollectionDataSourceItems = ( ]; }; -export const useCollectionDataSourceItems = (componentName) => { +export const useCollectionDataSourceItems = ( + componentName, + filter: (collection: Collection) => boolean = () => true, + onlyCurrentDataSource = false, +) => { const { t } = useTranslation(); - const { collections, getCollectionFields } = useCollectionManager(); + const dm = useDataSourceManager(); + const dataSourceKey = useDataSourceKey(); + let allCollections = dm.getAllCollections(filter); + if (onlyCurrentDataSource) { + allCollections = allCollections.filter((collection) => collection.key === dataSourceKey); + } + const { getTemplatesByCollection } = useSchemaTemplateManager(); - - return [ - { - type: 'itemGroup', - title: null, - children: [], - loadChildren: ({ searchValue } = { searchValue: '' }) => { - return getChildren({ - collections, - getCollectionFields, - componentName, - searchValue, - getTemplatesByCollection, - t, - }); - }, - }, - ]; -}; - -export const useCollectionDataSourceItemsV2 = (componentName) => { - const { t } = useTranslation(); - const { collections, getCollectionFields } = useCollectionManager(); - const { getTemplatesByCollection } = useSchemaTemplateManager(); - const res = useMemo(() => { - return getChildren({ - collections, - getCollectionFields, - componentName, - searchValue: '', - getTemplatesByCollection, - t, - }); - }, [collections, componentName, getCollectionFields, getTemplatesByCollection, t]); + return allCollections.map(({ key, displayName, collections }) => ({ + name: key, + label: displayName, + type: 'subMenu', + children: getChildren({ + collections, + componentName, + searchValue: '', + dataSource: key, + getTemplatesByCollection, + t, + }), + })); + }, [allCollections, componentName, dm, getTemplatesByCollection, t]); return res; }; @@ -905,9 +861,11 @@ export const createDetailsBlockSchema = (options) => { formItemInitializers = 'ReadPrettyFormItemInitializers', actionInitializers = 'DetailsActionInitializers', collection, + dataSource, association, resource, template, + settings, ...others } = options; const resourceName = resource || association || collection; @@ -917,6 +875,7 @@ export const createDetailsBlockSchema = (options) => { 'x-decorator': 'DetailsBlockProvider', 'x-decorator-props': { resource: resourceName, + dataSource, collection, association, readPretty: true, @@ -927,7 +886,8 @@ export const createDetailsBlockSchema = (options) => { // useParams: '{{ useParamsFromRecord }}', ...others, }, - 'x-designer': 'DetailsDesigner', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': settings, 'x-component': 'CardItem', properties: { [uid()]: { @@ -975,9 +935,11 @@ export const createListBlockSchema = (options) => { actionInitializers = 'ListActionInitializers', itemActionInitializers = 'ListItemActionInitializers', collection, + dataSource, association, resource, template, + settings, ...others } = options; const resourceName = resource || association || collection; @@ -988,6 +950,7 @@ export const createListBlockSchema = (options) => { 'x-decorator-props': { resource: resourceName, collection, + dataSource, association, readPretty: true, action: 'list', @@ -998,7 +961,8 @@ export const createListBlockSchema = (options) => { ...others, }, 'x-component': 'CardItem', - 'x-designer': 'List.Designer', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': settings, properties: { actionBar: { type: 'void', @@ -1064,6 +1028,8 @@ export const createGridCardBlockSchema = (options) => { association, resource, template, + dataSource, + settings, ...others } = options; const resourceName = resource || association || collection; @@ -1075,6 +1041,7 @@ export const createGridCardBlockSchema = (options) => { resource: resourceName, collection, association, + dataSource, readPretty: true, action: 'list', params: { @@ -1087,7 +1054,8 @@ export const createGridCardBlockSchema = (options) => { 'x-component-props': { useProps: '{{ useGridCardBlockItemProps }}', }, - 'x-designer': 'GridCard.Designer', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': settings, properties: { actionBar: { type: 'void', @@ -1149,12 +1117,14 @@ export const createFormBlockSchema = (options) => { actionInitializers = 'FormActionInitializers', collection, resource, + dataSource, association, action, actions = {}, 'x-designer': designer = 'FormV2.Designer', template, title, + settings, ...others } = options; const resourceName = resource || association || collection; @@ -1168,13 +1138,15 @@ export const createFormBlockSchema = (options) => { 'x-decorator-props': { ...others, action, + dataSource, resource: resourceName, collection, association, // action: 'get', // useParams: '{{ useParamsFromRecord }}', }, - 'x-designer': designer, + 'x-toolbar': 'BlockSchemaToolbar', + ...(settings ? { 'x-settings': settings } : { 'x-designer': designer }), 'x-component': 'CardItem', 'x-component-props': { title, @@ -1219,8 +1191,10 @@ export const createFilterFormBlockSchema = (options) => { collection, resource, association, + dataSource, action, template, + settings, ...others } = options; const resourceName = resource || association || collection; @@ -1231,10 +1205,12 @@ export const createFilterFormBlockSchema = (options) => { ...others, action, resource: resourceName, + dataSource, collection, association, }, - 'x-designer': 'FormV2.FilterDesigner', + 'x-toolbar': 'BlockSchemaToolbar', + ...(settings ? { 'x-settings': settings } : { 'x-designer': 'FormV2.FilterDesigner' }), 'x-component': 'CardItem', // 保存当前筛选区块所能过滤的数据区块 'x-filter-targets': [], @@ -1279,8 +1255,10 @@ export const createReadPrettyFormBlockSchema = (options) => { actionInitializers = 'ReadPrettyFormActionInitializers', collection, association, + dataSource, resource, template, + settings, ...others } = options; const resourceName = resource || association || collection; @@ -1292,12 +1270,14 @@ export const createReadPrettyFormBlockSchema = (options) => { resource: resourceName, collection, association, + dataSource, readPretty: true, action: 'get', useParams: '{{ useParamsFromRecord }}', ...others, }, - 'x-designer': 'FormV2.ReadPrettyDesigner', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': settings, 'x-component': 'CardItem', properties: { [uid()]: { @@ -1343,7 +1323,7 @@ export const createTableBlockSchema = (options) => { tableActionColumnInitializers, tableBlockProvider, disableTemplate, - TableBlockDesigner, + dataSource, blockType, pageSize = 20, ...others @@ -1354,6 +1334,7 @@ export const createTableBlockSchema = (options) => { 'x-acl-action': `${resource || collection}:list`, 'x-decorator-props': { collection, + dataSource, resource: resource || collection, action: 'list', params: { @@ -1366,7 +1347,8 @@ export const createTableBlockSchema = (options) => { blockType, ...others, }, - 'x-designer': TableBlockDesigner ?? 'TableBlockDesigner', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', 'x-component': 'CardItem', 'x-filter-targets': [], properties: { @@ -1422,26 +1404,28 @@ export const createTableBlockSchema = (options) => { }; export const createCollapseBlockSchema = (options) => { - const { collection, blockType } = options; + const { collection, dataSource, blockType } = options; const schema: ISchema = { type: 'void', 'x-decorator': 'AssociationFilter.Provider', 'x-decorator-props': { collection, + dataSource, blockType, associationFilterStyle: { width: '100%', }, name: 'filter-collapse', }, - 'x-designer': 'AssociationFilter.BlockDesigner', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:filterCollapse', 'x-component': 'CardItem', 'x-filter-targets': [], properties: { [uid()]: { type: 'void', 'x-action': 'associateFilter', - 'x-initializer': 'AssociationFilter.FilterBlockInitializer', + 'x-initializer': 'AssociationFilterInitializers', 'x-component': 'AssociationFilter', properties: {}, }, @@ -1452,7 +1436,7 @@ export const createCollapseBlockSchema = (options) => { }; export const createTableSelectorSchema = (options) => { - const { collection, resource, rowKey, ...others } = options; + const { collection, dataSource, resource, rowKey, ...others } = options; const schema: ISchema = { type: 'void', 'x-acl-action': `${resource || collection}:list`, @@ -1460,6 +1444,7 @@ export const createTableSelectorSchema = (options) => { 'x-decorator-props': { collection, resource: resource || collection, + dataSource, action: 'list', params: { pageSize: 20, @@ -1467,7 +1452,8 @@ export const createTableSelectorSchema = (options) => { rowKey, ...others, }, - 'x-designer': 'TableSelectorDesigner', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:tableSelector', 'x-component': 'CardItem', properties: { [uid()]: { @@ -1495,23 +1481,22 @@ export const createTableSelectorSchema = (options) => { }, }, }; - console.log(JSON.stringify(schema, null, 2)); return schema; }; const getChildren = ({ collections, - getCollectionFields, + dataSource, componentName, searchValue, getTemplatesByCollection, t, }: { collections: any[]; - getCollectionFields: (name: any) => CollectionFieldOptions[]; componentName: string; searchValue: string; - getTemplatesByCollection: (collectionName: string, resourceName?: string) => any; + dataSource: string; + getTemplatesByCollection: (dataSource: string, collectionName: string, resourceName?: string) => any; t; }) => { return collections @@ -1519,8 +1504,7 @@ const getChildren = ({ if (item.inherit) { return false; } - const fields = getCollectionFields(item.name); - if (item.autoGenId === false && !fields.find((v) => v.primaryKey)) { + if (!item.filterTargetKey) { return false; } else if ( ['Kanban', 'FormItem'].includes(componentName) && @@ -1530,14 +1514,16 @@ const getChildren = ({ } else if (item.template === 'file' && ['Kanban', 'FormItem', 'Calendar'].includes(componentName)) { return false; } else { - if (!item.title) { + const title = item.title || item.tableName; + if (!title) { return false; } - return item.title.toUpperCase().includes(searchValue.toUpperCase()) && !(item?.isThrough && item?.autoCreate); + return title.toUpperCase().includes(searchValue.toUpperCase()) && !(item?.isThrough && item?.autoCreate); } }) ?.map((item, index) => { - const templates = getTemplatesByCollection(item.name).filter((template) => { + const title = item.title || item.tableName || item.label; + const templates = getTemplatesByCollection(dataSource, item.name).filter((template) => { return ( componentName && template.componentName === componentName && @@ -1548,18 +1534,21 @@ const getChildren = ({ return { type: 'item', name: item.name, - title: item.title, + title, + dataSource, }; } return { key: `${componentName}_table_subMenu_${index}`, type: 'subMenu', name: `${item.name}_${index}`, - title: item.title, + title, + dataSource, children: [ { type: 'item', name: item.name, + dataSource, title: t('Blank block'), }, { @@ -1569,6 +1558,7 @@ const getChildren = ({ key: `${componentName}_table_subMenu_${index}_copy`, type: 'subMenu', name: 'copy', + dataSource, title: t('Duplicate template'), children: templates.map((template) => { const templateName = @@ -1578,6 +1568,7 @@ const getChildren = ({ mode: 'copy', name: item.name, template, + dataSource, title: templateName || t('Untitled'), }; }), @@ -1586,6 +1577,7 @@ const getChildren = ({ key: `${componentName}_table_subMenu_${index}_ref`, type: 'subMenu', name: 'ref', + dataSource, title: t('Reference template'), children: templates.map((template) => { const templateName = @@ -1595,6 +1587,7 @@ const getChildren = ({ mode: 'reference', name: item.name, template, + dataSource, title: templateName || t('Untitled'), }; }), diff --git a/packages/core/client/src/schema-items/GeneralSchemaItems.tsx b/packages/core/client/src/schema-items/GeneralSchemaItems.tsx index 425dd1284..02a541ce4 100644 --- a/packages/core/client/src/schema-items/GeneralSchemaItems.tsx +++ b/packages/core/client/src/schema-items/GeneralSchemaItems.tsx @@ -3,7 +3,7 @@ import { ISchema, observer, useField, useFieldSchema } from '@formily/react'; import _ from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useCollection, useCollectionManager } from '../collection-manager'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../collection-manager'; import { useDesignable } from '../schema-component'; import { getTempFieldState } from '../schema-component/antd/form-v2/utils'; import { SchemaSettingsModalItem, SchemaSettingsSwitchItem } from '../schema-settings'; @@ -13,8 +13,8 @@ export const GeneralSchemaItems: React.FC<{ }> = observer( (props) => { const { required = true } = props; - const { getCollectionJoinField } = useCollectionManager(); - const { getField } = useCollection(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); const field = useField(); const fieldSchema = useFieldSchema(); const { t } = useTranslation(); diff --git a/packages/core/client/src/schema-items/GeneralSettings.tsx b/packages/core/client/src/schema-items/GeneralSettings.tsx index 2dd72c3f9..d576a87e7 100644 --- a/packages/core/client/src/schema-items/GeneralSettings.tsx +++ b/packages/core/client/src/schema-items/GeneralSettings.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { useDesignable } from '../schema-component'; import { SchemaSettingOptions } from '../application'; import { useSchemaToolbar } from '../application/schema-toolbar'; -import { useCollection, useCollectionManager } from '../collection-manager'; +import { useCollection_deprecated, useCollectionManager_deprecated } from '../collection-manager'; export const generalSettingsItems: SchemaSettingOptions['items'] = [ { @@ -15,8 +15,8 @@ export const generalSettingsItems: SchemaSettingOptions['items'] = [ const { dn } = useDesignable(); const field = useField(); const fieldSchema = useFieldSchema(); - const { getCollectionJoinField } = useCollectionManager(); - const { getField } = useCollection(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); @@ -53,8 +53,8 @@ export const generalSettingsItems: SchemaSettingOptions['items'] = [ }, useVisible() { const fieldSchema = useFieldSchema(); - const { getCollectionJoinField } = useCollectionManager(); - const { getField } = useCollection(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); return !!collectionField; diff --git a/packages/core/client/src/schema-settings/DataTemplates/FormDataTemplates.tsx b/packages/core/client/src/schema-settings/DataTemplates/FormDataTemplates.tsx index 6b1a8a16d..b823962e2 100644 --- a/packages/core/client/src/schema-settings/DataTemplates/FormDataTemplates.tsx +++ b/packages/core/client/src/schema-settings/DataTemplates/FormDataTemplates.tsx @@ -5,7 +5,7 @@ import { Tree as AntdTree } from 'antd'; import _ from 'lodash'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useCollectionManager } from '../../collection-manager'; +import { useCollectionManager_deprecated } from '../../collection-manager'; import { mergeFilter } from '../../filter-provider/utils'; import { SchemaComponent, SchemaComponentContext, removeNullCondition } from '../../schema-component'; import { ITemplate } from '../../schema-component/antd/form-v2/Templates'; @@ -50,7 +50,7 @@ export const FormDataTemplates = observer( getScopeDataSource, useTitleFieldDataSource, } = useCollectionState(collectionName); - const { getCollection, getCollectionField } = useCollectionManager(); + const { getCollection, getCollectionField } = useCollectionManager_deprecated(); const { t } = useTranslation(); // 不要在后面的数组中依赖 defaultValues,否则会因为 defaultValues 的变化导致 activeData 响应性丢失 const activeData = useMemo( diff --git a/packages/core/client/src/schema-settings/DataTemplates/components/Designer.tsx b/packages/core/client/src/schema-settings/DataTemplates/components/Designer.tsx index 595313b9d..c37fa00fc 100644 --- a/packages/core/client/src/schema-settings/DataTemplates/components/Designer.tsx +++ b/packages/core/client/src/schema-settings/DataTemplates/components/Designer.tsx @@ -5,16 +5,17 @@ import { Select } from 'antd'; import _ from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { GeneralSchemaDesigner, SchemaSettingsDataScope, SchemaSettingsItem } from '../..'; +import { GeneralSchemaDesigner, SchemaSettingsItem } from '../..'; import { useFormBlockContext } from '../../../block-provider'; -import { useCollectionManager } from '../../../collection-manager'; +import { useCollectionManager_deprecated } from '../../../collection-manager'; import { mergeFilter } from '../../../filter-provider/utils'; import { removeNullCondition, useCompile, useDesignable } from '../../../schema-component'; import { ITemplate } from '../../../schema-component/antd/form-v2/Templates'; +import { SchemaSettingsDataScope } from '../../SchemaSettingsDataScope'; export const Designer = observer( () => { - const { getCollectionFields, getCollectionField, getCollection, isTitleField } = useCollectionManager(); + const { getCollectionFields, getCollectionField, getCollection, isTitleField } = useCollectionManager_deprecated(); const field = useField(); const fieldSchema = useFieldSchema(); const { t } = useTranslation(); diff --git a/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts b/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts index 5beb3fc98..894841639 100644 --- a/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts +++ b/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts @@ -1,7 +1,7 @@ import { ArrayField } from '@formily/core'; import { useField } from '@formily/react'; import React, { useCallback, useState } from 'react'; -import { useCollectionManager } from '../../../collection-manager'; +import { useCollectionManager_deprecated } from '../../../collection-manager'; import { useCompile } from '../../../schema-component'; import { TreeNode } from '../TreeLabel'; @@ -19,7 +19,8 @@ export const systemKeys = [ 'sequence', ]; export const useCollectionState = (currentCollectionName: string) => { - const { getCollectionFields, getAllCollectionsInheritChain, getCollection, getInterface } = useCollectionManager(); + const { getCollectionFields, getAllCollectionsInheritChain, getCollection, getInterface } = + useCollectionManager_deprecated(); const [collectionList] = useState(getCollectionList); const compile = useCompile(); const templateField: any = useField(); diff --git a/packages/core/client/src/schema-settings/DataTemplates/utils.tsx b/packages/core/client/src/schema-settings/DataTemplates/utils.tsx index 96620b959..2eba4e047 100644 --- a/packages/core/client/src/schema-settings/DataTemplates/utils.tsx +++ b/packages/core/client/src/schema-settings/DataTemplates/utils.tsx @@ -4,14 +4,14 @@ import { message } from 'antd'; import React, { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { getAssociationPath } from '../../block-provider/hooks'; -import { useCollectionManager } from '../../collection-manager'; +import { useCollectionManager_deprecated } from '../../collection-manager'; import { useCompile } from '../../schema-component'; import { TreeNode } from './TreeLabel'; import { systemKeys } from './hooks/useCollectionState'; import LRUCache from 'lru-cache'; export const useSyncFromForm = (fieldSchema, collection?, callBack?) => { - const { getCollectionJoinField, getCollectionFields } = useCollectionManager(); + const { getCollectionJoinField, getCollectionFields } = useCollectionManager_deprecated(); const array = ArrayBase.useArray(); const index = ArrayBase.useIndex(); const record = ArrayBase.useRecord(); diff --git a/packages/core/client/src/schema-settings/EnableChildCollections/DynamicComponent.tsx b/packages/core/client/src/schema-settings/EnableChildCollections/DynamicComponent.tsx index 1c58e7000..1f280b034 100644 --- a/packages/core/client/src/schema-settings/EnableChildCollections/DynamicComponent.tsx +++ b/packages/core/client/src/schema-settings/EnableChildCollections/DynamicComponent.tsx @@ -3,22 +3,28 @@ import { observer, useFieldSchema } from '@formily/react'; import React, { useEffect, useMemo } from 'react'; import { useCompile } from '../../schema-component'; import { Variable } from '.././../schema-component'; -import { useFormVariable } from '../VariableInput/hooks/useFormVariable'; -import { useIterationVariable } from '../VariableInput/hooks/useIterationVariable'; +import { useCurrentFormVariable } from '../VariableInput/hooks/useFormVariable'; +import { useCurrentObjectVariable } from '../VariableInput/hooks/useIterationVariable'; export const ChildDynamicComponent = observer( (props: { rootCollection: string; onChange; value; default; collectionField }) => { const { rootCollection, onChange, value, collectionField } = props; const fieldSchema = useFieldSchema(); - const formVariable = useFormVariable({ collectionName: rootCollection, collectionField }); - const iterationVariable = useIterationVariable({ + const { currentFormSettings } = useCurrentFormVariable({ + collectionName: rootCollection, + collectionField, + }); + const { currentObjectSettings } = useCurrentObjectVariable({ currentCollection: collectionField?.collectionName, schema: collectionField?.uiSchema, collectionField, }); const compile = useCompile(); - const result = useMemo(() => [formVariable, iterationVariable].filter(Boolean), [formVariable, iterationVariable]); + const result = useMemo( + () => [currentFormSettings, currentObjectSettings].filter(Boolean), + [currentFormSettings, currentObjectSettings], + ); const scope = compile(result); useEffect(() => { onChange(fieldSchema.default); diff --git a/packages/core/client/src/schema-settings/EnableChildCollections/index.tsx b/packages/core/client/src/schema-settings/EnableChildCollections/index.tsx index b3c34b555..b15fcf12d 100644 --- a/packages/core/client/src/schema-settings/EnableChildCollections/index.tsx +++ b/packages/core/client/src/schema-settings/EnableChildCollections/index.tsx @@ -1,7 +1,7 @@ import { observer, useForm } from '@formily/react'; import { action } from '@formily/reactive'; import React from 'react'; -import { useCollectionManager } from '../../collection-manager'; +import { useCollectionManager_deprecated } from '../../collection-manager'; import { SchemaComponent, useCompile } from '../../schema-component'; export const EnableChildCollections = observer( @@ -10,7 +10,7 @@ export const EnableChildCollections = observer( const { defaultValues, collectionName } = useProps(); const form = useForm(); const compile = useCompile(); - const { getChildrenCollections } = useCollectionManager(); + const { getChildrenCollections } = useCollectionManager_deprecated(); const childrenCollections = getChildrenCollections(collectionName); const useAsyncDataSource = (service: any) => { diff --git a/packages/core/client/src/schema-settings/GeneralSchemaDesigner.tsx b/packages/core/client/src/schema-settings/GeneralSchemaDesigner.tsx index 0128321a8..0b3b963d0 100644 --- a/packages/core/client/src/schema-settings/GeneralSchemaDesigner.tsx +++ b/packages/core/client/src/schema-settings/GeneralSchemaDesigner.tsx @@ -6,6 +6,8 @@ import classNames from 'classnames'; import React, { FC, useEffect, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { SchemaToolbarProvider, useSchemaInitializerRender, useSchemaSettingsRender } from '../application'; +import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider'; +import { useDataSource } from '../data-source/data-source/DataSourceProvider'; import { DragHandler, useCompile, useDesignable, useGridContext, useGridRowContext } from '../schema-component'; import { gridRowColWrap } from '../schema-initializer/utils'; import { SchemaSettingsDropdown } from './SchemaSettings'; @@ -56,6 +58,9 @@ export interface GeneralSchemaDesignerProps { draggable?: boolean; } +/** + * @deprecated use `SchemaToolbar` instead + */ export const GeneralSchemaDesigner: FC = (props: any) => { const { disableInitializer, title, template, schemaSettings, contextValue, draggable = true } = props; const { dn, designable } = useDesignable(); @@ -75,6 +80,10 @@ export const GeneralSchemaDesigner: FC = (props: any ); const rowCtx = useGridRowContext(); const ctx = useGridContext(); + const dm = useDataSourceManager(); + const dataSources = dm?.getDataSources(); + const dataSourceContext = useDataSource(); + const dataSource = dataSources?.length > 1 && dataSourceContext; const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName) ? `${template?.name} ${t('(Fields only)')}` : template?.name; @@ -96,14 +105,15 @@ export const GeneralSchemaDesigner: FC = (props: any if (!designable) { return null; } - return (
{title && (
- {compile(title)} + + {dataSource ? `${compile(dataSource?.displayName)} > ${compile(title)}` : compile(title)} + {template && ( {t('Reference template')}: {templateName || t('Untitled')} @@ -165,7 +175,12 @@ const InternalSchemaToolbar: FC = (props) => { const { designable } = useDesignable(); const fieldSchema = useFieldSchema(); const compile = useCompile(); + const { styles } = useStyles(); const { getAriaLabel } = useGetAriaLabelOfDesigner(); + const dm = useDataSourceManager(); + const dataSources = dm?.getDataSources(); + const dataSourceContext = useDataSource(); + const dataSource = dataSources?.length > 1 && dataSourceContext; const titleArr = useMemo(() => { if (!title) return undefined; @@ -223,13 +238,12 @@ const InternalSchemaToolbar: FC = (props) => { const settingsElement = useMemo(() => { return settings !== false && schemaSettingsExists ? schemaSettingsRender() : null; }, [schemaSettingsExists, schemaSettingsRender, settings]); - const { styles } = useStyles(); const toolbarRef = useRef(null); useEffect(() => { const toolbarElement = toolbarRef.current; - + const parentElement = toolbarElement?.parentElement; function show() { if (toolbarElement) { toolbarElement.style.display = 'block'; @@ -242,15 +256,20 @@ const InternalSchemaToolbar: FC = (props) => { } } - if (toolbarElement?.parentElement) { - toolbarElement.parentElement.addEventListener('mouseenter', show); - toolbarElement.parentElement.addEventListener('mouseleave', hide); + if (parentElement) { + const style = window.getComputedStyle(parentElement); + if (style.position === 'static') { + parentElement.style.position = 'relative'; + } + + parentElement.addEventListener('mouseenter', show); + parentElement.addEventListener('mouseleave', hide); } return () => { - if (toolbarElement?.parentElement) { - toolbarElement.parentElement.removeEventListener('mouseenter', show); - toolbarElement.parentElement.removeEventListener('mouseleave', hide); + if (parentElement) { + parentElement.removeEventListener('mouseenter', show); + parentElement.removeEventListener('mouseleave', hide); } }; }, []); @@ -270,7 +289,7 @@ const InternalSchemaToolbar: FC = (props) => { {titleArr.map((item) => ( - {item} + {dataSource ? `${compile(dataSource?.displayName)} > ${item}` : item} ))} diff --git a/packages/core/client/src/schema-settings/LinkageRules/Variables.tsx b/packages/core/client/src/schema-settings/LinkageRules/Variables.tsx index 0ea7b5d62..56616e5cc 100644 --- a/packages/core/client/src/schema-settings/LinkageRules/Variables.tsx +++ b/packages/core/client/src/schema-settings/LinkageRules/Variables.tsx @@ -1,5 +1,5 @@ import { useCompile } from '../../schema-component'; -import { useCollectionManager, useCollection } from '../../collection-manager'; +import { useCollectionManager_deprecated, useCollection_deprecated } from '../../collection-manager'; const supportsType = [ 'id', @@ -26,7 +26,7 @@ const supportsType = [ 'm2m', ]; const useVariableTypes = (currentCollection, excludes = []) => { - const { getCollectionFields, getInterface, getCollection } = useCollectionManager(); + const { getCollectionFields, getInterface, getCollection } = useCollectionManager_deprecated(); const collection = getCollection(currentCollection); const fields = getCollectionFields(currentCollection); return [ diff --git a/packages/core/client/src/schema-settings/LinkageRules/action-hooks.ts b/packages/core/client/src/schema-settings/LinkageRules/action-hooks.ts index 4193348b5..e2303b6b3 100644 --- a/packages/core/client/src/schema-settings/LinkageRules/action-hooks.ts +++ b/packages/core/client/src/schema-settings/LinkageRules/action-hooks.ts @@ -1,9 +1,9 @@ import { useTranslation } from 'react-i18next'; -import { useCollectionManager } from '../../collection-manager'; +import { useCollectionManager_deprecated } from '../../collection-manager'; import { ActionType } from './type'; export const useLinkageCollectionFieldOptions = (collectionName: string) => { - const { getCollectionFields, getInterface } = useCollectionManager(); + const { getCollectionFields, getInterface } = useCollectionManager_deprecated(); const fields = getCollectionFields(collectionName).filter((v) => { return !['id', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy'].includes(v.name); }); diff --git a/packages/core/client/src/schema-settings/LinkageRules/index.tsx b/packages/core/client/src/schema-settings/LinkageRules/index.tsx index 20339674e..8ebf4f373 100644 --- a/packages/core/client/src/schema-settings/LinkageRules/index.tsx +++ b/packages/core/client/src/schema-settings/LinkageRules/index.tsx @@ -3,7 +3,8 @@ import { Form } from '@formily/core'; import { observer, useFieldSchema } from '@formily/react'; import React, { useMemo } from 'react'; import { FormBlockContext } from '../../block-provider'; -import { useCollectionManager } from '../../collection-manager'; +import { useCollectionManager_deprecated } from '../../collection-manager'; +import { useCollectionParentRecordData } from '../../data-source/collection-record/CollectionRecordProvider'; import { RecordProvider } from '../../record-provider'; import { SchemaComponent } from '../../schema-component'; import { DynamicComponentProps } from '../../schema-component/antd/filter/DynamicComponent'; @@ -39,7 +40,8 @@ export const FormLinkageRules = observer( const { useProps, dynamicComponent } = props; const { options, defaultValues, collectionName, form, formBlockType, variables, localVariables, record } = useProps(); - const { getAllCollectionsInheritChain } = useCollectionManager(); + const { getAllCollectionsInheritChain } = useCollectionManager_deprecated(); + const parentRecordData = useCollectionParentRecordData(); const components = useMemo(() => ({ ArrayCollapse }), []); const schema = useMemo( @@ -166,7 +168,7 @@ export const FormLinkageRules = observer( return ( - + diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx index 9d90feac3..f8231da19 100644 --- a/packages/core/client/src/schema-settings/SchemaSettings.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx @@ -40,13 +40,13 @@ import { Router } from 'react-router-dom'; import { APIClientProvider, ActionContextProvider, - CollectionFieldOptions, - CollectionManagerContext, - CollectionProvider, - DatePickerProvider, + AssociationOrCollectionProvider, + CollectionFieldOptions_deprecated, + DataSourceApplicationProvider, Designable, FormDialog, FormProvider, + CollectionRecordProvider, RemoteSchemaComponent, SchemaComponent, SchemaComponentContext, @@ -54,28 +54,36 @@ import { createDesignable, findFormBlock, useAPIClient, - useActionContext, useBlockRequestContext, - useCollection, - useCollectionManager, + useCollectionManager_deprecated, + useCollection_deprecated, useCompile, + useDataBlockProps, useDesignable, useFilterBlock, useGlobalTheme, useLinkageCollectionFilterOptions, + useCollectionRecord, useRecord, useSchemaSettingsItem, useSortFields, } from '..'; -import { BlockRequestContext, useFormBlockContext, useFormBlockType, useTableBlockContext } from '../block-provider'; +import { + BlockRequestContext_deprecated, + useFormBlockContext, + useFormBlockType, + useTableBlockContext, +} from '../block-provider'; import { FormActiveFieldsProvider, findFilterTargets, updateFilterTargets, useFormActiveFields, } from '../block-provider/hooks'; -import { useCollectionFilterOptionsV2 } from '../collection-manager/action-hooks'; import { SelectWithTitle, SelectWithTitleProps } from '../common/SelectWithTitle'; +import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider'; +import { useDataSourceKey } from '../data-source/data-source/DataSourceProvider'; +import { useNiceDropdownMaxHeight } from '../common/useNiceDropdownHeight'; import { FilterBlockType, getSupportFieldsByAssociation, @@ -83,25 +91,18 @@ import { isSameCollection, useSupportedBlocks, } from '../filter-provider/utils'; -import { FlagProvider, useFlag } from '../flag-provider'; +import { FlagProvider } from '../flag-provider'; import { useCollectMenuItem, useCollectMenuItems, useMenuItem } from '../hooks/useMenuItem'; +import { SubFormProvider, useSubFormValue } from '../schema-component/antd/association-field/hooks'; import { getTargetKey } from '../schema-component/antd/association-filter/utilts'; -import { DynamicComponentProps } from '../schema-component/antd/filter/DynamicComponent'; import { useSchemaTemplateManager } from '../schema-templates'; import { useBlockTemplateContext } from '../schema-templates/BlockTemplate'; import { useLocalVariables, useVariables } from '../variables'; -import { isVariable } from '../variables/utils/isVariable'; 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, getShouldChange } from './VariableInput/VariableInput'; -import { BaseVariableProvider, IsDisabledParams } from './VariableInput/hooks/useBaseVariable'; -import { Option } from './VariableInput/type'; -import { formatVariableScop } from './VariableInput/utils/formatVariableScop'; -import { DataScopeProps } from './types'; export interface SchemaSettingsProps { title?: any; @@ -142,7 +143,7 @@ interface SchemaSettingsProviderProps { export const SchemaSettingsProvider: React.FC = (props) => { const { children, fieldSchema, ...others } = props; const { getTemplateBySchema } = useSchemaTemplateManager(); - const { name } = useCollection(); + const { name } = useCollection_deprecated(); const template = getTemplateBySchema(fieldSchema); return ( @@ -156,19 +157,16 @@ export const SchemaSettingsDropdown: React.FC = (props) => const [visible, setVisible] = useState(false); const { Component, getMenuItems } = useMenuItem(); const [, startTransition] = useReactTransition(); + const dropdownMaxHeight = useNiceDropdownMaxHeight([visible]); - const changeMenu: DropdownProps['onOpenChange'] = (nextOpen: boolean, info) => { - // 在 antd v5.8.6 版本中,点击菜单项不会触发菜单关闭,但是升级到 v5.12.2 后会触发关闭。查阅文档发现 - // 在 v5.11.0 版本中增加了一个 info.source,可以通过这个来判断一下,如果是点击的是菜单项就不关闭菜单, - // 这样就可以和之前的行为保持一致了。 - // 下面是模仿官方文档示例做的修改:https://ant.design/components/dropdown-cn + const changeMenu: DropdownProps['onOpenChange'] = useCallback((nextOpen: boolean, info) => { if (info.source === 'trigger' || nextOpen) { // 当鼠标快速滑过时,终止菜单的渲染,防止卡顿 startTransition(() => { setVisible(nextOpen); }); } - }; + }, []); const items = getMenuItems(() => props.children); @@ -177,16 +175,14 @@ export const SchemaSettingsDropdown: React.FC = (props) => { - changeMenu(open, info); - }} + onOpenChange={changeMenu} overlayClassName={css` .ant-dropdown-menu-item-group-list { max-height: 300px; overflow-y: auto; } `} - menu={{ items }} + menu={{ items, style: { maxHeight: dropdownMaxHeight, overflowY: 'auto' } }} >
{typeof title === 'string' ? {title} : title}
@@ -197,7 +193,7 @@ export const SchemaSettingsDropdown: React.FC = (props) => export const SchemaSettingsTemplate = function Template(props) { const { componentName, collectionName, resourceName, needRender } = props; const { t } = useTranslation(); - const { getCollection } = useCollectionManager(); + const { getCollection } = useCollectionManager_deprecated(); const { dn, setVisible, template, fieldSchema } = useSchemaSettings(); const compile = useCompile(); const api = useAPIClient(); @@ -233,7 +229,7 @@ export const SchemaSettingsTemplate = function Template(props) { title="Save as template" onClick={async () => { setVisible(false); - const { title } = collectionName ? getCollection(collectionName) : { title: '' }; + const collection = collectionName && getCollection(collectionName); const values = await FormDialog( t('Save as template'), () => { @@ -247,7 +243,9 @@ export const SchemaSettingsTemplate = function Template(props) { name: { title: t('Template name'), required: true, - default: title ? `${compile(title)}_${t(componentName)}` : t(componentName), + default: collection + ? `${compile(collection?.title || collection?.name)}_${t(componentName)}` + : t(componentName), 'x-decorator': 'FormItem', 'x-component': 'Input', }, @@ -270,6 +268,7 @@ export const SchemaSettingsTemplate = function Template(props) { collectionName, resourceName, componentName, + dataSourceKey: collection.dataSource, name: values.name, uid: fieldSchema['x-uid'], }); @@ -326,7 +325,7 @@ export const SchemaSettingsFormItemTemplate = function FormItemTemplate(props) { const { insertAdjacentPosition = 'afterBegin', componentName, collectionName, resourceName } = props; const { t } = useTranslation(); const compile = useCompile(); - const { getCollection } = useCollectionManager(); + const { getCollection } = useCollectionManager_deprecated(); const { dn, setVisible, template, fieldSchema } = useSchemaSettings(); const api = useAPIClient(); const { saveAsTemplate, copyTemplateSchema } = useSchemaTemplateManager(); @@ -378,7 +377,7 @@ export const SchemaSettingsFormItemTemplate = function FormItemTemplate(props) { title="Save as block template" onClick={async () => { setVisible(false); - const { title } = getCollection(collectionName); + const collection = collectionName && getCollection(collectionName); const gridSchema = findGridSchema(fieldSchema); const values = await FormDialog( t('Save as template'), @@ -397,7 +396,11 @@ export const SchemaSettingsFormItemTemplate = function FormItemTemplate(props) { name: { title: t('Template name'), required: true, - default: `${compile(title)}_${componentTitle[componentName] || componentName}`, + default: collection + ? `${compile(collection?.title || collection?.name)}_${t( + componentTitle[componentName] || componentName, + )}` + : t(componentTitle[componentName] || componentName), 'x-decorator': 'FormItem', 'x-component': 'Input', }, @@ -597,13 +600,13 @@ export const SchemaSettingsConnectDataBlocks: FC = (props) ...others } = props; const options = useContext(SchemaOptionsContext); - const cm = useContext(CollectionManagerContext); - const collection = useCollection(); + const collection = useCollection_deprecated(); const apiClient = useAPIClient(); const { theme } = useGlobalTheme(); - const ctx = useContext(BlockRequestContext); + const ctx = useBlockRequestContext(); const upLevelActiveFields = useFormActiveFields(); const { locale } = useContext(ConfigProvider.ConfigContext); + const dm = useDataSourceManager(); + const dataSourceKey = useDataSourceKey(); + const record = useCollectionRecord(); + const { association } = useDataBlockProps() || {}; + + // 解决变量`当前对象`值在弹窗中丢失的问题 + const { formValue: subFormValue } = useSubFormValue(); if (hidden) { return null; @@ -975,38 +984,46 @@ export const SchemaSettingsModalItem: FC = (props) { title: schema.title || title, width }, () => { return ( - - - - - - - 576px - @media (min-width: 576px) { - min-width: 520px; - } - - // screen <= 576px - @media (max-width: 576px) { - min-width: 320px; - } - `} + + + + + + + - - - - - - - - - - - - + + 576px + @media (min-width: 576px) { + min-width: 520px; + } + + // screen <= 576px + @media (max-width: 576px) { + min-width: 320px; + } + `} + > + + + + + + + + + + + + + + ); }, theme, @@ -1078,7 +1095,7 @@ export const SchemaSettingsDefaultSortingRules = function DefaultSortingRules(pr const fieldSchema = useFieldSchema(); const field = useField(); const title = props.title || t('Set default sorting rules'); - const { name } = useCollection(); + const { name } = useCollection_deprecated(); const defaultSort = get(fieldSchema, path) || []; const sort = defaultSort?.map((item: string) => { return item.startsWith('-') @@ -1348,7 +1365,7 @@ export const SchemaSettingsEnableChildCollections = function EnableChildCollecti const { dn } = useDesignable(); const { t } = useTranslation(); const allowAddToCurrent = fieldSchema?.['x-allow-add-to-current']; - const { getCollectionJoinField } = useCollectionManager(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); const ctx = useBlockRequestContext(); const collectionField = getCollectionJoinField(fieldSchema?.parent?.['x-collection-field']) || {}; const isAssocationAdd = fieldSchema?.parent?.['x-component'] === 'CollectionField'; @@ -1428,155 +1445,7 @@ export const SchemaSettingsEnableChildCollections = function EnableChildCollecti ); }; -export const SchemaSettingsDateFormat = function DateFormatConfig(props: { fieldSchema: Schema }) { - const { fieldSchema } = props; - const field = useField(); - 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 ( - { - 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']; - //子表格/表格区块 - const parts = (field.path.entire as string).split('.'); - parts.pop(); - const modifiedString = parts.join('.'); - field.query(`${modifiedString}.*[0:].${fieldSchema.name}`).forEach((f) => { - f.setComponentProps({ ...data }); - }); - dn.emit('patch', { - schema, - }); - dn.refresh(); - }} - /> - ); -}; - -const defaultInputStyle = css` +export const defaultInputStyle = css` & > .nb-form-item { flex: 1; } @@ -1592,425 +1461,51 @@ export const findParentFieldSchema = (fieldSchema: Schema) => { } }; -export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props: { fieldSchema?: Schema }) { - const currentSchema = useFieldSchema(); - const fieldSchema = props?.fieldSchema ?? currentSchema; - const field: Field = useField(); - const { dn } = useDesignable(); +export const SchemaSettingsSortField = () => { + const { fields } = useCollection_deprecated(); + const field = useField(); + const fieldSchema = useFieldSchema(); const { t } = useTranslation(); - const actionCtx = useActionContext(); - let targetField; + const { dn } = useDesignable(); + const compile = useCompile(); + const { service } = useTableBlockContext(); - const { getField } = useCollection(); - const { getCollectionJoinField, getCollectionFields, getAllCollectionsInheritChain } = useCollectionManager(); - const variables = useVariables(); - const localVariables = useLocalVariables(); - const collection = useCollection(); - const record = useRecord(); - const { form } = useFormBlockContext(); - const { getFields } = useCollectionFilterOptionsV2(collection); - const { isInSubForm, isInSubTable } = useFlag() || {}; - - const { name } = collection; - const collectionField = useMemo( - () => getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']), - [fieldSchema, getCollectionJoinField, getField], - ); - 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 isAllowContextVariable = - actionCtx?.fieldSchema?.['x-action'] === 'customize:create' && - (collectionField?.interface === 'm2m' || - (parentCollectionField?.type === 'hasMany' && collectionField?.interface === 'm2o')); - - const returnScope = useCallback( - (scope: Option[]) => { - const currentForm = scope.find((item) => item.value === '$nForm'); - const fields = getCollectionFields(name); - - // fix https://nocobase.height.app/T-1355 - // 工作流人工节点的 `自定义表单` 区块,与其它表单区块不同,根据它的数据表名称,获取到的字段列表为空,所以需要在这里特殊处理一下 - if (!fields?.length && currentForm) { - currentForm.children = formatVariableScop(getFields()); - } - - return scope; - }, - [getFields, name], - ); - - const DefaultValueComponent: any = useMemo(() => { - return { - ArrayCollapse, - FormLayout, - VariableInput: (props) => { - return ( - - - - ); - }, - }; - }, [isInSubForm, isInSubTable]); - - const schema = useMemo(() => { - return { - type: 'object', - title: t('Set default value'), - properties: { - default: { - 'x-decorator': 'FormItem', - 'x-component': 'VariableInput', - 'x-component-props': { - ...(fieldSchema?.['x-component-props'] || {}), - collectionField, - contextCollectionName: isAllowContextVariable && tableCtx.collection, - schema: collectionField?.uiSchema, - targetFieldSchema: fieldSchema, - className: defaultInputStyle, - form, - record, - returnScope, - shouldChange: getShouldChange({ - collectionField, - variables, - localVariables, - getAllCollectionsInheritChain, - }), - renderSchemaComponent: function Com(props) { - const s = _.cloneDeep(fieldSchemaWithoutRequired) || ({} as Schema); - s.title = ''; - s.name = 'default'; - s['x-read-pretty'] = false; - s['x-disabled'] = false; - - const defaultValue = getFieldDefaultValue(s, collectionField); - - if (collectionField.target && s['x-component-props']) { - s['x-component-props'].mode = 'Select'; - } - - if (collectionField?.uiSchema.type) { - s.type = collectionField.uiSchema.type; - } - - if (collectionField?.uiSchema['x-component'] === 'Checkbox') { - s['x-component-props'].defaultChecked = defaultValue; - - // 在这里如果不设置 type 为 void,会导致设置的默认值不生效 - // 但是我不知道为什么必须要设置为 void ? - s.type = 'void'; - } - - const schema = { - ...(s || {}), - 'x-decorator': 'FormItem', - 'x-component-props': { - ...s['x-component-props'], - collectionName: collectionField?.collectionName, - targetField, - onChange: props.onChange, - defaultValue: isVariable(defaultValue) ? '' : defaultValue, - style: { - width: '100%', - verticalAlign: 'top', - minWidth: '200px', - }, - }, - default: isVariable(defaultValue) ? '' : defaultValue, - } as ISchema; - - return ( - - - - ); - }, - }, - title: t('Default value'), - default: getFieldDefaultValue(fieldSchema, collectionField), - }, - }, - } as ISchema; - }, [ - collectionField, - fieldSchema, - fieldSchemaWithoutRequired, - form, - getAllCollectionsInheritChain, - isAllowContextVariable, - localVariables, - record, - returnScope, - t, - tableCtx.collection, - targetField, - variables, - ]); - const handleSubmit: (values: any) => void = useCallback( - (v) => { - const schema: ISchema = { - ['x-uid']: fieldSchema['x-uid'], - }; - fieldSchema.default = v.default; - if (!v.default && v.default !== 0) { - field.value = null; - } - schema.default = v.default; - dn.emit('patch', { - schema, - currentSchema, - }); - }, - [currentSchema, dn, field, fieldSchema], - ); + const options = fields + .filter((field) => !field?.target && field.interface === 'sort') + .map((field) => ({ + value: field?.name, + label: compile(field?.uiSchema?.title) || field?.name, + })); return ( - - ); -}; - -export const SchemaSettingsSortingRule = function SortRuleConfigure(props) { - const field = useField(); - const { dn } = useDesignable(); - const { t } = useTranslation(); - const currentSchema = useFieldSchema(); - const { getField } = useCollection(); - const { getCollectionJoinField } = useCollectionManager(); - const fieldSchema = props?.fieldSchema ?? currentSchema; - const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); - const sortFields = useSortFields(collectionField?.target); - const defaultSort = fieldSchema['x-component-props']?.service?.params?.sort || []; - const sort = defaultSort?.map((item: string) => { - return item?.startsWith('-') - ? { - field: item.substring(1), - direction: 'desc', - } - : { - field: item, - direction: 'asc', - }; - }); - return ( - { - const sortArr = sort.map((item) => { - return item.direction === 'desc' ? `-${item.field}` : item.field; - }); - _.set(field.componentProps, 'service.params.sort', sortArr); - props?.onSubmitCallBack?.(sortArr); - fieldSchema['x-component-props'] = field.componentProps; + { + fieldSchema['x-decorator-props'].dragSortBy = dragSortBy; + service.run({ ...service.params?.[0], sort: dragSortBy }); + field.decoratorProps.dragSortBy = dragSortBy; dn.emit('patch', { schema: { ['x-uid']: fieldSchema['x-uid'], - 'x-component-props': field.componentProps, + 'x-decorator-props': fieldSchema['x-decorator-props'], }, }); + dn.refresh(); }} /> ); }; -export const SchemaSettingsDataScope: FC = function DataScopeConfigure(props) { - const { t } = useTranslation(); - const { getFields } = useCollectionFilterOptionsV2(props.collectionName); - const record = useRecord(); - const { form } = useFormBlockContext(); - const variables = useVariables(); - const localVariables = useLocalVariables(); - const { getAllCollectionsInheritChain } = useCollectionManager(); - const { isInSubForm, isInSubTable } = useFlag() || {}; - - const dynamicComponent = useCallback( - (props: DynamicComponentProps) => { - return ( - - - - ); - }, - [form, getAllCollectionsInheritChain, localVariables, record, variables], - ); - - const getSchema = () => { - return { - type: 'object', - title: t('Set the data scope'), - properties: { - filter: { - enum: props.collectionFilterOption || getFields(), - 'x-decorator': (props) => ( - - - {props.children} - - - ), - 'x-decorator-props': { - isDisabled, - }, - 'x-component': 'Filter', - 'x-component-props': { - collectionName: props.collectionName, - dynamicComponent: props.dynamicComponent || dynamicComponent, - }, - }, - }, - }; - }; - - return ( - ISchema} - onSubmit={props.onSubmit} - /> - ); -}; - // 是否是系统字段 -export const isSystemField = (collectionField: CollectionFieldOptions, getInterface) => { +export const isSystemField = (collectionField: CollectionFieldOptions_deprecated, getInterface) => { const i = getInterface?.(collectionField?.interface); return i?.group === 'systemInfo'; }; -export const isPatternDisabled = (fieldSchema: Schema) => { - return fieldSchema?.['x-component-props']?.['pattern-disable'] == true; -}; - -function getFieldDefaultValue(fieldSchema: ISchema, collectionField: CollectionFieldOptions) { +export function getFieldDefaultValue(fieldSchema: ISchema, collectionField: CollectionFieldOptions_deprecated) { const result = fieldSchema?.default ?? collectionField?.defaultValue; return result; } - -function isDisabled(params: IsDisabledParams) { - const { option, collectionField, uiSchema } = params; - - if (!uiSchema || !collectionField) { - return true; - } - - // json 类型的字段,允许设置任意类型的值 - if (collectionField.interface === 'json') { - return false; - } - - // 数据范围支持选择 `对多` 、`对一` 的关系字段 - if (option.target) { - return false; - } - - if (['input', 'markdown', 'richText', 'textarea', 'username'].includes(collectionField.interface)) { - return !['string', 'number'].includes(option.schema?.type); - } - - if (collectionField.interface && option.interface) { - return collectionField.interface !== option.interface; - } - - if (uiSchema?.['x-component'] !== option.schema?.['x-component']) { - return true; - } - - return false; -} diff --git a/packages/core/client/src/schema-settings/SchemaSettingsDataScope.tsx b/packages/core/client/src/schema-settings/SchemaSettingsDataScope.tsx new file mode 100644 index 000000000..042393a58 --- /dev/null +++ b/packages/core/client/src/schema-settings/SchemaSettingsDataScope.tsx @@ -0,0 +1,113 @@ +import { ISchema } from '@formily/react'; +import React, { FC, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { DatePickerProvider, useCollectionManager_deprecated, useRecord } from '..'; +import { useFormBlockContext } from '../block-provider'; +import { useCollectionFilterOptionsV2 } from '../collection-manager/action-hooks'; +import { FlagProvider, useFlag } from '../flag-provider'; +import { DynamicComponentProps } from '../schema-component/antd/filter/DynamicComponent'; +import { useLocalVariables, useVariables } from '../variables'; +import { VariableInput, getShouldChange } from './VariableInput/VariableInput'; +import { BaseVariableProvider, IsDisabledParams } from './VariableInput/hooks/useBaseVariable'; +import { DataScopeProps } from './types'; +import { SchemaSettingsModalItem } from './SchemaSettings'; + +export const SchemaSettingsDataScope: FC = function DataScopeConfigure(props) { + const { t } = useTranslation(); + const { getFields } = useCollectionFilterOptionsV2(props.collectionName); + const record = useRecord(); + const { form } = useFormBlockContext(); + const variables = useVariables(); + const localVariables = useLocalVariables(); + const { getAllCollectionsInheritChain } = useCollectionManager_deprecated(); + const { isInSubForm, isInSubTable } = useFlag() || {}; + + const dynamicComponent = useCallback( + (props: DynamicComponentProps) => { + return ( + + + + ); + }, + [form, getAllCollectionsInheritChain, localVariables, record, variables], + ); + + const getSchema = () => { + return { + type: 'object', + title: t('Set the data scope'), + properties: { + filter: { + enum: props.collectionFilterOption || getFields(), + 'x-decorator': (props) => ( + + + {props.children} + + + ), + 'x-decorator-props': { + isDisabled, + }, + 'x-component': 'Filter', + 'x-component-props': { + collectionName: props.collectionName, + dynamicComponent: props.dynamicComponent || dynamicComponent, + }, + }, + }, + }; + }; + + return ( + ISchema} + onSubmit={props.onSubmit} + /> + ); +}; + +function isDisabled(params: IsDisabledParams) { + const { option, collectionField, uiSchema } = params; + + if (!uiSchema || !collectionField) { + return true; + } + + // json 类型的字段,允许设置任意类型的值 + if (collectionField.interface === 'json') { + return false; + } + + // 数据范围支持选择 `对多` 、`对一` 的关系字段 + if (option.target) { + return false; + } + + if (['input', 'markdown', 'richText', 'textarea', 'username'].includes(collectionField.interface)) { + return !['string', 'number'].includes(option.schema?.type); + } + + if (collectionField.interface && option.interface) { + return collectionField.interface !== option.interface; + } + + if (uiSchema?.['x-component'] !== option.schema?.['x-component']) { + return true; + } + + return false; +} diff --git a/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx b/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx new file mode 100644 index 000000000..8e74a6bfa --- /dev/null +++ b/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx @@ -0,0 +1,155 @@ +import { css } from '@emotion/css'; +import { ISchema, Schema, useField } from '@formily/react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useCollectionManager_deprecated, useDesignable } from '..'; +import { DateFormatCom, ExpiresRadio } from './DateFormat/ExpiresRadio'; +import { SchemaSettingsModalItem } from './SchemaSettings'; + +export const SchemaSettingsDateFormat = function DateFormatConfig(props: { fieldSchema: Schema }) { + const { fieldSchema } = props; + const field = useField(); + const { dn } = useDesignable(); + const { t } = useTranslation(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + 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 ( + { + 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']; + //子表格/表格区块 + const parts = (field.path.entire as string).split('.'); + parts.pop(); + const modifiedString = parts.join('.'); + field.query(`${modifiedString}.*[0:].${fieldSchema.name}`).forEach((f) => { + f.setComponentProps({ ...data }); + }); + dn.emit('patch', { + schema, + }); + dn.refresh(); + }} + /> + ); +}; diff --git a/packages/core/client/src/schema-settings/SchemaSettingsDefaultValue.tsx b/packages/core/client/src/schema-settings/SchemaSettingsDefaultValue.tsx new file mode 100644 index 000000000..5be4f99c3 --- /dev/null +++ b/packages/core/client/src/schema-settings/SchemaSettingsDefaultValue.tsx @@ -0,0 +1,223 @@ +import { Field } from '@formily/core'; +import { ISchema, Schema, useField, useFieldSchema } from '@formily/react'; +import _ from 'lodash'; +import React, { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + FormProvider, + SchemaComponent, + useActionContext, + useCollectionManager_deprecated, + useCollection_deprecated, + useDesignable, + useRecord, +} from '..'; +import { useFormBlockContext, useTableBlockContext } from '../block-provider'; +import { useCollectionFilterOptionsV2 } from '../collection-manager/action-hooks'; +import { FlagProvider, useFlag } from '../flag-provider'; +import { useLocalVariables, useVariables } from '../variables'; +import { isVariable } from '../variables/utils/isVariable'; +import { VariableInput, getShouldChange } from './VariableInput/VariableInput'; +import { Option } from './VariableInput/type'; +import { formatVariableScop } from './VariableInput/utils/formatVariableScop'; +import { + findParentFieldSchema, + defaultInputStyle, + getFieldDefaultValue, + SchemaSettingsModalItem, +} from './SchemaSettings'; +import { ArrayCollapse, FormLayout } from '@formily/antd-v5'; + +export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props: { fieldSchema?: Schema }) { + const currentSchema = useFieldSchema(); + const fieldSchema = props?.fieldSchema ?? currentSchema; + const field: Field = useField(); + const { dn } = useDesignable(); + const { t } = useTranslation(); + const actionCtx = useActionContext(); + let targetField; + + const { getField } = useCollection_deprecated(); + const { getCollectionJoinField, getCollectionFields, getAllCollectionsInheritChain } = + useCollectionManager_deprecated(); + const variables = useVariables(); + const localVariables = useLocalVariables(); + const collection = useCollection_deprecated(); + const record = useRecord(); + const { form } = useFormBlockContext(); + const { getFields } = useCollectionFilterOptionsV2(collection); + const { isInSubForm, isInSubTable } = useFlag() || {}; + + const { name } = collection; + const collectionField = useMemo( + () => getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']), + [fieldSchema, getCollectionJoinField, getField], + ); + 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 isAllowContextVariable = + actionCtx?.fieldSchema?.['x-action'] === 'customize:create' && + (collectionField?.interface === 'm2m' || + (parentCollectionField?.type === 'hasMany' && collectionField?.interface === 'm2o')); + + const returnScope = useCallback( + (scope: Option[]) => { + const currentForm = scope.find((item) => item.value === '$nForm'); + const fields = getCollectionFields(name); + + // fix https://nocobase.height.app/T-1355 + // 工作流人工节点的 `自定义表单` 区块,与其它表单区块不同,根据它的数据表名称,获取到的字段列表为空,所以需要在这里特殊处理一下 + if (!fields?.length && currentForm) { + currentForm.children = formatVariableScop(getFields()); + } + + return scope; + }, + [getFields, name], + ); + + const DefaultValueComponent: any = useMemo(() => { + return { + ArrayCollapse, + FormLayout, + VariableInput: (props) => { + return ( + + + + ); + }, + }; + }, [isInSubForm, isInSubTable]); + + const schema = useMemo(() => { + return { + type: 'object', + title: t('Set default value'), + properties: { + default: { + 'x-decorator': 'FormItem', + 'x-component': 'VariableInput', + 'x-component-props': { + ...(fieldSchema?.['x-component-props'] || {}), + collectionField, + contextCollectionName: isAllowContextVariable && tableCtx.collection, + schema: collectionField?.uiSchema, + targetFieldSchema: fieldSchema, + className: defaultInputStyle, + form, + record, + returnScope, + shouldChange: getShouldChange({ + collectionField, + variables, + localVariables, + getAllCollectionsInheritChain, + }), + renderSchemaComponent: function Com(props) { + const s = _.cloneDeep(fieldSchemaWithoutRequired) || ({} as Schema); + s.title = ''; + s.name = 'default'; + s['x-read-pretty'] = false; + s['x-disabled'] = false; + + const defaultValue = getFieldDefaultValue(s, collectionField); + + if (collectionField.target && s['x-component-props']) { + s['x-component-props'].mode = 'Select'; + } + + if (collectionField?.uiSchema.type) { + s.type = collectionField.uiSchema.type; + } + + if (collectionField?.uiSchema['x-component'] === 'Checkbox') { + s['x-component-props'].defaultChecked = defaultValue; + + // 在这里如果不设置 type 为 void,会导致设置的默认值不生效 + // 但是我不知道为什么必须要设置为 void ? + s.type = 'void'; + } + + const schema = { + ...(s || {}), + 'x-decorator': 'FormItem', + 'x-component-props': { + ...s['x-component-props'], + collectionName: collectionField?.collectionName, + targetField, + onChange: props.onChange, + defaultValue: isVariable(defaultValue) ? '' : defaultValue, + style: { + width: '100%', + verticalAlign: 'top', + minWidth: '200px', + }, + }, + default: isVariable(defaultValue) ? '' : defaultValue, + } as ISchema; + + return ( + + + + ); + }, + }, + title: t('Default value'), + default: getFieldDefaultValue(fieldSchema, collectionField), + }, + }, + } as ISchema; + }, [ + collectionField, + fieldSchema, + fieldSchemaWithoutRequired, + form, + getAllCollectionsInheritChain, + isAllowContextVariable, + localVariables, + record, + returnScope, + t, + tableCtx.collection, + targetField, + variables, + ]); + + const handleSubmit: (values: any) => void = useCallback( + (v) => { + const schema: ISchema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema.default = v.default; + if (!v.default && v.default !== 0) { + field.value = null; + } + schema.default = v.default; + dn.emit('patch', { + schema, + currentSchema, + }); + }, + [currentSchema, dn, field, fieldSchema], + ); + + return ( + + ); +}; diff --git a/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts b/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts new file mode 100644 index 000000000..2afec8ee8 --- /dev/null +++ b/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts @@ -0,0 +1,98 @@ +import { Plugin } from '../application/Plugin'; +import { addChildActionSettings } from '../modules/actions/add-child/addChildActionSettings'; +import { addNewActionSettings } from '../modules/actions/add-new/addNewActionSettings'; +import { customizeAddRecordActionSettings } from '../modules/actions/add-record/customizeAddRecordActionSettings'; +import { bulkDeleteActionSettings } from '../modules/actions/bulk-destroy/bulkDeleteActionSettings'; +import { deleteActionSettings } from '../modules/actions/delete/deleteActionSettings'; +import { expendableActionSettings } from '../modules/actions/expand-collapse/expendableActionSettings'; +import { filterActionSettings } from '../modules/actions/filter/filterActionSettings'; +import { refreshActionSettings } from '../modules/actions/refresh/refreshActionSettings'; +import { customizeSaveRecordActionSettings } from '../modules/actions/save-record/customizeSaveRecordActionSettings'; +import { createSubmitActionSettings } from '../modules/actions/submit/createSubmitActionSettings'; +import { submitActionSettings, updateSubmitActionSettings } from '../modules/actions/submit/updateSubmitActionSettings'; +import { customizeUpdateRecordActionSettings } from '../modules/actions/update-record/customizeUpdateRecordActionSettings'; +import { customizePopupActionSettings } from '../modules/actions/view-edit-popup/customizePopupActionSettings'; +import { editActionSettings } from '../modules/actions/view-edit-popup/editActionSettings'; +import { viewActionSettings } from '../modules/actions/view-edit-popup/viewActionSettings'; +import { multiDataDetailsBlockSettings } from '../modules/blocks/data-blocks/details-multi/multiDataDetailsBlockSettings'; +import { singleDataDetailsBlockSettings } from '../modules/blocks/data-blocks/details-single/singleDataDetailsBlockSettings'; +import { + createFormBlockSettings, + creationFormBlockSettings, +} from '../modules/blocks/data-blocks/form/createFormBlockSettings'; +import { editFormBlockSettings } from '../modules/blocks/data-blocks/form/editFormBlockSettings'; +import { fieldSettingsFormItem } from '../modules/blocks/data-blocks/form/fieldSettingsFormItem'; +import { gridCardBlockSettings } from '../modules/blocks/data-blocks/grid-card/gridCardBlockSettings'; +import { listBlockSettings } from '../modules/blocks/data-blocks/list/listBlockSettings'; +import { tableSelectorBlockSettings } from '../modules/blocks/data-blocks/table-selector/tableSelectorBlockSettings'; +import { tableBlockSettings } from '../modules/blocks/data-blocks/table/tableBlockSettings'; +import { tableColumnSettings } from '../modules/blocks/data-blocks/table/tableColumnSettings'; +import { filterCollapseBlockSettings } from '../modules/blocks/filter-blocks/collapse/filterCollapseBlockSettings'; +import { filterCollapseItemFieldSettings } from '../modules/blocks/filter-blocks/collapse/filterCollapseItemFieldSettings'; +import { filterFormBlockSettings } from '../modules/blocks/filter-blocks/form/filterFormBlockSettings'; +import { filterFormItemFieldSettings } from '../modules/blocks/filter-blocks/form/filterFormItemFieldSettings'; +import { markdownBlockSettings } from '../modules/blocks/other-blocks/markdown/markdownBlockSettings'; +import { cascadeSelectComponentFieldSettings } from '../modules/fields/component/CascadeSelect/cascadeSelectComponentFieldSettings'; +import { datePickerComponentFieldSettings } from '../modules/fields/component/DatePicker/datePickerComponentFieldSettings'; +import { fileManagerComponentFieldSettings } from '../modules/fields/component/FileManager/fileManagerComponentFieldSettings'; +import { uploadAttachmentComponentFieldSettings } from '../modules/fields/component/FileManager/uploadAttachmentComponentFieldSettings'; +import { subformComponentFieldSettings } from '../modules/fields/component/Nester/subformComponentFieldSettings'; +import { recordPickerComponentFieldSettings } from '../modules/fields/component/Picker/recordPickerComponentFieldSettings'; +import { subformPopoverComponentFieldSettings } from '../modules/fields/component/PopoverNester/subformPopoverComponentFieldSettings'; +import { selectComponentFieldSettings } from '../modules/fields/component/Select/selectComponentFieldSettings'; +import { subTablePopoverComponentFieldSettings } from '../modules/fields/component/SubTable/subTablePopoverComponentFieldSettings'; +import { tagComponentFieldSettings } from '../modules/fields/component/Tag/tagComponentFieldSettings'; + +export class SchemaSettingsPlugin extends Plugin { + async load() { + // block settings + this.schemaSettingsManager.add(tableBlockSettings); + this.schemaSettingsManager.add(creationFormBlockSettings); + this.schemaSettingsManager.add(createFormBlockSettings); + this.schemaSettingsManager.add(editFormBlockSettings); + this.schemaSettingsManager.add(filterFormBlockSettings); + this.schemaSettingsManager.add(filterFormItemFieldSettings); + this.schemaSettingsManager.add(multiDataDetailsBlockSettings); + this.schemaSettingsManager.add(singleDataDetailsBlockSettings); + this.schemaSettingsManager.add(tableSelectorBlockSettings); + this.schemaSettingsManager.add(listBlockSettings); + this.schemaSettingsManager.add(gridCardBlockSettings); + this.schemaSettingsManager.add(filterCollapseBlockSettings); + this.schemaSettingsManager.add(markdownBlockSettings); + + // action settings + this.schemaSettingsManager.add(addNewActionSettings); + this.schemaSettingsManager.add(filterActionSettings); + this.schemaSettingsManager.add(refreshActionSettings); + this.schemaSettingsManager.add(viewActionSettings); + this.schemaSettingsManager.add(editActionSettings); + this.schemaSettingsManager.add(deleteActionSettings); + this.schemaSettingsManager.add(bulkDeleteActionSettings); + this.schemaSettingsManager.add(customizeAddRecordActionSettings); + this.schemaSettingsManager.add(customizePopupActionSettings); + this.schemaSettingsManager.add(customizeUpdateRecordActionSettings); + this.schemaSettingsManager.add(createSubmitActionSettings); + this.schemaSettingsManager.add(updateSubmitActionSettings); + this.schemaSettingsManager.add(submitActionSettings); + this.schemaSettingsManager.add(customizeSaveRecordActionSettings); + this.schemaSettingsManager.add(addChildActionSettings); + this.schemaSettingsManager.add(expendableActionSettings); + + // field settings + this.schemaSettingsManager.add(fieldSettingsFormItem); + this.schemaSettingsManager.add(tableColumnSettings); + this.schemaSettingsManager.add(filterCollapseItemFieldSettings); + + // field component settings + this.schemaSettingsManager.add(selectComponentFieldSettings); + this.schemaSettingsManager.add(recordPickerComponentFieldSettings); + this.schemaSettingsManager.add(subformComponentFieldSettings); + this.schemaSettingsManager.add(subformPopoverComponentFieldSettings); + this.schemaSettingsManager.add(subTablePopoverComponentFieldSettings); + this.schemaSettingsManager.add(datePickerComponentFieldSettings); + this.schemaSettingsManager.add(fileManagerComponentFieldSettings); + this.schemaSettingsManager.add(tagComponentFieldSettings); + this.schemaSettingsManager.add(cascadeSelectComponentFieldSettings); + this.schemaSettingsManager.add(uploadAttachmentComponentFieldSettings); + } +} diff --git a/packages/core/client/src/schema-settings/SchemaSettingsSortingRule.tsx b/packages/core/client/src/schema-settings/SchemaSettingsSortingRule.tsx new file mode 100644 index 000000000..cc6789672 --- /dev/null +++ b/packages/core/client/src/schema-settings/SchemaSettingsSortingRule.tsx @@ -0,0 +1,123 @@ +import { ISchema, useField, useFieldSchema } from '@formily/react'; +import _ from 'lodash'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useCollectionManager_deprecated, useCollection_deprecated, useDesignable, useSortFields } from '..'; +import { SchemaSettingsModalItem } from './SchemaSettings'; +import { ArrayItems } from '@formily/antd-v5'; + +export const SchemaSettingsSortingRule = function SortRuleConfigure(props) { + const field = useField(); + const { dn } = useDesignable(); + const { t } = useTranslation(); + const currentSchema = useFieldSchema(); + const { getField } = useCollection_deprecated(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const fieldSchema = props?.fieldSchema ?? currentSchema; + const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + const sortFields = useSortFields(collectionField?.target); + const defaultSort = fieldSchema['x-component-props']?.service?.params?.sort || []; + const sort = defaultSort?.map((item: string) => { + return item?.startsWith('-') + ? { + field: item.substring(1), + direction: 'desc', + } + : { + field: item, + direction: 'asc', + }; + }); + return ( + { + const sortArr = sort.map((item) => { + return item.direction === 'desc' ? `-${item.field}` : item.field; + }); + _.set(field.componentProps, 'service.params.sort', sortArr); + props?.onSubmitCallBack?.(sortArr); + fieldSchema['x-component-props'] = field.componentProps; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-component-props': field.componentProps, + }, + }); + }} + /> + ); +}; diff --git a/packages/core/client/src/schema-settings/VariableInput/VariableInput.tsx b/packages/core/client/src/schema-settings/VariableInput/VariableInput.tsx index ea11bb82d..10020e01a 100644 --- a/packages/core/client/src/schema-settings/VariableInput/VariableInput.tsx +++ b/packages/core/client/src/schema-settings/VariableInput/VariableInput.tsx @@ -4,23 +4,23 @@ import { Schema } from '@formily/json-schema'; import _ from 'lodash'; import React, { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { CollectionFieldOptions } from '../../collection-manager'; +import { CollectionFieldOptions_deprecated } from '../../collection-manager'; import { Variable, useVariableScope } from '../../schema-component'; import { useValues } from '../../schema-component/antd/filter/useValues'; import { VariableOption, VariablesContextType } from '../../variables/types'; import { isVariable } from '../../variables/utils/isVariable'; import { useBlockCollection } from './hooks/useBlockCollection'; import { useContextAssociationFields } from './hooks/useContextAssociationFields'; -import { useRecordVariable } from './hooks/useRecordVariable'; -import { useUserVariable } from './hooks/useUserVariable'; +import { useCurrentRecordVariable } from './hooks/useRecordVariable'; +import { useCurrentUserVariable } from './hooks/useUserVariable'; import { useVariableOptions } from './hooks/useVariableOptions'; import { Option } from './type'; interface GetShouldChangeProps { - collectionField: CollectionFieldOptions; + collectionField: CollectionFieldOptions_deprecated; variables: VariablesContextType; localVariables: VariableOption | VariableOption[]; - /** `useCollectionManager` 返回的 */ + /** `useCollectionManager_deprecated` 返回的 */ getAllCollectionsInheritChain: (collectionName: string) => string[]; } @@ -39,7 +39,7 @@ type Props = { children?: any; className?: string; style?: React.CSSProperties; - collectionField: CollectionFieldOptions; + collectionField: CollectionFieldOptions_deprecated; contextCollectionName?: string; /**指定当前表单数据表 */ currentFormCollectionName?: string; @@ -229,21 +229,21 @@ export interface FormatVariableScopeReturn { */ export function useCompatOldVariables(props: { uiSchema: any; - collectionField: CollectionFieldOptions; + collectionField: CollectionFieldOptions_deprecated; blockCollectionName: string; noDisabled?: boolean; targetFieldSchema?: Schema; }) { const { uiSchema, collectionField, noDisabled, targetFieldSchema, blockCollectionName } = props; const { t } = useTranslation(); - const lowLevelUserVariable = useUserVariable({ + const { currentUserSettings } = useCurrentUserVariable({ maxDepth: 1, uiSchema: uiSchema, collectionField, noDisabled, targetFieldSchema, }); - const currentRecordVariable = useRecordVariable({ + const { currentRecordSettings } = useCurrentRecordVariable({ schema: uiSchema, collectionName: blockCollectionName, collectionField, @@ -304,7 +304,7 @@ export function useCompatOldVariables(props: { if (userVariable) { userVariable.value = 'currentUser'; } else { - variables.unshift({ ...lowLevelUserVariable, value: 'currentUser' }); + variables.unshift({ ...currentUserSettings, value: 'currentUser' }); } } @@ -313,7 +313,7 @@ export function useCompatOldVariables(props: { if (formVariable) { formVariable.value = 'currentRecord'; } else { - variables.unshift({ ...currentRecordVariable, value: 'currentRecord' }); + variables.unshift({ ...currentRecordSettings, value: 'currentRecord' }); } } diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useBaseVariable.tsx b/packages/core/client/src/schema-settings/VariableInput/hooks/useBaseVariable.tsx index 0f73e11b7..937a399f2 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useBaseVariable.tsx +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useBaseVariable.tsx @@ -1,21 +1,21 @@ import { ISchema, Schema } from '@formily/json-schema'; import React, { useContext, useMemo } from 'react'; -import { CollectionFieldOptions, useCollectionManager } from '../../../collection-manager'; +import { CollectionFieldOptions_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; import { useCompile, useGetFilterOptions } from '../../../schema-component'; import { isSpecialCaseField } from '../../../schema-component/antd/form-item/hooks/useSpecialCase'; import { FieldOption, Option } from '../type'; export interface IsDisabledParams { option: FieldOption; - collectionField: CollectionFieldOptions; + collectionField: CollectionFieldOptions_deprecated; uiSchema: ISchema; /** 消费变量值的字段 */ targetFieldSchema: Schema; - getCollectionField: (name: string) => CollectionFieldOptions; + getCollectionField: (name: string) => CollectionFieldOptions_deprecated; } interface GetOptionsParams { - collectionField: CollectionFieldOptions; + collectionField: CollectionFieldOptions_deprecated; uiSchema: any; depth: number; /** 消费变量值的字段 */ @@ -28,12 +28,12 @@ interface GetOptionsParams { loadChildren?: (option: Option) => Promise; compile: (value: string) => any; isDisabled?: (params: IsDisabledParams) => boolean; - getCollectionField?: (name: string) => CollectionFieldOptions; + getCollectionField?: (name: string) => CollectionFieldOptions_deprecated; } interface BaseProps { // 当前字段 - collectionField: CollectionFieldOptions; + collectionField: CollectionFieldOptions_deprecated; /** 当前字段的 `uiSchema`,和 `collectionField.uiSchema` 不同,该值也包含操作符中 schema(参见 useValues) */ uiSchema: any; /** 消费变量值的字段 */ @@ -59,6 +59,7 @@ interface BaseProps { * @param option */ returnFields?(fields: FieldOption[], option: Option): FieldOption[]; + dataSource?: string; } interface BaseVariableProviderProps { @@ -138,12 +139,13 @@ export const useBaseVariable = ({ noChildren = false, // TODO: 等整理完完整测试用例后,再开启该功能 noDisabled = true, + dataSource, returnFields = (fields) => fields, }: BaseProps) => { const compile = useCompile(); const getFilterOptions = useGetFilterOptions(); const { isDisabled } = useContext(BaseVariableContext) || {}; - const { getCollectionField } = useCollectionManager(); + const { getCollectionField } = useCollectionManager_deprecated(dataSource); const loadChildren = (option: Option): Promise => { if (!option.field?.target) { @@ -154,7 +156,7 @@ export const useBaseVariable = ({ return new Promise((resolve) => { setTimeout(() => { const children = ( - getChildren(returnFields(getFilterOptions(target), option), { + getChildren(returnFields(getFilterOptions(target, dataSource), option), { collectionField, uiSchema, targetFieldSchema, diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useContextAssociationFields.tsx b/packages/core/client/src/schema-settings/VariableInput/hooks/useContextAssociationFields.tsx index 0ceaaab4b..d16ac795e 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useContextAssociationFields.tsx +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useContextAssociationFields.tsx @@ -1,12 +1,12 @@ import { error } from '@nocobase/utils/client'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { CollectionFieldOptions, useCollectionManager } from '../../../collection-manager'; +import { CollectionFieldOptions_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; import { useCompile, useGetFilterOptions } from '../../../schema-component'; import { FieldOption, Option } from '../type'; export const useIsSameOrChildCollection = () => { - const { getChildrenCollections } = useCollectionManager(); + const { getChildrenCollections } = useCollectionManager_deprecated(); return (contextCollection, targetCollection) => { if (contextCollection === targetCollection) { return true; @@ -71,7 +71,7 @@ export const useContextAssociationFields = ({ schema: any; maxDepth?: number; contextCollectionName: string; - collectionField: CollectionFieldOptions; + collectionField: CollectionFieldOptions_deprecated; }) => { const { t } = useTranslation(); const compile = useCompile(); diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useDateVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useDateVariable.ts index d1566b2fa..ccd01fbcd 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useDateVariable.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useDateVariable.ts @@ -1,11 +1,12 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { getDateRanges } from '../../../schema-component/antd/date-picker/util'; interface Props { operator?: { value: string; }; - schema: any; + schema?: any; /** * 不需要禁用选项,一般会在表达式中使用 */ @@ -13,6 +14,9 @@ interface Props { } /** + * @deprecated + * 该 hook 已废弃,请使用 `useDatetimeVariable` 代替 + * * 变量:`日期变量` * @param param0 * @returns @@ -170,3 +174,165 @@ export const useDateVariable = ({ operator, schema, noDisabled }: Props) => { return result; }; + +/** + * 变量:`日期变量` + * @param param0 + * @returns + */ +export const useDatetimeVariable = ({ operator, schema, noDisabled }: Props = {}) => { + const { t } = useTranslation(); + const datetimeSettings = useMemo(() => { + const operatorValue = operator?.value || ''; + const disabled = noDisabled ? false : !['DatePicker', 'DatePicker.RangePicker'].includes(schema?.['x-component']); + const dateOptions = [ + { + key: 'now', + value: 'now', + label: t('Current time'), + disabled: noDisabled ? false : schema?.['x-component'] !== 'DatePicker' || operatorValue === '$dateBetween', + }, + { + key: 'yesterday', + value: 'yesterday', + label: t('Yesterday'), + disabled, + }, + { + key: 'today', + value: 'today', + label: t('Today'), + disabled, + }, + { + key: 'tomorrow', + value: 'tomorrow', + label: t('Tomorrow'), + disabled, + }, + { + key: 'lastIsoWeek', + value: 'lastIsoWeek', + label: t('Last week'), + disabled, + }, + { + key: 'thisIsoWeek', + value: 'thisIsoWeek', + label: t('This week'), + disabled, + }, + { + key: 'nextIsoWeek', + value: 'nextIsoWeek', + label: t('Next week'), + disabled, + }, + { + key: 'lastMonth', + value: 'lastMonth', + label: t('Last month'), + disabled, + }, + { + key: 'thisMonth', + value: 'thisMonth', + label: t('This month'), + disabled, + }, + { + key: 'nextMonth', + value: 'nextMonth', + label: t('Next month'), + disabled, + }, + { + key: 'lastQuarter', + value: 'lastQuarter', + label: t('Last quarter'), + disabled, + }, + { + key: 'thisQuarter', + value: 'thisQuarter', + label: t('This quarter'), + disabled, + }, + { + key: 'nextQuarter', + value: 'nextQuarter', + label: t('Next quarter'), + disabled, + }, + { + key: 'lastYear', + value: 'lastYear', + label: t('Last year'), + disabled, + }, + { + key: 'thisYear', + value: 'thisYear', + label: t('This year'), + disabled, + }, + { + key: 'nextYear', + value: 'nextYear', + label: t('Next year'), + disabled, + }, + { + key: 'last7Days', + value: 'last7Days', + label: t('Last 7 days'), + disabled, + }, + { + key: 'next7Days', + value: 'next7Days', + label: t('Next 7 days'), + disabled, + }, + { + key: 'last30Days', + value: 'last30Days', + label: t('Last 30 days'), + disabled, + }, + { + key: 'next30Days', + value: 'next30Days', + label: t('Next 30 days'), + disabled, + }, + { + key: 'last90Days', + value: 'last90Days', + label: t('Last 90 days'), + disabled, + }, + { + key: 'next90Days', + value: 'next90Days', + label: t('Next 90 days'), + disabled, + }, + ]; + + return { + label: t('Date variables'), + value: '$nDate', + key: '$nDate', + disabled: dateOptions.every((option) => option.disabled), + children: dateOptions, + }; + }, [schema?.['x-component']]); + + const datetimeCtx = useMemo(() => getDateRanges(), []); + + return { + datetimeSettings, + datetimeCtx, + }; +}; diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useFormVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useFormVariable.ts index 6809bb486..aeabcaa45 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useFormVariable.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useFormVariable.ts @@ -1,18 +1,24 @@ +import { Form } from '@formily/core'; import { Schema } from '@formily/json-schema'; import { useTranslation } from 'react-i18next'; -import { CollectionFieldOptions } from '../../../collection-manager'; +import { useFormBlockContext } from '../../../block-provider'; +import { CollectionFieldOptions_deprecated } from '../../../collection-manager'; import { useBaseVariable } from './useBaseVariable'; interface Props { - collectionName: string; - collectionField: CollectionFieldOptions; + collectionName?: string; + collectionField?: CollectionFieldOptions_deprecated; schema?: any; noDisabled?: boolean; /** 消费变量值的字段 */ targetFieldSchema?: Schema; + form?: Form; } /** + * @deprecated + * 该 hook 已废弃,请使用 `useCurrentFormVariable` 代替 + * * 变量:`当前表单` * @param param0 * @returns @@ -44,3 +50,53 @@ export const useFormVariable = ({ collectionName, collectionField, schema, noDis return result; }; + +/** + * 变量:`当前表单` + * @param param0 + * @returns + */ +export const useCurrentFormVariable = ({ + collectionName, + collectionField, + schema, + noDisabled, + targetFieldSchema, + form: _form, +}: Props = {}) => { + // const { getActiveFieldsName } = useFormActiveFields() || {}; + const { t } = useTranslation(); + const { form } = useFormBlockContext(); + const currentFormSettings = useBaseVariable({ + collectionField, + uiSchema: schema, + targetFieldSchema, + maxDepth: 4, + name: '$nForm', + title: t('Current form'), + collectionName: collectionName, + noDisabled, + returnFields: (fields, option) => { + // fix https://nocobase.height.app/T-2277 + return fields; + // const activeFieldsName = getActiveFieldsName?.('form') || []; + + // return option.depth === 0 + // ? fields.filter((field) => { + // return activeFieldsName.includes(field.name); + // }) + // : fields; + }, + }); + + const formInstance = _form || form; + + return { + /** 变量配置 */ + currentFormSettings, + /** 变量值 */ + currentFormCtx: formInstance?.values, + /** 用来判断是否可以显示`当前表单`变量 */ + shouldDisplayCurrentForm: !!formInstance && !formInstance.readPretty, + }; +}; diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useIterationVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useIterationVariable.ts index 10b1cec4f..8834af4cf 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useIterationVariable.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useIterationVariable.ts @@ -1,9 +1,15 @@ import { Schema } from '@formily/json-schema'; import { useTranslation } from 'react-i18next'; -import { CollectionFieldOptions } from '../../../collection-manager'; +import { CollectionFieldOptions_deprecated } from '../../../collection-manager'; +import { CollectionFieldOptions } from '../../../data-source/collection/Collection'; +import { useFlag } from '../../../flag-provider'; +import { useSubFormValue } from '../../../schema-component/antd/association-field/hooks'; import { useBaseVariable } from './useBaseVariable'; /** + * @deprecated + * 该 hook 已废弃,请使用 `useCurrentObjectVariable` 代替 + * * 变量:`当前对象` * @param param0 * @returns @@ -16,7 +22,7 @@ export const useIterationVariable = ({ targetFieldSchema, }: { currentCollection: string; - collectionField: CollectionFieldOptions; + collectionField: CollectionFieldOptions_deprecated; schema?: any; noDisabled?: boolean; /** 消费变量值的字段 */ @@ -48,3 +54,58 @@ export const useIterationVariable = ({ return result; }; + +/** + * 变量:`当前对象` + * @param param0 + * @returns + */ +export const useCurrentObjectVariable = ({ + currentCollection, + collectionField, + schema, + noDisabled, + targetFieldSchema, +}: { + currentCollection?: string; + collectionField?: CollectionFieldOptions; + schema?: any; + noDisabled?: boolean; + /** 消费变量值的字段 */ + targetFieldSchema?: Schema; +} = {}) => { + // const { getActiveFieldsName } = useFormActiveFields() || {}; + const { formValue: currentObjectCtx } = useSubFormValue(); + const { isInSubForm, isInSubTable } = useFlag() || {}; + const { t } = useTranslation(); + const currentObjectSettings = useBaseVariable({ + collectionField, + uiSchema: schema, + targetFieldSchema, + maxDepth: 4, + name: '$iteration', + title: t('Current object'), + collectionName: currentCollection, + noDisabled, + returnFields: (fields, option) => { + // fix https://nocobase.height.app/T-2277 + return fields; + // const activeFieldsName = getActiveFieldsName?.('nester') || []; + + // return option.depth === 0 + // ? fields.filter((field) => { + // return activeFieldsName?.includes(field.name); + // }) + // : fields; + }, + }); + + return { + /** 是否显示变量 */ + shouldDisplayCurrentObject: isInSubForm || isInSubTable, + /** 变量的值 */ + currentObjectCtx, + /** 变量的配置项 */ + currentObjectSettings, + }; +}; diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useParentRecordVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useParentRecordVariable.ts index 245f8b288..443ee7fe3 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useParentRecordVariable.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useParentRecordVariable.ts @@ -1,17 +1,28 @@ import { Schema } from '@formily/json-schema'; import { useTranslation } from 'react-i18next'; -import { CollectionFieldOptions } from '../../../collection-manager'; +import { CollectionFieldOptions_deprecated } from '../../../collection-manager'; +import { useParentCollection } from '../../../data-source/collection/AssociationProvider'; +import { useCollectionRecord } from '../../../data-source/collection-record/CollectionRecordProvider'; +import { useFlag } from '../../../flag-provider/hooks/useFlag'; import { useBaseVariable } from './useBaseVariable'; interface Props { - collectionField: CollectionFieldOptions; - schema: any; - collectionName: string; + collectionField?: CollectionFieldOptions_deprecated; + schema?: any; + /** @deprecated */ + collectionName?: string; noDisabled?: boolean; /** 消费变量值的字段 */ targetFieldSchema?: Schema; } +/** + * @deprecated + * 该 hook 已废弃,请使用 `useCurrentParentRecordVariable` 代替 + * + * @param props + * @returns + */ export const useParentRecordVariable = (props: Props) => { const { t } = useTranslation(); @@ -27,3 +38,32 @@ export const useParentRecordVariable = (props: Props) => { return currentRecordVariable; }; + +/** + * 变量:`上级记录` + * @param props + * @returns + */ +export const useCurrentParentRecordVariable = (props: Props = {}) => { + const { t } = useTranslation(); + const record = useCollectionRecord(); + const { name: parentCollectionName } = useParentCollection() || {}; + const { isInSubForm, isInSubTable } = useFlag() || {}; + + const currentParentRecordSettings = useBaseVariable({ + collectionField: props.collectionField, + uiSchema: props.schema, + name: '$nParentRecord', + title: t('Parent record'), + collectionName: parentCollectionName, + noDisabled: props.noDisabled, + targetFieldSchema: props.targetFieldSchema, + }); + + return { + currentParentRecordSettings, + currentParentRecordCtx: record?.parentRecord?.data, + shouldDisplayCurrentParentRecord: !!record?.parentRecord?.data && !isInSubForm && !isInSubTable, + collectionName: parentCollectionName, + }; +}; diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useRecordVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useRecordVariable.ts index c9e303caf..dfa733c2d 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useRecordVariable.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useRecordVariable.ts @@ -1,17 +1,29 @@ import { Schema } from '@formily/json-schema'; +import _ from 'lodash'; import { useTranslation } from 'react-i18next'; -import { CollectionFieldOptions } from '../../../collection-manager'; +import { CollectionFieldOptions_deprecated } from '../../../collection-manager'; +import { useCollection } from '../../../data-source/collection/CollectionProvider'; +import { useCollectionRecord } from '../../../data-source/collection-record/CollectionRecordProvider'; +import { useFlag } from '../../../flag-provider/hooks/useFlag'; import { useBaseVariable } from './useBaseVariable'; interface Props { - collectionField: CollectionFieldOptions; - schema: any; - collectionName: string; + collectionField?: CollectionFieldOptions_deprecated; + schema?: any; + collectionName?: string; noDisabled?: boolean; /** 消费变量值的字段 */ targetFieldSchema?: Schema; } +/** + * @deprecated + * 该 hook 已废弃,请使用 `useCurrentRecordVariable` 代替 + * + * 变量:`当前记录` + * @param props + * @returns + */ export const useRecordVariable = (props: Props) => { const { t } = useTranslation(); @@ -27,3 +39,36 @@ export const useRecordVariable = (props: Props) => { return currentRecordVariable; }; + +/** + * 变量:`当前记录` + * @param props + * @returns + */ +export const useCurrentRecordVariable = (props: Props = {}) => { + const { t } = useTranslation(); + const { isInSubForm, isInSubTable } = useFlag() || {}; + const record = useCollectionRecord(); + const recordData = isInSubForm || isInSubTable ? record?.parentRecord?.data : record?.data; + const { name: collectionName } = useCollection() || {}; + const currentRecordSettings = useBaseVariable({ + collectionField: props.collectionField, + uiSchema: props.schema, + name: '$nRecord', + title: t('Current record'), + collectionName: props.collectionName || collectionName, + noDisabled: props.noDisabled, + targetFieldSchema: props.targetFieldSchema, + }); + + return { + /** 变量配置 */ + currentRecordSettings, + /** 变量值 */ + currentRecordCtx: recordData, + /** 用于判断是否需要显示配置项 */ + shouldDisplayCurrentRecord: !_.isEmpty(recordData), + /** 当前记录对应的 collection name */ + collectionName, + }; +}; diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useRoleVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useRoleVariable.ts index f75c05477..6c853110c 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useRoleVariable.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useRoleVariable.ts @@ -1,9 +1,14 @@ import { Schema } from '@formily/json-schema'; import { useTranslation } from 'react-i18next'; -import { CollectionFieldOptions } from '../../../collection-manager'; +import { useAPIClient } from '../../../api-client'; +import { CollectionFieldOptions_deprecated } from '../../../collection-manager'; +import { CollectionFieldOptions } from '../../../data-source/collection/Collection'; import { useBaseVariable } from './useBaseVariable'; /** + * @deprecated + * 该 hook 已废弃,请使用 `useCurrentRoleVariable` 代替 + * * 变量:`当前角色` * @param param0 * @returns @@ -15,7 +20,7 @@ export const useRoleVariable = ({ targetFieldSchema, maxDepth = 0, }: { - collectionField: CollectionFieldOptions; + collectionField: CollectionFieldOptions_deprecated; uiSchema: any; maxDepth?: number; noDisabled?: boolean; @@ -37,3 +42,44 @@ export const useRoleVariable = ({ return result; }; + +/** + * 变量:`当前角色` + * @param param0 + * @returns + */ +export const useCurrentRoleVariable = ({ + collectionField, + uiSchema, + noDisabled, + targetFieldSchema, + maxDepth = 0, +}: { + collectionField?: CollectionFieldOptions; + uiSchema?: any; + maxDepth?: number; + noDisabled?: boolean; + /** 消费变量值的字段 */ + targetFieldSchema?: Schema; +} = {}) => { + const { t } = useTranslation(); + const apiClient = useAPIClient(); + const currentRoleSettings = useBaseVariable({ + collectionField, + uiSchema, + maxDepth, + name: '$nRole', + title: t('Current role'), + collectionName: 'roles', + noDisabled, + targetFieldSchema, + noChildren: true, + }); + + return { + /** 变量配置项 */ + currentRoleSettings, + /** 变量的值 */ + currentRoleCtx: apiClient.auth?.role, + }; +}; diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useUserVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useUserVariable.ts index 93ca47cb1..e8dd74101 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useUserVariable.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useUserVariable.ts @@ -1,9 +1,15 @@ import { Schema } from '@formily/json-schema'; import { useTranslation } from 'react-i18next'; -import { CollectionFieldOptions } from '../../../collection-manager'; +import { CollectionFieldOptions_deprecated } from '../../../collection-manager'; +import { CollectionFieldOptions } from '../../../data-source/collection/Collection'; +import { DEFAULT_DATA_SOURCE_KEY } from '../../../data-source/data-source/DataSourceManager'; +import { useCurrentUserContext } from '../../../user'; import { useBaseVariable } from './useBaseVariable'; /** + * @deprecated + * 该 hook 已废弃,请使用 `useCurrentUserVariable` 代替 + * * 变量:`当前用户` * @param param0 * @returns @@ -15,7 +21,7 @@ export const useUserVariable = ({ targetFieldSchema, maxDepth = 3, }: { - collectionField: CollectionFieldOptions; + collectionField: CollectionFieldOptions_deprecated; uiSchema: any; maxDepth?: number; noDisabled?: boolean; @@ -30,9 +36,50 @@ export const useUserVariable = ({ name: '$user', title: t('Current user'), collectionName: 'users', + dataSource: DEFAULT_DATA_SOURCE_KEY, noDisabled, targetFieldSchema, }); return result; }; + +/** + * 变量:`当前用户` + * @param param0 + * @returns + */ +export const useCurrentUserVariable = ({ + collectionField, + uiSchema, + noDisabled, + targetFieldSchema, + maxDepth = 3, +}: { + collectionField?: CollectionFieldOptions; + uiSchema?: any; + maxDepth?: number; + noDisabled?: boolean; + /** 消费变量值的字段 */ + targetFieldSchema?: Schema; +} = {}) => { + const { t } = useTranslation(); + const data = useCurrentUserContext(); + const currentUserSettings = useBaseVariable({ + collectionField, + uiSchema, + maxDepth, + name: '$user', + title: t('Current user'), + collectionName: 'users', + noDisabled, + targetFieldSchema, + }); + + return { + /** 变量的配置项 */ + currentUserSettings, + /** 变量的值 */ + currentUserCtx: data?.data?.data, + }; +}; diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useVariableOptions.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useVariableOptions.ts index b360701c8..c7c5a182d 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useVariableOptions.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useVariableOptions.ts @@ -1,26 +1,24 @@ import { Form } from '@formily/core'; import { ISchema, Schema } from '@formily/react'; -import _ from 'lodash'; import { useMemo } from 'react'; -import { CollectionFieldOptions, useCollection } from '../../../collection-manager'; -import { useFlag } from '../../../flag-provider'; +import { CollectionFieldOptions_deprecated, useCollection_deprecated } from '../../../collection-manager'; import { useBlockCollection } from './useBlockCollection'; -import { useDateVariable } from './useDateVariable'; -import { useFormVariable } from './useFormVariable'; -import { useIterationVariable } from './useIterationVariable'; -import { useParentRecordVariable } from './useParentRecordVariable'; -import { useRecordVariable } from './useRecordVariable'; -import { useRoleVariable } from './useRoleVariable'; -import { useUserVariable } from './useUserVariable'; +import { useDatetimeVariable } from './useDateVariable'; +import { useCurrentFormVariable } from './useFormVariable'; +import { useCurrentObjectVariable } from './useIterationVariable'; +import { useCurrentParentRecordVariable } from './useParentRecordVariable'; +import { useCurrentRecordVariable } from './useRecordVariable'; +import { useCurrentRoleVariable } from './useRoleVariable'; +import { useCurrentUserVariable } from './useUserVariable'; interface Props { /** * 消费该变量的字段 */ - collectionField: CollectionFieldOptions; + collectionField: CollectionFieldOptions_deprecated; form: Form; /** - * `useRecord` 返回的值 + * `useRecord ` 返回的值 */ record?: Record; /** @@ -55,45 +53,45 @@ export const useVariableOptions = ({ currentIterationCollectionName, }: Props) => { const { name: blockCollectionName = record?.__collectionName } = useBlockCollection(); - const { isInSubForm, isInSubTable } = useFlag() || {}; - const { name } = useCollection(); + const { name } = useCollection_deprecated(); const blockParentCollectionName = record?.__parent?.__collectionName; - const userVariable = useUserVariable({ + const { currentUserSettings } = useCurrentUserVariable({ maxDepth: 3, uiSchema: uiSchema, collectionField, noDisabled, targetFieldSchema, }); - const roleVariable = useRoleVariable({ + const { currentRoleSettings } = useCurrentRoleVariable({ uiSchema: uiSchema, collectionField, noDisabled, targetFieldSchema, }); - const dateVariable = useDateVariable({ operator, schema: uiSchema, noDisabled }); - const formVariable = useFormVariable({ + const { datetimeSettings } = useDatetimeVariable({ operator, schema: uiSchema, noDisabled }); + const { currentFormSettings, shouldDisplayCurrentForm } = useCurrentFormVariable({ schema: uiSchema, collectionName: currentFormCollectionName || blockCollectionName, collectionField, noDisabled, targetFieldSchema, + form, }); - const iterationVariable = useIterationVariable({ + const { currentObjectSettings, shouldDisplayCurrentObject } = useCurrentObjectVariable({ currentCollection: currentIterationCollectionName || name, collectionField, schema: uiSchema, noDisabled, targetFieldSchema, }); - const currentRecordVariable = useRecordVariable({ + const { currentRecordSettings, shouldDisplayCurrentRecord } = useCurrentRecordVariable({ schema: uiSchema, collectionName: blockCollectionName, collectionField, noDisabled, targetFieldSchema, }); - const currentParentRecordVariable = useParentRecordVariable({ + const { currentParentRecordSettings, shouldDisplayCurrentParentRecord } = useCurrentParentRecordVariable({ schema: uiSchema, collectionName: blockParentCollectionName, collectionField, @@ -103,29 +101,25 @@ export const useVariableOptions = ({ return useMemo(() => { return [ - userVariable, - roleVariable, - dateVariable, - form && !form.readPretty && formVariable, - (isInSubForm || isInSubTable) && iterationVariable, - blockCollectionName && !_.isEmpty(_.omit(record, ['__parent', '__collectionName'])) && currentRecordVariable, - blockParentCollectionName && - !_.isEmpty(_.omit(record?.__parent, ['__parent', '__collectionName'])) && - currentParentRecordVariable, + currentUserSettings, + currentRoleSettings, + datetimeSettings, + shouldDisplayCurrentForm && currentFormSettings, + shouldDisplayCurrentObject && currentObjectSettings, + shouldDisplayCurrentRecord && currentRecordSettings, + shouldDisplayCurrentParentRecord && currentParentRecordSettings, ].filter(Boolean); }, [ - userVariable, - roleVariable, - dateVariable, - form, - formVariable, - isInSubForm, - isInSubTable, - iterationVariable, - blockCollectionName, - record, - currentRecordVariable, - blockParentCollectionName, - currentParentRecordVariable, + currentUserSettings, + currentRoleSettings, + datetimeSettings, + shouldDisplayCurrentForm, + currentFormSettings, + shouldDisplayCurrentObject, + currentObjectSettings, + shouldDisplayCurrentRecord, + currentRecordSettings, + shouldDisplayCurrentParentRecord, + currentParentRecordSettings, ]); }; diff --git a/packages/core/client/src/schema-settings/hooks/useGetAriaLabelOfDesigner.ts b/packages/core/client/src/schema-settings/hooks/useGetAriaLabelOfDesigner.ts index 09ab38ce2..fab7add10 100644 --- a/packages/core/client/src/schema-settings/hooks/useGetAriaLabelOfDesigner.ts +++ b/packages/core/client/src/schema-settings/hooks/useGetAriaLabelOfDesigner.ts @@ -1,6 +1,6 @@ import { useFieldSchema } from '@formily/react'; import { useCallback } from 'react'; -import { useCollection } from '../../collection-manager'; +import { useCollection_deprecated } from '../../collection-manager'; /** * label = 'designer' + name + x-component + [x-designer] + [collectionName] + [x-collection-field] + [postfix] @@ -8,18 +8,19 @@ import { useCollection } from '../../collection-manager'; */ export const useGetAriaLabelOfDesigner = () => { const fieldSchema = useFieldSchema(); - const { name: _collectionName } = useCollection(); + const { name: _collectionName } = useCollection_deprecated(); const getAriaLabel = useCallback( (name: string, postfix?: string) => { if (!fieldSchema) return ''; const component = fieldSchema['x-component']; const designer = fieldSchema['x-designer'] ? `-${fieldSchema['x-designer']}` : ''; + const settings = fieldSchema['x-settings'] ? `-${fieldSchema['x-settings']}` : ''; const collectionField = fieldSchema['x-collection-field'] ? `-${fieldSchema['x-collection-field']}` : ''; const collectionName = _collectionName ? `-${_collectionName}` : ''; postfix = postfix ? `-${postfix}` : ''; - return `designer-${name}-${component}${designer}${collectionName}${collectionField}${postfix}`; + return `designer-${name}-${component}${designer}${settings}${collectionName}${collectionField}${postfix}`; }, [fieldSchema, _collectionName], ); diff --git a/packages/core/client/src/schema-settings/hooks/useIsAllowToSetDefaultValue.tsx b/packages/core/client/src/schema-settings/hooks/useIsAllowToSetDefaultValue.tsx index c3b897f81..122bebbf5 100644 --- a/packages/core/client/src/schema-settings/hooks/useIsAllowToSetDefaultValue.tsx +++ b/packages/core/client/src/schema-settings/hooks/useIsAllowToSetDefaultValue.tsx @@ -1,14 +1,12 @@ import { Form } from '@formily/core'; import { Schema, useFieldSchema } from '@formily/react'; import React, { useContext, useMemo } from 'react'; -import { - CollectionFieldOptions, - useCollection, - useCollectionManager, - useFormBlockContext, - useFormBlockType, -} from '../..'; -import { isPatternDisabled, isSystemField } from '../SchemaSettings'; +import { CollectionFieldOptions_deprecated } from '../..'; +import { isSystemField } from '../SchemaSettings'; +import { isPatternDisabled } from '../isPatternDisabled'; +import { useFormBlockContext, useFormBlockType } from '../../block-provider'; +import { useCollectionManager_deprecated } from '../../collection-manager/hooks/useCollectionManager_deprecated'; +import { useCollection_deprecated } from '../../collection-manager/hooks/useCollection_deprecated'; interface DefaultValueProviderProps { isAllowToSetDefaultValue: (params: IsAllowToSetDefaultValueParams) => boolean; @@ -16,7 +14,7 @@ interface DefaultValueProviderProps { } export interface IsAllowToSetDefaultValueParams { - collectionField: CollectionFieldOptions; + collectionField: CollectionFieldOptions_deprecated; getInterface: (name: string) => any; formBlockType?: 'update' | 'create'; form: Form; @@ -27,7 +25,7 @@ export interface IsAllowToSetDefaultValueParams { interface Props { form?: Form; fieldSchema?: Schema; - collectionField?: CollectionFieldOptions; + collectionField?: CollectionFieldOptions_deprecated; } const DefaultValueContext = React.createContext>(null); @@ -42,9 +40,9 @@ export const DefaultValueProvider = (props: DefaultValueProviderProps) => { return {props.children}; }; -const useIsAllowToSetDefaultValue = ({ form, fieldSchema, collectionField }: Props = {}) => { - const { getInterface, getCollectionJoinField } = useCollectionManager(); - const { getField } = useCollection(); +export const useIsAllowToSetDefaultValue = ({ form, fieldSchema, collectionField }: Props = {}) => { + const { getInterface, getCollectionJoinField } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); const { form: innerForm } = useFormBlockContext(); const innerFieldSchema = useFieldSchema(); const { type } = useFormBlockType(); @@ -66,8 +64,6 @@ const useIsAllowToSetDefaultValue = ({ form, fieldSchema, collectionField }: Pro }; }; -export default useIsAllowToSetDefaultValue; - export const interfacesOfUnsupportedDefaultValue = [ 'o2o', 'oho', diff --git a/packages/core/client/src/schema-settings/hooks/useIsShowMultipleSwitch.ts b/packages/core/client/src/schema-settings/hooks/useIsShowMultipleSwitch.ts index ca649a6be..67aab0708 100644 --- a/packages/core/client/src/schema-settings/hooks/useIsShowMultipleSwitch.ts +++ b/packages/core/client/src/schema-settings/hooks/useIsShowMultipleSwitch.ts @@ -1,5 +1,6 @@ import { useField, useFieldSchema } from '@formily/react'; -import { useCollectionManager } from '../../collection-manager'; +import { useCollectionManager_deprecated } from '../../collection-manager'; +import { useColumnSchema } from '../../schema-component/antd/table-v2/Table.Column.Decorator'; /** * 是否显示 `允许多选` 开关 @@ -7,7 +8,7 @@ import { useCollectionManager } from '../../collection-manager'; export function useIsShowMultipleSwitch() { const field = useField(); const fieldSchema = useFieldSchema(); - const { getCollectionField } = useCollectionManager(); + const { getCollectionField } = useCollectionManager_deprecated(); const collectionField = fieldSchema['x-collection-field'] ? getCollectionField(fieldSchema['x-collection-field']) diff --git a/packages/core/client/src/schema-settings/hooks/useParseDataScopeFilter.ts b/packages/core/client/src/schema-settings/hooks/useParseDataScopeFilter.ts index 6c02297a7..5830c9f21 100644 --- a/packages/core/client/src/schema-settings/hooks/useParseDataScopeFilter.ts +++ b/packages/core/client/src/schema-settings/hooks/useParseDataScopeFilter.ts @@ -10,14 +10,13 @@ interface Props { * 被排除的变量不会被解析,会按原值返回 */ exclude?: string[]; - currentRecord?: any; } // TODO: 建议变量名统一命名为 `$n` 开头,以防止与 formily 内置变量冲突 const defaultExclude = ['$user', '$date', '$nDate', '$nRole']; -const useParseDataScopeFilter = ({ exclude = defaultExclude, currentRecord }: Props = {}) => { - const localVariables = useLocalVariables({ currentRecord }); +const useParseDataScopeFilter = ({ exclude = defaultExclude }: Props = {}) => { + const localVariables = useLocalVariables(); const variables = useVariables(); /** diff --git a/packages/core/client/src/schema-settings/index.ts b/packages/core/client/src/schema-settings/index.ts index bb12a9fa2..5030aed16 100644 --- a/packages/core/client/src/schema-settings/index.ts +++ b/packages/core/client/src/schema-settings/index.ts @@ -1,10 +1,16 @@ -export * from './GeneralSchemaDesigner'; -export * from './SchemaSettings'; -export * from './hooks/useIsAllowToSetDefaultValue'; -export * from './hooks/useGetAriaLabelOfDesigner'; export * from './DataTemplates/hooks/useCollectionState'; export * from './DataTemplates/utils'; +export * from './GeneralSchemaDesigner'; +export * from './SchemaSettings'; +export * from './hooks/useGetAriaLabelOfDesigner'; +export * from './hooks/useIsAllowToSetDefaultValue'; +export * from './isPatternDisabled'; +export * from './SchemaSettingsDataScope'; +export * from './SchemaSettingsDefaultValue'; +export * from './SchemaSettingsDateFormat'; +export * from './SchemaSettingsSortingRule'; export { default as useParseDataScopeFilter } from './hooks/useParseDataScopeFilter'; +export { SchemaSettingsPlugin } from './SchemaSettingsPlugin'; export * from './VariableInput'; diff --git a/packages/core/client/src/schema-settings/isPatternDisabled.tsx b/packages/core/client/src/schema-settings/isPatternDisabled.tsx new file mode 100644 index 000000000..cff5a220c --- /dev/null +++ b/packages/core/client/src/schema-settings/isPatternDisabled.tsx @@ -0,0 +1,5 @@ +import { Schema } from '@formily/react'; + +export const isPatternDisabled = (fieldSchema: Schema) => { + return fieldSchema?.['x-component-props']?.['pattern-disable'] == true; +}; diff --git a/packages/core/client/src/schema-settings/styles.ts b/packages/core/client/src/schema-settings/styles.ts index 58b588576..cf1a0d16a 100644 --- a/packages/core/client/src/schema-settings/styles.ts +++ b/packages/core/client/src/schema-settings/styles.ts @@ -6,7 +6,10 @@ export const useStyles = createStyles(() => { position: 'absolute', zIndex: 999, display: 'none', - inset: 0, + top: 0, + left: 0, + right: 0, + bottom: 0, border: '2px solid var(--colorBorderSettingsHover)', background: 'var(--colorBgSettingsHover)', pointerEvents: 'none', diff --git a/packages/core/client/src/schema-templates/BlockTemplatePage.tsx b/packages/core/client/src/schema-templates/BlockTemplatePage.tsx index 5665f31e1..b5b5bacd1 100644 --- a/packages/core/client/src/schema-templates/BlockTemplatePage.tsx +++ b/packages/core/client/src/schema-templates/BlockTemplatePage.tsx @@ -1,9 +1,7 @@ import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { CollectionManagerProvider } from '../collection-manager'; import { SchemaComponent } from '../schema-component'; -import { uiSchemaTemplatesCollection } from './collections/uiSchemaTemplates'; import { uiSchemaTemplatesSchema } from './schemas/uiSchemaTemplates'; export const BlockTemplatePage = () => { @@ -12,18 +10,12 @@ export const BlockTemplatePage = () => {
- - - +
); }; export const BlockTemplatesPane = () => { - return ( - - - - ); + return ; }; diff --git a/packages/core/client/src/schema-templates/SchemaTemplateManagerProvider.tsx b/packages/core/client/src/schema-templates/SchemaTemplateManagerProvider.tsx index 872436cec..0411c3cde 100644 --- a/packages/core/client/src/schema-templates/SchemaTemplateManagerProvider.tsx +++ b/packages/core/client/src/schema-templates/SchemaTemplateManagerProvider.tsx @@ -6,8 +6,9 @@ import { useLocation } from 'react-router-dom'; import { useAPIClient, useRequest } from '../api-client'; import { Plugin } from '../application/Plugin'; import { useAppSpin } from '../application/hooks/useAppSpin'; -import { useCollectionManager } from '../collection-manager'; +import { useCollectionManager_deprecated } from '../collection-manager'; import { BlockTemplate } from './BlockTemplate'; +import { DEFAULT_DATA_SOURCE_KEY } from '../data-source'; export const SchemaTemplateManagerContext = createContext({}); @@ -37,7 +38,7 @@ export const useSchemaTemplate = () => { }; export const useSchemaTemplateManager = () => { - const { getInheritCollections } = useCollectionManager(); + const { getInheritCollections } = useCollectionManager_deprecated(); const { refresh, templates = [] } = useContext(SchemaTemplateManagerContext); const api = useAPIClient(); return { @@ -103,10 +104,14 @@ export const useSchemaTemplateManager = () => { getTemplateById(key) { return templates?.find((template) => template.key === key); }, - getTemplatesByCollection(collectionName: string, resourceName: string = null) { - const parentCollections = getInheritCollections(collectionName); + getTemplatesByCollection(dataSource: string, collectionName: string) { + const parentCollections = getInheritCollections(collectionName, dataSource); const totalCollections = parentCollections.concat([collectionName]); - const items = templates?.filter?.((template) => totalCollections.includes(template.collectionName)); + const items = templates?.filter?.( + (template) => + (template.dataSourceKey || DEFAULT_DATA_SOURCE_KEY) === dataSource && + totalCollections.includes(template.collectionName), + ); return items || []; }, getTemplatesByComponentName(componentName: string): Array { @@ -123,7 +128,7 @@ const Internal = (props) => { resource: 'uiSchemaTemplates', action: 'list', params: { - appends: ['collection'], + // appends: ['collection'], paginate: false, }, }; @@ -169,10 +174,6 @@ export class RemoteSchemaTemplateManagerPlugin extends Plugin { } addRoutes() { - this.app.router.add('admin.plugins.block-templates', { - path: '/admin/plugins/block-templates', - Component: 'BlockTemplatePage', - }); this.app.router.add('admin.plugins.block-templates-key', { path: '/admin/plugins/block-templates/:key', Component: 'BlockTemplateDetails', diff --git a/packages/core/client/src/schema-templates/schemas/CollectionTitle.tsx b/packages/core/client/src/schema-templates/schemas/CollectionTitle.tsx new file mode 100644 index 000000000..6b9214807 --- /dev/null +++ b/packages/core/client/src/schema-templates/schemas/CollectionTitle.tsx @@ -0,0 +1,22 @@ +import React, { useMemo } from 'react'; +import { useDataSourceManager, useCollectionRecordData } from '../../data-source'; +import { useCompile } from '../../schema-component'; + +export const CollectionTitle = (props) => { + const { dataSourceKey = 'main', collectionName } = useCollectionRecordData(); + const dataSourceManager = useDataSourceManager(); + const compile = useCompile(); + const [title1, title2] = useMemo(() => { + const dataSource = dataSourceManager.getDataSource(dataSourceKey); + if (dataSource) { + const collection = dataSource.collectionManager.getCollection(collectionName); + return [dataSource.displayName, collection?.title || collectionName]; + } + return [dataSourceKey, null]; + }, [dataSourceManager, dataSourceKey, collectionName]); + return ( + <> + {compile(title1)} {'>'} {compile(title2)} + + ); +}; diff --git a/packages/core/client/src/schema-templates/schemas/uiSchemaTemplates.ts b/packages/core/client/src/schema-templates/schemas/uiSchemaTemplates.ts index acc9a0bf2..f4fe3494c 100644 --- a/packages/core/client/src/schema-templates/schemas/uiSchemaTemplates.ts +++ b/packages/core/client/src/schema-templates/schemas/uiSchemaTemplates.ts @@ -2,6 +2,8 @@ import { ISchema } from '@formily/react'; import { uid } from '@formily/shared'; import { useBulkDestroyActionProps, useDestroyActionProps, useUpdateActionProps } from '../../block-provider/hooks'; import { useSchemaTemplateManager } from '../SchemaTemplateManagerProvider'; +import { uiSchemaTemplatesCollection } from '../collections/uiSchemaTemplates'; +import { CollectionTitle } from './CollectionTitle'; const useUpdateSchemaTemplateActionProps = () => { const props = useUpdateActionProps(); @@ -43,12 +45,12 @@ export const uiSchemaTemplatesSchema: ISchema = { type: 'void', 'x-decorator': 'TableBlockProvider', 'x-decorator-props': { - collection: 'uiSchemaTemplates', + collection: uiSchemaTemplatesCollection, resource: 'uiSchemaTemplates', action: 'list', params: { pageSize: 20, - appends: ['collection'], + // appends: ['collection'], sort: ['-createdAt'], }, rowKey: 'key', @@ -135,7 +137,7 @@ export const uiSchemaTemplatesSchema: ISchema = { 'x-decorator': 'FormBlockProvider', 'x-decorator-props': { resource: 'uiSchemaTemplates', - collection: 'uiSchemaTemplates', + collection: uiSchemaTemplatesCollection, action: 'get', useParams: '{{ useParamsFromRecord }}', }, @@ -222,13 +224,11 @@ export const uiSchemaTemplatesSchema: ISchema = { column2: { type: 'void', title: '{{t("Collection display name")}}', - 'x-decorator': 'TableV2.Column.Decorator', 'x-component': 'TableV2.Column', properties: { 'collection.title': { type: 'string', - 'x-collection-field': 'uiSchemaTemplates.collection', - 'x-component': 'Input', + 'x-component': CollectionTitle, 'x-read-pretty': true, 'x-component-props': { ellipsis: true, diff --git a/packages/core/client/src/variables/VariablesProvider.tsx b/packages/core/client/src/variables/VariablesProvider.tsx index ba61b3121..425556cb9 100644 --- a/packages/core/client/src/variables/VariablesProvider.tsx +++ b/packages/core/client/src/variables/VariablesProvider.tsx @@ -3,8 +3,8 @@ import { getValuesByPath } from '@nocobase/utils/client'; import _ from 'lodash'; import React, { createContext, useCallback, useEffect, useMemo, useRef } from 'react'; import { useAPIClient } from '../api-client'; -import type { CollectionFieldOptions } from '../collection-manager'; -import { useCollectionManager } from '../collection-manager'; +import type { CollectionFieldOptions_deprecated } from '../collection-manager'; +import { useCollectionManager_deprecated } from '../collection-manager'; import { useCompile } from '../schema-component'; import useBuiltInVariables from './hooks/useBuiltinVariables'; import { VariableOption, VariablesContextType } from './types'; @@ -33,7 +33,7 @@ const getFieldPath = (variablePath: string, variableToCollectionName: Record { const ctxRef = useRef>({}); const api = useAPIClient(); - const { getCollectionJoinField } = useCollectionManager(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); const compile = useCompile(); const { builtinVariables } = useBuiltInVariables(); @@ -68,7 +68,7 @@ const VariablesProvider = ({ children }) => { let current = mergeCtxWithLocalVariables(ctxRef.current, localVariables); let collectionName = getFieldPath(variableName, _variableToCollectionName); - if (!current[variableName]) { + if (!(variableName in current)) { throw new Error(`VariablesProvider: ${variableName} is not found`); } @@ -78,7 +78,7 @@ const VariablesProvider = ({ children }) => { } const key = list[index]; - const associationField: CollectionFieldOptions = getCollectionJoinField( + const associationField: CollectionFieldOptions_deprecated = getCollectionJoinField( getFieldPath(list.slice(0, index + 1).join('.'), _variableToCollectionName), ); if (Array.isArray(current)) { diff --git a/packages/core/client/src/variables/__tests__/useVariables.test.tsx b/packages/core/client/src/variables/__tests__/useVariables.test.tsx index 90f704316..c56e3277e 100644 --- a/packages/core/client/src/variables/__tests__/useVariables.test.tsx +++ b/packages/core/client/src/variables/__tests__/useVariables.test.tsx @@ -9,7 +9,7 @@ import useVariables from '../hooks/useVariables'; vi.mock('../../collection-manager', async () => { return { - useCollectionManager: () => { + useCollectionManager_deprecated: () => { return { getCollectionJoinField: (path: string) => { if (path === 'users.belongsToField') { diff --git a/packages/core/client/src/variables/hooks/useBuiltinVariables.ts b/packages/core/client/src/variables/hooks/useBuiltinVariables.ts index e21bc01b4..3b40c68b2 100644 --- a/packages/core/client/src/variables/hooks/useBuiltinVariables.ts +++ b/packages/core/client/src/variables/hooks/useBuiltinVariables.ts @@ -1,26 +1,23 @@ import { dayjs } from '@nocobase/utils/client'; import { useMemo } from 'react'; -import { useAPIClient } from '../../api-client'; -import { getDateRanges } from '../../schema-component/antd/date-picker/util'; -import { useCurrentUserContext } from '../../user'; +import { useCurrentUserVariable, useDatetimeVariable } from '../../schema-settings'; +import { useCurrentRoleVariable } from '../../schema-settings/VariableInput/hooks/useRoleVariable'; import { VariableOption } from '../types'; const useBuiltInVariables = () => { - const data = useCurrentUserContext(); - const apiClient = useAPIClient(); - - const currentUser = data?.data?.data; - const dateVars = getDateRanges(); + const { currentUserCtx } = useCurrentUserVariable(); + const { currentRoleCtx } = useCurrentRoleVariable(); + const { datetimeCtx } = useDatetimeVariable(); const builtinVariables: VariableOption[] = useMemo(() => { return [ { name: '$user', - ctx: currentUser, + ctx: currentUserCtx, collectionName: 'users', }, { name: '$nRole', - ctx: apiClient.auth?.role, + ctx: currentRoleCtx, collectionName: 'roles', }, /** @@ -29,12 +26,12 @@ const useBuiltInVariables = () => { */ { name: 'currentUser', - ctx: currentUser, + ctx: currentUserCtx, collectionName: 'users', }, { name: '$nDate', - ctx: dateVars, + ctx: datetimeCtx, }, /** * @deprecated @@ -42,7 +39,7 @@ const useBuiltInVariables = () => { */ { name: '$date', - ctx: dateVars, + ctx: datetimeCtx, }, /** * @deprecated @@ -63,7 +60,7 @@ const useBuiltInVariables = () => { ctx: () => dayjs().toISOString(), }, ]; - }, [currentUser]); + }, [currentUserCtx]); return { builtinVariables }; }; diff --git a/packages/core/client/src/variables/hooks/useLocalVariables.tsx b/packages/core/client/src/variables/hooks/useLocalVariables.tsx index 5d67e25cc..fb19b81a5 100644 --- a/packages/core/client/src/variables/hooks/useLocalVariables.tsx +++ b/packages/core/client/src/variables/hooks/useLocalVariables.tsx @@ -1,38 +1,33 @@ import { Form } from '@formily/core'; import { useMemo } from 'react'; -import { useFormBlockContext } from '../../block-provider'; -import { useCollection } from '../../collection-manager'; -import { useRecord } from '../../record-provider'; -import { useSubFormValue } from '../../schema-component/antd/association-field/hooks'; -import { getDateRanges } from '../../schema-component/antd/date-picker/util'; +import { useCollection_deprecated } from '../../collection-manager'; import { useBlockCollection } from '../../schema-settings/VariableInput/hooks/useBlockCollection'; +import { useDatetimeVariable } from '../../schema-settings/VariableInput/hooks/useDateVariable'; +import { useCurrentFormVariable } from '../../schema-settings/VariableInput/hooks/useFormVariable'; +import { useCurrentObjectVariable } from '../../schema-settings/VariableInput/hooks/useIterationVariable'; +import { useCurrentParentRecordVariable } from '../../schema-settings/VariableInput/hooks/useParentRecordVariable'; +import { useCurrentRecordVariable } from '../../schema-settings/VariableInput/hooks/useRecordVariable'; import { VariableOption } from '../types'; interface Props { collectionName?: string; - currentRecord?: Record; currentForm?: Form; } const useLocalVariables = (props?: Props) => { - const { name: currentCollectionName } = useCollection(); - const { formValue: subFormValue } = useSubFormValue(); + const { currentObjectCtx, shouldDisplayCurrentObject } = useCurrentObjectVariable(); + const { currentRecordCtx, collectionName: collectionNameOfRecord } = useCurrentRecordVariable(); + const { currentParentRecordCtx, collectionName: collectionNameOfParentRecord } = useCurrentParentRecordVariable(); + const { datetimeCtx } = useDatetimeVariable(); + const { currentFormCtx } = useCurrentFormVariable({ form: props?.currentForm }); + const { name: currentCollectionName } = useCollection_deprecated(); let { name } = useBlockCollection(); - let currentRecord = useRecord(); - let { form } = useFormBlockContext(); - if (props?.currentForm) { - form = props.currentForm; - } - if (props?.currentRecord) { - currentRecord = props.currentRecord; - } if (props?.collectionName) { name = props.collectionName; } return useMemo(() => { - const dateVars = getDateRanges(); return ( [ /** @@ -41,8 +36,8 @@ const useLocalVariables = (props?: Props) => { */ { name: 'currentRecord', - ctx: currentRecord, - collectionName: name, + ctx: currentRecordCtx, + collectionName: collectionNameOfRecord, }, /** * @deprecated @@ -50,7 +45,7 @@ const useLocalVariables = (props?: Props) => { */ { name, - ctx: form?.values || currentRecord, + ctx: currentFormCtx || currentRecordCtx, collectionName: name, }, /** @@ -59,27 +54,27 @@ const useLocalVariables = (props?: Props) => { */ { name: '$form', - ctx: form?.values, + ctx: currentFormCtx, collectionName: name, }, { name: '$nRecord', - ctx: currentRecord, - collectionName: currentRecord?.__collectionName, + ctx: currentRecordCtx, + collectionName: collectionNameOfRecord, }, { name: '$nParentRecord', - ctx: currentRecord?.__parent, - collectionName: currentRecord?.__parent?.__collectionName, + ctx: currentParentRecordCtx, + collectionName: collectionNameOfParentRecord, }, { name: '$nForm', - ctx: form?.values, + ctx: currentFormCtx, collectionName: name, }, { name: '$nDate', - ctx: dateVars, + ctx: datetimeCtx, }, /** * @deprecated @@ -87,12 +82,27 @@ const useLocalVariables = (props?: Props) => { */ { name: '$date', - ctx: dateVars, + ctx: datetimeCtx, + }, + shouldDisplayCurrentObject && { + name: '$iteration', + ctx: currentObjectCtx, + collectionName: currentCollectionName, }, - subFormValue && { name: '$iteration', ctx: subFormValue, collectionName: currentCollectionName }, ] as VariableOption[] ).filter(Boolean); - }, [currentRecord, name, form?.values, subFormValue, currentCollectionName]); // 尽量保持返回的值不变,这样可以减少接口的请求次数,因为关系字段会缓存到变量的 ctx 中 + }, [ + currentRecordCtx, + collectionNameOfRecord, + name, + currentFormCtx, + currentParentRecordCtx, + collectionNameOfParentRecord, + datetimeCtx, + shouldDisplayCurrentObject, + currentObjectCtx, + currentCollectionName, + ]); // 尽量保持返回的值不变,这样可以减少接口的请求次数,因为关系字段会缓存到变量的 ctx 中 }; export default useLocalVariables; diff --git a/packages/core/client/src/variables/types.ts b/packages/core/client/src/variables/types.ts index 23209cbc7..8760e6e59 100644 --- a/packages/core/client/src/variables/types.ts +++ b/packages/core/client/src/variables/types.ts @@ -1,5 +1,5 @@ import type { Dispatch, SetStateAction } from 'react'; -import { CollectionFieldOptions } from '../collection-manager'; +import { CollectionFieldOptions_deprecated } from '../collection-manager'; export interface VariablesContextType { /** @@ -63,7 +63,7 @@ export interface VariablesContextType { getCollectionField: ( variableString: string, localVariables?: VariableOption | VariableOption[], - ) => Promise; + ) => Promise; removeVariable: (variableName: string) => void; } diff --git a/packages/core/client/src/variables/utils/transformVariableValue.ts b/packages/core/client/src/variables/utils/transformVariableValue.ts index a20266c2b..2d3d157eb 100644 --- a/packages/core/client/src/variables/utils/transformVariableValue.ts +++ b/packages/core/client/src/variables/utils/transformVariableValue.ts @@ -1,11 +1,11 @@ import _ from 'lodash'; -import { CollectionFieldOptions } from '../../collection-manager'; +import { CollectionFieldOptions_deprecated } from '../../collection-manager'; interface Deps { /** * 消费当前变量值的 collection field,根据其值去判断应该怎么转换变量值 */ - targetCollectionField: CollectionFieldOptions; + targetCollectionField: CollectionFieldOptions_deprecated; } /** diff --git a/packages/core/data-source-manager/.npmignore b/packages/core/data-source-manager/.npmignore new file mode 100644 index 000000000..65f5e8779 --- /dev/null +++ b/packages/core/data-source-manager/.npmignore @@ -0,0 +1,2 @@ +/node_modules +/src diff --git a/packages/core/data-source-manager/LICENSE b/packages/core/data-source-manager/LICENSE new file mode 100644 index 000000000..f49a4e16e --- /dev/null +++ b/packages/core/data-source-manager/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/packages/core/data-source-manager/package.json b/packages/core/data-source-manager/package.json new file mode 100644 index 000000000..1209b3310 --- /dev/null +++ b/packages/core/data-source-manager/package.json @@ -0,0 +1,23 @@ +{ + "name": "@nocobase/data-source-manager", + "version": "0.19.0-alpha.9", + "description": "", + "license": "Apache-2.0", + "main": "./lib/index.js", + "types": "./lib/index.d.ts", + "dependencies": { + "@nocobase/actions": "0.19.0-alpha.9", + "@nocobase/cache": "0.19.0-alpha.9", + "@nocobase/database": "0.19.0-alpha.9", + "@nocobase/resourcer": "0.19.0-alpha.9", + "@nocobase/utils": "0.19.0-alpha.9", + "@types/jsonwebtoken": "^8.5.8", + "jsonwebtoken": "^8.5.1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nocobase/nocobase.git", + "directory": "packages/auth" + }, + "gitHead": "979a9c59a98c61a2287dd847580746a9b597cbde" +} diff --git a/packages/core/data-source-manager/src/__tests__/data-source-factory.test.ts b/packages/core/data-source-manager/src/__tests__/data-source-factory.test.ts new file mode 100644 index 000000000..1f2f7e8c0 --- /dev/null +++ b/packages/core/data-source-manager/src/__tests__/data-source-factory.test.ts @@ -0,0 +1,41 @@ +import { createMockServer } from '@nocobase/test'; +import { DataSource } from '../data-source'; +import { ICollectionManager } from '../types'; + +describe('data source factory', () => { + it('should register data source type', async () => { + class MockDataSource extends DataSource { + createCollectionManager(options?: any): ICollectionManager { + return undefined; + } + } + + const app = await createMockServer({ + acl: false, + resourcer: { + prefix: '/api/', + }, + name: 'test-app-1', + }); + + app.dataSourceManager.factory.register('mock', MockDataSource); + + const ds = app.dataSourceManager.factory.create('mock'); + + expect(ds).toBeInstanceOf(MockDataSource); + }); + + it('should throw error when data source type not found', async () => { + const app = await createMockServer({ + acl: false, + resourcer: { + prefix: '/api/', + }, + name: 'test-app-2', + }); + + expect(() => { + app.dataSourceManager.factory.create('mock'); + }).toThrowError('Data source type "mock" not found'); + }); +}); diff --git a/packages/core/data-source-manager/src/__tests__/data-source-manager.test.ts b/packages/core/data-source-manager/src/__tests__/data-source-manager.test.ts new file mode 100644 index 000000000..2b4002260 --- /dev/null +++ b/packages/core/data-source-manager/src/__tests__/data-source-manager.test.ts @@ -0,0 +1,64 @@ +import { createMockServer, mockDatabase, supertest } from '@nocobase/test'; +import { SequelizeDataSource } from '../sequelize-data-source'; + +describe('example', () => { + test.skip('case1', async () => { + const app = await createMockServer({ + acl: false, + resourcer: { + prefix: '/api/', + }, + name: 'test1', + }); + let body; + app.use(async (ctx, next) => { + body = ctx.request.body; + await next(); + }); + const agent = supertest.agent(app.callback()); + await agent.post('/api/test1:create').set('x-data-source', 'ds1').send({ name: 'n1' }); + expect(body.name).toBe('n1'); + await app.destroy(); + }); + + test('case2', async () => { + const app = await createMockServer({ + acl: false, + resourcer: { + prefix: '/api/', + }, + name: 'test2', + }); + + const database = mockDatabase({ + tablePrefix: 'ds1_', + }); + await database.clean({ drop: true }); + const ds1 = new SequelizeDataSource({ + name: 'ds1', + resourceManager: {}, + collectionManager: { + database, + }, + }); + ds1.collectionManager.defineCollection({ + name: 'test1', + fields: [{ type: 'string', name: 'name' }], + }); + await ds1.collectionManager.sync(); + ds1.acl.allow('test1', 'create', 'public'); + app.dataSourceManager.add(ds1); + const res = await app + .agent() + .post('/api/test1:create') + .set('x-data-source', 'ds1') + .send({ name: 'n1' }) + .auth('abc', { type: 'bearer' }) + .set('X-Authenticator', 'basic'); + expect(res.status).toBe(200); + const r = ds1.collectionManager.getRepository('test1'); + const m = await r.findOne(); + expect(m.name).toBe('n1'); + await app.destroy(); + }); +}); diff --git a/packages/core/data-source-manager/src/__tests__/koa.test.ts b/packages/core/data-source-manager/src/__tests__/koa.test.ts new file mode 100644 index 000000000..54193b29e --- /dev/null +++ b/packages/core/data-source-manager/src/__tests__/koa.test.ts @@ -0,0 +1,45 @@ +import { mockDatabase } from '@nocobase/test'; +import Koa from 'koa'; +import bodyParser from 'koa-bodyparser'; +import supertest from 'supertest'; +import { DataSourceManager } from '../data-source-manager'; +import { SequelizeDataSource } from '../sequelize-data-source'; + +describe('example', () => { + test('case1', async () => { + const app = new Koa(); + const dsm = new DataSourceManager(); + app.use(bodyParser()); + app.use(async (ctx, next) => { + console.log('middleware2...', ctx.request.body); + await next(); + }); + app.use(dsm.middleware()); + const database = mockDatabase({ + tablePrefix: 'ds1_', + }); + await database.clean({ drop: true }); + const ds1 = new SequelizeDataSource({ + name: 'ds1', + resourceManager: {}, + collectionManager: { + database, + }, + }); + + ds1.collectionManager.defineCollection({ + name: 'test1', + fields: [{ type: 'string', name: 'name' }], + }); + await ds1.collectionManager.sync(); + ds1.acl.allow('test1', 'create', 'public'); + dsm.add(ds1); + const agent = supertest.agent(app.callback()); + const res = await agent.post('/api/test1:create').set('x-data-source', 'ds1').send({ name: 'n1' }); + console.log(res.status); + expect(res.status).toBe(200); + const r = ds1.collectionManager.getRepository('test1'); + const m = await r.findOne(); + expect(m.name).toBe('n1'); + }); +}); diff --git a/packages/core/data-source-manager/src/collection-field.ts b/packages/core/data-source-manager/src/collection-field.ts new file mode 100644 index 000000000..5e747c94e --- /dev/null +++ b/packages/core/data-source-manager/src/collection-field.ts @@ -0,0 +1,15 @@ +import { FieldOptions, IField } from './types'; + +export class CollectionField implements IField { + options; + constructor(options: FieldOptions) { + this.updateOptions(options); + } + + updateOptions(options: any) { + this.options = { + ...this.options, + ...options, + }; + } +} diff --git a/packages/core/data-source-manager/src/collection-manager.ts b/packages/core/data-source-manager/src/collection-manager.ts new file mode 100644 index 000000000..62e0ac0ee --- /dev/null +++ b/packages/core/data-source-manager/src/collection-manager.ts @@ -0,0 +1,74 @@ +import { Collection } from './collection'; +import { CollectionOptions, ICollection, ICollectionManager, IRepository, MergeOptions } from './types'; + +export class CollectionManager implements ICollectionManager { + protected collections = new Map(); + protected repositories = new Map(); + protected models = new Map(); + + constructor(options = {}) {} + + getRegisteredFieldType(type) {} + getRegisteredFieldInterface(key: string) {} + + getRegisteredModel(key: string) { + return this.models.get(key); + } + + getRegisteredRepository(key: any) { + if (typeof key !== 'string') { + return key; + } + return this.repositories.get(key); + } + + registerFieldTypes() {} + registerFieldInterfaces() {} + registerCollectionTemplates() {} + + registerModels(models: Record) { + Object.keys(models).forEach((key) => { + this.models.set(key, models[key]); + }); + } + + registerRepositories(repositories: Record) { + Object.keys(repositories).forEach((key) => { + this.repositories.set(key, repositories[key]); + }); + } + + defineCollection(options: CollectionOptions): ICollection { + const collection = this.newCollection(options); + this.collections.set(options.name, collection); + return collection; + } + + extendCollection(collectionOptions: CollectionOptions, mergeOptions?: MergeOptions): ICollection { + const collection = this.getCollection(collectionOptions.name); + return collection; + } + + hasCollection(name: string) { + return !!this.getCollection(name); + } + + getCollection(name: string) { + return this.collections.get(name); + } + + getCollections(): Array { + return [...this.collections.values()]; + } + + getRepository(name: string, sourceId?: string | number): IRepository { + const collection = this.getCollection(name); + return collection.repository; + } + + async sync() {} + + protected newCollection(options) { + return new Collection(options, this); + } +} diff --git a/packages/core/data-source-manager/src/collection.ts b/packages/core/data-source-manager/src/collection.ts new file mode 100644 index 000000000..b496112ab --- /dev/null +++ b/packages/core/data-source-manager/src/collection.ts @@ -0,0 +1,55 @@ +import { CollectionOptions, ICollection, ICollectionManager, IField, IRepository } from './types'; +import { default as lodash } from 'lodash'; +import merge from 'deepmerge'; +import { CollectionField } from './collection-field'; + +export class Collection implements ICollection { + repository: IRepository; + fields: Map = new Map(); + + constructor( + protected options: CollectionOptions, + protected collectionManager: ICollectionManager, + ) { + this.setRepository(options.repository); + if (options.fields) { + for (const field of options.fields) { + this.setField(field.name, field); + } + } + } + + updateOptions(options: CollectionOptions, mergeOptions?: any) { + let newOptions = lodash.cloneDeep(options); + newOptions = merge(this.options, newOptions, mergeOptions); + this.options = newOptions; + + if (options.repository) { + this.setRepository(options.repository); + } + + return this; + } + + setField(name: string, options: any) { + const field = new CollectionField(options); + this.fields.set(name, field); + return field; + } + + removeField(name: string) { + this.fields.delete(name); + } + + getField(name: string) { + return this.fields.get(name); + } + + getFields() { + return [...this.fields.values()]; + } + + protected setRepository(repository: any) { + this.repository = this.collectionManager.getRegisteredRepository(repository || 'Repository'); + } +} diff --git a/packages/core/data-source-manager/src/data-source-factory.ts b/packages/core/data-source-manager/src/data-source-factory.ts new file mode 100644 index 000000000..ec1043d9e --- /dev/null +++ b/packages/core/data-source-manager/src/data-source-factory.ts @@ -0,0 +1,23 @@ +import { DataSource } from './data-source'; + +export class DataSourceFactory { + public collectionTypes: Map = new Map(); + + register(type: string, dataSourceClass: typeof DataSource) { + this.collectionTypes.set(type, dataSourceClass); + } + + getClass(type: string): typeof DataSource { + return this.collectionTypes.get(type); + } + + create(type: string, options: any = {}): DataSource { + const klass = this.collectionTypes.get(type); + if (!klass) { + throw new Error(`Data source type "${type}" not found`); + } + + // @ts-ignore + return new klass(options); + } +} diff --git a/packages/core/data-source-manager/src/data-source-manager.ts b/packages/core/data-source-manager/src/data-source-manager.ts new file mode 100644 index 000000000..8f1fb77de --- /dev/null +++ b/packages/core/data-source-manager/src/data-source-manager.ts @@ -0,0 +1,41 @@ +import { ToposortOptions } from '@nocobase/utils'; +import { DataSource } from './data-source'; +import { DataSourceFactory } from './data-source-factory'; + +export class DataSourceManager { + dataSources: Map; + factory: DataSourceFactory = new DataSourceFactory(); + + protected middlewares = []; + + constructor(public options = {}) { + this.dataSources = new Map(); + this.middlewares = []; + } + + async add(dataSource: DataSource, options: any = {}) { + await dataSource.load(options); + this.dataSources.set(dataSource.name, dataSource); + } + + use(fn: any, options?: ToposortOptions) { + this.middlewares.push([fn, options]); + } + + middleware() { + return async (ctx, next) => { + const name = ctx.get('x-data-source'); + if (name) { + if (this.dataSources.has(name)) { + const ds = this.dataSources.get(name); + ctx.dataSource = ds; + return ds.middleware(this.middlewares)(ctx, next); + } else { + ctx.throw(`data source ${name} does not exist`); + } + } + await next(); + console.log('next....'); + }; + } +} diff --git a/packages/core/data-source-manager/src/data-source-with-database.ts b/packages/core/data-source-manager/src/data-source-with-database.ts new file mode 100644 index 000000000..d9220bddb --- /dev/null +++ b/packages/core/data-source-manager/src/data-source-with-database.ts @@ -0,0 +1,5 @@ +import Database from '@nocobase/database'; + +export interface DataSourceWithDatabase { + db: Database; +} diff --git a/packages/core/data-source-manager/src/data-source.ts b/packages/core/data-source-manager/src/data-source.ts new file mode 100644 index 000000000..d0c09d8c9 --- /dev/null +++ b/packages/core/data-source-manager/src/data-source.ts @@ -0,0 +1,104 @@ +import { ACL } from '@nocobase/acl'; +import { getNameByParams, parseRequest } from '@nocobase/resourcer'; +import compose from 'koa-compose'; +import { ResourceManager } from './resource-manager'; +import { loadDefaultActions } from './load-default-actions'; +import { ICollectionManager } from './types'; +import EventEmitter from 'events'; + +export type DataSourceOptions = any; + +export abstract class DataSource extends EventEmitter { + public collectionManager: ICollectionManager; + public resourceManager: ResourceManager; + public acl: ACL; + + constructor(protected options: DataSourceOptions) { + super(); + this.init(options); + } + + get name() { + return this.options.name; + } + + static testConnection(options?: any): Promise { + return Promise.resolve(true); + } + + init(options: DataSourceOptions = {}) { + this.acl = this.createACL(); + + this.resourceManager = this.createResourceManager({ + prefix: '/api', + ...options.resourceManager, + }); + + this.collectionManager = this.createCollectionManager(options); + this.resourceManager.registerActionHandlers(loadDefaultActions(this)); + + if (options.acl !== false) { + this.resourceManager.use(this.acl.middleware(), { tag: 'acl', after: ['auth'] }); + } + } + + collectionToResourceMiddleware() { + return async (ctx, next) => { + const params = parseRequest( + { + path: ctx.request.path, + method: ctx.request.method, + }, + { + prefix: this.resourceManager.options.prefix, + accessors: this.resourceManager.options.accessors, + }, + ); + if (!params) { + return next(); + } + const resourceName = getNameByParams(params); + // 如果资源名称未被定义 + if (this.resourceManager.isDefined(resourceName)) { + return next(); + } + // 如果经过加载后是已经定义的表 + if (!this.collectionManager.hasCollection(resourceName)) { + return next(); + } + this.resourceManager.define({ + name: resourceName, + }); + return next(); + }; + } + + middleware(middlewares: any = []) { + if (!this['_used']) { + for (const [fn, options] of middlewares) { + this.resourceManager.use(fn, options); + } + this['_used'] = true; + } + return async (ctx, next) => { + ctx.getCurrentRepository = () => { + const { resourceName, resourceOf } = ctx.action; + return this.collectionManager.getRepository(resourceName, resourceOf); + }; + + return compose([this.collectionToResourceMiddleware(), this.resourceManager.restApiMiddleware()])(ctx, next); + }; + } + + createACL() { + return new ACL(); + } + + createResourceManager(options) { + return new ResourceManager(options); + } + + async load(options: any = {}) {} + + abstract createCollectionManager(options?: any): ICollectionManager; +} diff --git a/packages/core/data-source-manager/src/default-actions/list.ts b/packages/core/data-source-manager/src/default-actions/list.ts new file mode 100644 index 000000000..399365f33 --- /dev/null +++ b/packages/core/data-source-manager/src/default-actions/list.ts @@ -0,0 +1,75 @@ +import { assign } from '@nocobase/utils'; +import { getRepositoryFromParams, pageArgsToLimitArgs } from './utils'; +import { Context } from '@nocobase/actions'; + +function totalPage(total, pageSize): number { + return Math.ceil(total / pageSize); +} + +function findArgs(ctx: Context) { + const resourceName = ctx.action.resourceName; + const params = ctx.action.params; + + if (params.tree) { + const [collectionName, associationName] = resourceName.split('.'); + const collection = ctx.db.getCollection(resourceName); + if (collection.options.tree && !(associationName && collectionName === collection.name)) { + const foreignKey = collection.treeParentField?.foreignKey || 'parentId'; + assign(params, { filter: { [foreignKey]: null } }, { filter: 'andMerge' }); + } + } + + const { tree, fields, filter, appends, except, sort } = params; + + return { tree, filter, fields, appends, except, sort }; +} + +async function listWithPagination(ctx: Context) { + const { page = 1, pageSize = 50 } = ctx.action.params; + + const repository = getRepositoryFromParams(ctx); + + const options = { + context: ctx, + ...findArgs(ctx), + ...pageArgsToLimitArgs(parseInt(String(page)), parseInt(String(pageSize))), + }; + + Object.keys(options).forEach((key) => { + if (options[key] === undefined) { + delete options[key]; + } + }); + + const [rows, count] = await repository.findAndCount(options); + + ctx.body = { + count, + rows, + page: Number(page), + pageSize: Number(pageSize), + totalPage: totalPage(count, pageSize), + }; +} + +async function listWithNonPaged(ctx: Context) { + const repository = getRepositoryFromParams(ctx); + + const rows = await repository.find({ context: ctx, ...findArgs(ctx) }); + + ctx.body = rows; +} + +export async function list(ctx: Context, next) { + const { paginate } = ctx.action.params; + + if (paginate === false || paginate === 'false') { + await listWithNonPaged(ctx); + ctx.paginate = false; + } else { + await listWithPagination(ctx); + ctx.paginate = true; + } + + await next(); +} diff --git a/packages/core/data-source-manager/src/default-actions/move.ts b/packages/core/data-source-manager/src/default-actions/move.ts new file mode 100644 index 000000000..120420f2f --- /dev/null +++ b/packages/core/data-source-manager/src/default-actions/move.ts @@ -0,0 +1,22 @@ +import actions, { Context } from '@nocobase/actions'; + +import { getRepositoryFromParams } from './utils'; + +const databaseMoveAction = actions.move; + +export async function move(ctx: Context, next) { + const repository = getRepositoryFromParams(ctx); + + if (repository.move) { + ctx.body = await repository.move(ctx.action.params); + await next(); + return; + } + + if (repository.database) { + ctx.databaseRepository = repository; + return databaseMoveAction(ctx, next); + } + + throw new Error(`Repository can not handle action move for ${ctx.action.resourceName}`); +} diff --git a/packages/core/data-source-manager/src/default-actions/proxy-to-repository.ts b/packages/core/data-source-manager/src/default-actions/proxy-to-repository.ts new file mode 100644 index 000000000..7b71d2d9a --- /dev/null +++ b/packages/core/data-source-manager/src/default-actions/proxy-to-repository.ts @@ -0,0 +1,21 @@ +import { getRepositoryFromParams } from './utils'; +import lodash from 'lodash'; +import { DataSource } from '@nocobase/data-source-manager'; + +export function proxyToRepository(paramKeys: string[] | ((ctx: any) => object), repositoryMethod: string) { + return async function (ctx, next) { + const repository = getRepositoryFromParams(ctx); + const callObj = + typeof paramKeys === 'function' ? paramKeys(ctx) : { ...lodash.pick(ctx.action.params, paramKeys), context: ctx }; + const dataSource: DataSource = ctx.dataSource; + + if (!repository[repositoryMethod]) { + throw new Error( + `Repository can not handle action ${repositoryMethod} for ${ctx.action.resourceName} in ${dataSource.name}`, + ); + } + + ctx.body = await repository[repositoryMethod](callObj); + await next(); + }; +} diff --git a/packages/core/data-source-manager/src/default-actions/utils.ts b/packages/core/data-source-manager/src/default-actions/utils.ts new file mode 100644 index 000000000..fe6520673 --- /dev/null +++ b/packages/core/data-source-manager/src/default-actions/utils.ts @@ -0,0 +1,27 @@ +import { Context } from '@nocobase/actions'; +import { DataSource, IRepository } from '@nocobase/data-source-manager'; + +export function pageArgsToLimitArgs( + page: number, + pageSize: number, +): { + offset: number; + limit: number; +} { + return { + offset: (page - 1) * pageSize, + limit: pageSize, + }; +} + +export function getRepositoryFromParams(ctx: Context): IRepository { + const { resourceName, resourceOf } = ctx.action; + + const dataSource: DataSource = ctx.dataSource; + + if (resourceOf) { + return dataSource.collectionManager.getRepository(resourceName, resourceOf); + } + + return dataSource.collectionManager.getRepository(resourceName); +} diff --git a/packages/core/data-source-manager/src/index.ts b/packages/core/data-source-manager/src/index.ts new file mode 100644 index 000000000..3abc107ee --- /dev/null +++ b/packages/core/data-source-manager/src/index.ts @@ -0,0 +1,11 @@ +export * from './collection-manager'; +export * from './data-source'; +export * from './data-source-manager'; +export * from './resource-manager'; +export * from './sequelize-collection-manager'; +export * from './sequelize-data-source'; + +export * from './load-default-actions'; +export * from './types'; + +export * from './data-source-with-database'; diff --git a/packages/core/data-source-manager/src/load-default-actions.ts b/packages/core/data-source-manager/src/load-default-actions.ts new file mode 100644 index 000000000..e1ba576fc --- /dev/null +++ b/packages/core/data-source-manager/src/load-default-actions.ts @@ -0,0 +1,77 @@ +import { list } from './default-actions/list'; +import { move } from './default-actions/move'; +import { proxyToRepository } from './default-actions/proxy-to-repository'; +import { DataSource } from './data-source'; + +type Actions = { [key: string]: { params: Array | ((ctx: any) => Array); method: string } }; + +const actions: Actions = { + add: { + params(ctx) { + return ctx.action.params.filterByTk || ctx.action.params.filterByTks || ctx.action.params.values; + }, + method: 'add', + }, + create: { + params: ['whitelist', 'blacklist', 'updateAssociationValues', 'values'], + method: 'create', + }, + get: { + params: ['filterByTk', 'fields', 'appends', 'except', 'filter', 'targetCollection'], + method: 'findOne', + }, + update: { + params: [ + 'filterByTk', + 'values', + 'whitelist', + 'blacklist', + 'filter', + 'updateAssociationValues', + 'forceUpdate', + 'targetCollection', + ], + method: 'update', + }, + destroy: { + params: ['filterByTk', 'filter'], + method: 'destroy', + }, + firstOrCreate: { + params: ['values', 'filterKeys'], + method: 'firstOrCreate', + }, + updateOrCreate: { + params: ['values', 'filterKeys'], + method: 'updateOrCreate', + }, + remove: { + params(ctx) { + return ctx.action.params.filterByTk || ctx.action.params.filterByTks || ctx.action.params.values; + }, + method: 'remove', + }, + set: { + params(ctx) { + return ctx.action.params.filterByTk || ctx.action.params.filterByTks || ctx.action.params.values; + }, + method: 'set', + }, + toggle: { + params(ctx) { + return ctx.action.params.values; + }, + method: 'toggle', + }, +}; + +export function loadDefaultActions(dataSource: DataSource) { + return { + ...Object.keys(actions).reduce((carry, key) => { + carry[key] = proxyToRepository(actions[key].params, actions[key].method); + return carry; + }, {}), + list, + move, + }; +} diff --git a/packages/core/data-source-manager/src/repository.ts b/packages/core/data-source-manager/src/repository.ts new file mode 100644 index 000000000..95593e265 --- /dev/null +++ b/packages/core/data-source-manager/src/repository.ts @@ -0,0 +1,24 @@ +import { IModel, IRepository } from './types'; +import * as console from 'console'; + +export class Repository implements IRepository { + async create(options) { + console.log('Repository.create....'); + } + async update(options) {} + async find(options?: any): Promise { + return []; + } + async findOne(options?: any): Promise { + return {}; + } + async destroy(options) {} + + count(options?: any): Promise { + return Promise.resolve(undefined); + } + + findAndCount(options?: any): Promise<[IModel[], Number]> { + return Promise.resolve([[], undefined]); + } +} diff --git a/packages/core/data-source-manager/src/resource-manager.ts b/packages/core/data-source-manager/src/resource-manager.ts new file mode 100644 index 000000000..5aba4c26f --- /dev/null +++ b/packages/core/data-source-manager/src/resource-manager.ts @@ -0,0 +1,3 @@ +import { Resourcer } from '@nocobase/resourcer'; + +export class ResourceManager extends Resourcer {} diff --git a/packages/core/data-source-manager/src/sequelize-collection-manager.ts b/packages/core/data-source-manager/src/sequelize-collection-manager.ts new file mode 100644 index 000000000..41a97f410 --- /dev/null +++ b/packages/core/data-source-manager/src/sequelize-collection-manager.ts @@ -0,0 +1,88 @@ +import Database from '@nocobase/database'; +import { CollectionOptions, ICollection, ICollectionManager, IRepository, MergeOptions } from './types'; + +export class SequelizeCollectionManager implements ICollectionManager { + db: Database; + options: any; + constructor(options) { + this.db = this.createDB(options); + this.options = options; + } + + collectionsFilter() { + if (this.options.collectionsFilter) { + return this.options.collectionsFilter; + } + + return (collection) => { + return collection.options.introspected; + }; + } + + createDB(options: any = {}) { + if (options.database instanceof Database) { + return options.database; + } + + return new Database(options); + } + + registerFieldTypes(types: Record) { + this.db.registerFieldTypes(types); + } + + registerFieldInterfaces() {} + + registerCollectionTemplates() {} + + registerModels(models: Record) { + return this.db.registerModels(models); + } + + registerRepositories(repositories: Record) { + return this.db.registerModels(repositories); + } + + getRegisteredRepository(key: any) { + if (typeof key !== 'string') { + return key; + } + return this.db.repositories.get(key); + } + + defineCollection(options: CollectionOptions) { + const collection = this.db.collection(options); + // @ts-ignore + collection.model.refreshAttributes(); + + // @ts-ignore + collection.model._findAutoIncrementAttribute(); + return collection; + } + + extendCollection(collectionOptions: CollectionOptions, mergeOptions?: MergeOptions): ICollection { + return this.db.extendCollection(collectionOptions, mergeOptions) as unknown as ICollection; + } + + hasCollection(name: string) { + return this.db.hasCollection(name); + } + + getCollection(name: string) { + return this.db.getCollection(name); + } + + getCollections() { + const collectionsFilter = this.collectionsFilter(); + + return [...this.db.collections.values()].filter((collection) => collectionsFilter(collection)); + } + + getRepository(name: string, sourceId?: string | number): R { + return this.db.getRepository(name, sourceId) as R; + } + + async sync() { + await this.db.sync(); + } +} diff --git a/packages/core/data-source-manager/src/sequelize-data-source.ts b/packages/core/data-source-manager/src/sequelize-data-source.ts new file mode 100644 index 000000000..0a24d8ce2 --- /dev/null +++ b/packages/core/data-source-manager/src/sequelize-data-source.ts @@ -0,0 +1,10 @@ +import { DataSource } from './data-source'; +import { SequelizeCollectionManager } from './sequelize-collection-manager'; + +export class SequelizeDataSource extends DataSource { + async load() {} + + createCollectionManager(options?: any) { + return new SequelizeCollectionManager(options.collectionManager); + } +} diff --git a/packages/core/data-source-manager/src/types.ts b/packages/core/data-source-manager/src/types.ts new file mode 100644 index 000000000..887c3ae84 --- /dev/null +++ b/packages/core/data-source-manager/src/types.ts @@ -0,0 +1,75 @@ +export type CollectionOptions = { + name: string; + repository?: string; + fields: any[]; + [key: string]: any; +}; + +export type FieldOptions = { + name: string; + field: string; + rawType: string; + type: string; + description?: string; + interface?: string; + uiSchema?: any; + possibleTypes?: string[]; + defaultValue?: any; + primaryKey: boolean; + unique: boolean; + allowNull?: boolean; + autoIncrement?: boolean; + [key: string]: any; +}; + +export interface IField { + options: FieldOptions; +} +export interface ICollection { + repository: any; + updateOptions(options: any): void; + setField(name: string, options: any): IField; + removeField(name: string): void; + getFields(): Array; + getField(name: string): IField; + [key: string]: any; +} +export interface IModel { + [key: string]: any; +} + +export interface IRepository { + find(options?: any): Promise; + findOne(options?: any): Promise; + count(options?: any): Promise; + findAndCount(options?: any): Promise<[IModel[], Number]>; + create(options: any): void; + update(options: any): void; + destroy(options: any): void; + [key: string]: any; +} + +export type MergeOptions = { + [key: string]: any; +}; + +export interface ICollectionManager { + registerFieldTypes(types: Record): void; + registerFieldInterfaces(interfaces: Record): void; + registerCollectionTemplates(templates: Record): void; + registerModels(models: Record): void; + registerRepositories(repositories: Record): void; + + getRegisteredRepository(key: string): IRepository; + + defineCollection(options: CollectionOptions): ICollection; + + extendCollection(collectionOptions: CollectionOptions, mergeOptions?: MergeOptions): ICollection; + + hasCollection(name: string): boolean; + getCollection(name: string): ICollection; + + getCollections(): Array; + getRepository(name: string, sourceId?: string | number): IRepository; + sync(): Promise; +} diff --git a/packages/core/database/src/__tests__/fields/belongs-to-field.test.ts b/packages/core/database/src/__tests__/fields/belongs-to-field.test.ts index 30f57d790..abb81b887 100644 --- a/packages/core/database/src/__tests__/fields/belongs-to-field.test.ts +++ b/packages/core/database/src/__tests__/fields/belongs-to-field.test.ts @@ -14,6 +14,71 @@ describe('belongs to field', () => { await db.close(); }); + it('should throw error when associated with item that null with target key', async () => { + const User = db.collection({ + name: 'users', + fields: [{ type: 'string', name: 'name', unique: true }], + }); + + const Profile = db.collection({ + name: 'profiles', + autoGenId: true, + timestamps: false, + fields: [ + { + type: 'belongsTo', + name: 'user', + target: 'users', + foreignKey: 'userName', + targetKey: 'name', + }, + ], + }); + + await db.sync(); + + await expect( + Profile.repository.create({ + values: { + user: {}, + }, + }), + ).rejects.toThrow('The target key name is not set in users'); + }); + it('should check association keys type', async () => { + const User = db.collection({ + name: 'users', + fields: [ + { type: 'string', name: 'name' }, + { type: 'bigInt', name: 'description' }, + ], + }); + + let error; + try { + const Post = db.collection({ + name: 'posts', + fields: [ + { type: 'string', name: 'title' }, + { type: 'string', name: 'userName' }, + { + type: 'belongsTo', + name: 'user', + foreignKey: 'userName', + targetKey: 'description', // wrong type + }, + ], + }); + } catch (e) { + error = e; + } + + expect(error).toBeDefined(); + expect(error.message).toBe( + 'Foreign key "userName" type "STRING" does not match target key "description" type "BIGINT" in belongs to relation "user" of collection "posts"', + ); + }); + it('association undefined', async () => { const Comment = db.collection({ name: 'comments', diff --git a/packages/core/database/src/__tests__/fields/belongs-to-many-field.test.ts b/packages/core/database/src/__tests__/fields/belongs-to-many-field.test.ts index 15b94eb87..2570a0e6a 100644 --- a/packages/core/database/src/__tests__/fields/belongs-to-many-field.test.ts +++ b/packages/core/database/src/__tests__/fields/belongs-to-many-field.test.ts @@ -14,6 +14,57 @@ describe('belongs to many field', () => { await db.close(); }); + it('should check belongs to many association keys', async () => { + const PostTag = db.collection({ + name: 'postsTags', + fields: [ + { + type: 'bigInt', + name: 'id', + primaryKey: true, + }, + { + type: 'text', + name: 'postId', + }, + { + type: 'text', + name: 'tagId', + }, + ], + }); + + await db.sync(); + + const Post = db.collection({ + name: 'posts', + fields: [{ type: 'string', name: 'name' }], + }); + + const Tag = db.collection({ + name: 'tags', + fields: [{ type: 'string', name: 'name' }], + }); + + let error; + + try { + Post.setField('tags', { + type: 'belongsToMany', + through: 'postsTags', + target: 'tags', + foreignKey: 'postId', + otherKey: 'tagId', + }); + } catch (e) { + error = e; + } + + expect(error).toBeDefined(); + + console.log(error.message); + }); + it('should define belongs to many when change alias name', async () => { const Through = db.collection({ name: 't1', diff --git a/packages/core/database/src/__tests__/fields/has-many-field.test.ts b/packages/core/database/src/__tests__/fields/has-many-field.test.ts index 129b85754..ae20c69eb 100644 --- a/packages/core/database/src/__tests__/fields/has-many-field.test.ts +++ b/packages/core/database/src/__tests__/fields/has-many-field.test.ts @@ -14,6 +14,134 @@ describe('has many field', () => { await db.close(); }); + it('should create has many association', async () => { + const syncOptions = { + force: false, + alter: { + drop: false, + }, + }; + const A = db.collection({ + name: 'a', + autoGenId: false, + timestamps: false, + fields: [ + { + type: 'string', + name: 'field1', + primaryKey: true, + defaultValue: null, + }, + { + type: 'string', + name: 'name', + unique: true, + defaultValue: null, + }, + ], + }); + + await db.sync(syncOptions); + + const B = db.collection({ + name: 'b', + autoGenId: false, + timestamps: false, + fields: [ + { + type: 'string', + name: 'key1', + primaryKey: true, + unique: false, + defaultValue: null, + }, + { + type: 'string', + name: 'field2', + }, + ], + }); + + await db.sync(syncOptions); + + A.setField('fields', { + type: 'hasMany', + target: 'b', + sourceKey: 'name', + foreignKey: 'ttt_name', + }); + + await db.sync(syncOptions); + }); + + it('should throw error when associated with item that null with source key', async () => { + const User = db.collection({ + name: 'users', + autoGenId: true, + timestamps: false, + fields: [ + { type: 'string', name: 'name', unique: true }, + { + type: 'hasMany', + name: 'profiles', + target: 'profiles', + foreignKey: 'userName', + sourceKey: 'name', + }, + ], + }); + + const Profile = db.collection({ + name: 'profiles', + fields: [ + { + type: 'string', + name: 'address', + }, + ], + }); + + await db.sync(); + + await expect( + User.repository.create({ + values: { + profiles: [ + { + address: 'address1', + }, + ], + }, + }), + ).rejects.toThrow('The source key name is not set in users'); + }); + + it('should check association keys type', async () => { + const Post = db.collection({ + name: 'posts', + fields: [{ type: 'bigInt', name: 'title' }], + }); + + let error; + try { + const User = db.collection({ + name: 'users', + fields: [ + { type: 'string', name: 'name' }, + { + type: 'hasMany', + name: 'posts', + foreignKey: 'title', // wrong type + sourceKey: 'name', + }, + ], + }); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + }); + it('association undefined', async () => { const collection = db.collection({ name: 'posts', diff --git a/packages/core/database/src/__tests__/fields/has-one-field.test.ts b/packages/core/database/src/__tests__/fields/has-one-field.test.ts index 8046a093d..e964ed1f4 100644 --- a/packages/core/database/src/__tests__/fields/has-one-field.test.ts +++ b/packages/core/database/src/__tests__/fields/has-one-field.test.ts @@ -14,6 +14,75 @@ describe('has many field', () => { await db.close(); }); + it('should throw error when associated with item that null with source key', async () => { + const User = db.collection({ + name: 'users', + autoGenId: true, + timestamps: false, + fields: [ + { type: 'string', name: 'name', unique: true }, + { + type: 'hasOne', + name: 'profile', + target: 'profiles', + foreignKey: 'userName', + sourceKey: 'name', + }, + ], + }); + + const Profile = db.collection({ + name: 'profiles', + fields: [ + { + type: 'string', + name: 'address', + }, + ], + }); + + await db.sync(); + + await expect( + User.repository.create({ + values: { + profile: { + address: 'address1', + }, + }, + }), + ).rejects.toThrow('The source key name is not set in users'); + }); + + it('should check has one association keys type', async () => { + const Profile = db.collection({ + name: 'profiles', + fields: [{ type: 'bigInt', name: 'content' }], + }); + + let error; + try { + const User = db.collection({ + name: 'users', + fields: [ + { type: 'string', name: 'name' }, + { + type: 'hasOne', + name: 'profile', + foreignKey: 'content', // wrong type + sourceKey: 'name', + }, + ], + }); + } catch (e) { + error = e; + } + + expect(error).toBeDefined(); + expect(error.message).toBe( + 'Foreign key "content" type "BIGINT" does not match source key "name" type "STRING" in has one relation "profile" of collection "users"', + ); + }); it('association undefined', async () => { const User = db.collection({ name: 'users', diff --git a/packages/core/database/src/__tests__/inhertits/collection-inherits.test.ts b/packages/core/database/src/__tests__/inhertits/collection-inherits.test.ts index dba864cd5..d9e4c8ed0 100644 --- a/packages/core/database/src/__tests__/inhertits/collection-inherits.test.ts +++ b/packages/core/database/src/__tests__/inhertits/collection-inherits.test.ts @@ -2,9 +2,9 @@ import { BelongsToManyRepository } from '@nocobase/database'; import Database from '../../database'; import { InheritedCollection } from '../../inherited-collection'; import { mockDatabase } from '../index'; -import pgOnly from './helper'; +import { isPg } from '@nocobase/test'; -pgOnly()('collection inherits', () => { +describe.runIf(isPg())('collection inherits', () => { let db: Database; beforeEach(async () => { @@ -364,7 +364,7 @@ pgOnly()('collection inherits', () => { }); }); - it('should create inherits from empty table', async () => { + it('should throw error when create inherits from empty table', async () => { const empty = db.collection({ name: 'empty', timestamps: false, @@ -389,22 +389,20 @@ pgOnly()('collection inherits', () => { fields: [{ type: 'string', name: 'name' }], }); - await db.sync({ - force: false, - alter: { - drop: false, - }, - }); + let error; + try { + await db.sync({ + force: false, + alter: { + drop: false, + }, + }); + } catch (e) { + error = e; + } - expect(inherits instanceof InheritedCollection).toBeTruthy(); - - const record = await inherits.repository.create({ - values: { - name: 'test', - }, - }); - - expect(record.get('name')).toEqual('test'); + expect(error).toBeTruthy(); + expect(error.message.includes("can't inherit from")).toBeTruthy(); }); it('should not throw error when fields have same type with parent', async () => { diff --git a/packages/core/database/src/__tests__/non-id-primary-key.test.ts b/packages/core/database/src/__tests__/non-id-primary-key.test.ts new file mode 100644 index 000000000..2cda6a508 --- /dev/null +++ b/packages/core/database/src/__tests__/non-id-primary-key.test.ts @@ -0,0 +1,83 @@ +import { Database, mockDatabase } from '@nocobase/database'; + +describe('non-id primary key', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase({}); + + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should create auto increment field as primary key', async () => { + const User = db.collection({ + name: 'users', + autoGenId: false, + timestamps: false, + }); + await db.sync(); + + User.setField('field_auto_incr', { type: 'integer', primaryKey: true, autoIncrement: true }); + + await db.sync(); + + await User.repository.create({}); + }); + + it('should add createdAt && updatedAt field', async () => { + const User = db.collection({ + name: 'users', + autoGenId: false, + timestamps: false, + fields: [ + { + type: 'string', + name: 'name', + }, + ], + }); + + await db.sync(); + + User.setField('createdAt', { + uiSchema: { + 'x-component-props': { dateFormat: 'YYYY-MM-DD', showTime: true, timeFormat: 'HH:mm:ss' }, + type: 'datetime', + title: '{{t("Created at")}}', + 'x-component': 'DatePicker', + 'x-read-pretty': true, + }, + name: 'xxxx', + type: 'date', + field: 'createdAt', + interface: 'createdAt', + }); + + User.setField('updatedAt', { + uiSchema: { + 'x-component-props': { dateFormat: 'YYYY-MM-DD', showTime: true, timeFormat: 'HH:mm:ss' }, + type: 'datetime', + title: '{{t("Updated at")}}', + 'x-component': 'DatePicker', + 'x-read-pretty': true, + }, + name: 'updatedAttt', + type: 'date', + field: 'updatedAt', + interface: 'updatedAt', + }); + + await db.sync(); + + const user = await User.repository.create({ + values: { name: 'test' }, + }); + + expect(user.get('xxxx')).toBeTruthy(); + expect(user.get('updatedAttt')).toBeTruthy(); + }); +}); diff --git a/packages/core/database/src/__tests__/sync/default-value.test.ts b/packages/core/database/src/__tests__/sync/default-value.test.ts new file mode 100644 index 000000000..e46a5d7cc --- /dev/null +++ b/packages/core/database/src/__tests__/sync/default-value.test.ts @@ -0,0 +1,49 @@ +import { Database, mockDatabase } from '../../index'; + +describe('default value', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase({}); + + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should sync field default value', async () => { + const User = db.collection({ + name: 'users', + fields: [ + { type: 'string', name: 'userName', defaultValue: 'u1' }, + { type: 'string', name: 'userEmail' }, + ], + }); + + await db.sync(); + + const getFieldDefaultValue = (tableInfo, fieldName) => { + const columnName = User.model.rawAttributes[fieldName].field; + + const field = tableInfo[columnName]; + + return field.defaultValue || null; + }; + + const userTableInfo: any = await db.sequelize.getQueryInterface().describeTable(User.getTableNameWithSchema()); + + expect(getFieldDefaultValue(userTableInfo, 'userName')).toBe('u1'); + expect(getFieldDefaultValue(userTableInfo, 'userEmail')).toBeNull(); + + User.setField('userName', { type: 'string', name: 'userName' }); + User.setField('userEmail', { type: 'string', name: 'userEmail', defaultValue: 'e1' }); + + await db.sync(); + const userTableInfo2: any = await db.sequelize.getQueryInterface().describeTable(User.getTableNameWithSchema()); + + expect(getFieldDefaultValue(userTableInfo2, 'userName')).toBeNull(); + expect(getFieldDefaultValue(userTableInfo2, 'userEmail')).toBe('e1'); + }); +}); diff --git a/packages/core/database/src/__tests__/sync/delete-field.test.ts b/packages/core/database/src/__tests__/sync/delete-field.test.ts new file mode 100644 index 000000000..e04f37ac0 --- /dev/null +++ b/packages/core/database/src/__tests__/sync/delete-field.test.ts @@ -0,0 +1,38 @@ +import { Database, mockDatabase } from '../../index'; + +describe.skip('delete field', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase({}); + + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should delete field', async () => { + const User = db.collection({ + name: 'users', + fields: [ + { type: 'string', name: 'name' }, + { type: 'string', name: 'email' }, + ], + }); + + await db.sync(); + + const userTableInfo = await db.sequelize.getQueryInterface().describeTable(User.getTableNameWithSchema()); + + expect(userTableInfo.email).toBeDefined(); + + User.removeField('email'); + + await db.sync(); + + const userTableInfo2 = await db.sequelize.getQueryInterface().describeTable(User.getTableNameWithSchema()); + expect(userTableInfo2.email).toBeUndefined(); + }); +}); diff --git a/packages/core/database/src/__tests__/sync/empty-table.test.ts b/packages/core/database/src/__tests__/sync/empty-table.test.ts new file mode 100644 index 000000000..6d0416fb4 --- /dev/null +++ b/packages/core/database/src/__tests__/sync/empty-table.test.ts @@ -0,0 +1,107 @@ +import { Database, mockDatabase } from '@nocobase/database'; +import * as process from 'process'; + +describe('empty table', () => { + let db: Database; + const syncOptions = { + alter: { + drop: false, + }, + force: false, + }; + + beforeEach(async () => { + db = mockDatabase({}); + + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should change primary key after insert data', async () => { + const empty = db.collection({ + name: 'empty', + autoGenId: false, + timestamps: false, + }); + + await db.sync(syncOptions); + + empty.setField('code', { + type: 'string', + primaryKey: true, + }); + + await db.sync(syncOptions); + await empty.repository.create({ + values: [ + { + code: 'code1', + }, + { + code: 'code2', + }, + ], + }); + + // remove code field + await empty.removeFieldFromDb('code'); + await db.sync(syncOptions); + + empty.setField('code2', { + type: 'string', + primaryKey: true, + }); + + await db.sync(syncOptions); + + await empty.repository.create({ + values: [ + { + code2: 'code1', + }, + { + code2: 'code2', + }, + ], + }); + }); + + it.skipIf(process.env['DB_DIALECT'] == 'sqlite')('should add primary key field into empty table', async () => { + const empty = db.collection({ + name: 'empty', + autoGenId: false, + }); + + await db.sync(syncOptions); + + empty.setField('code', { + type: 'string', + primaryKey: true, + }); + + await db.sync(syncOptions); + + const emptyInfo = await db.sequelize.getQueryInterface().describeTable(empty.getTableNameWithSchema()); + expect(emptyInfo.code.primaryKey).toBeTruthy(); + + const empty2 = db.collection({ + name: 'empty2', + autoGenId: false, + }); + + await db.sync(syncOptions); + + empty2.setField('code', { + type: 'string', + primaryKey: true, + }); + + await db.sync(syncOptions); + + const empty2Info = await db.sequelize.getQueryInterface().describeTable(empty2.getTableNameWithSchema()); + expect(empty2Info.code.primaryKey).toBeTruthy(); + }); +}); diff --git a/packages/core/database/src/__tests__/sync/primary-key.test.ts b/packages/core/database/src/__tests__/sync/primary-key.test.ts new file mode 100644 index 000000000..a43caae11 --- /dev/null +++ b/packages/core/database/src/__tests__/sync/primary-key.test.ts @@ -0,0 +1,124 @@ +import { Database, mockDatabase } from '../../index'; +import * as process from 'process'; + +describe('primary key', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase({}); + + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should create primary key without auto increment', async () => { + const User = db.collection({ + name: 'users', + autoGenId: false, + timestamps: false, + }); + + await db.sync(); + + User.setField('someField', { + primaryKey: true, + type: 'string', + defaultValue: '12321', + }); + + await db.sync(); + + const getTableInfo = async () => { + return await db.sequelize.getQueryInterface().describeTable(User.getTableNameWithSchema()); + }; + + const assertPrimaryKey = async (fieldName, primaryKey) => { + const tableInfo = await getTableInfo(); + const field = User.model.rawAttributes[fieldName].field; + expect(tableInfo[field].primaryKey).toBe(primaryKey); + }; + + await assertPrimaryKey('someField', true); + }); + + it('should create primary key with auto increment', async () => { + const User = db.collection({ + name: 'users', + autoGenId: false, + timestamps: false, + }); + + await db.sync(); + + User.setField('someField', { + primaryKey: true, + autoIncrement: true, + type: 'integer', + defaultValue: null, + }); + + await db.sync(); + + const getTableInfo = async () => { + return await db.sequelize.getQueryInterface().describeTable(User.getTableNameWithSchema()); + }; + + const assertPrimaryKey = async (fieldName, primaryKey) => { + const tableInfo = await getTableInfo(); + const field = User.model.rawAttributes[fieldName].field; + expect(tableInfo[field].primaryKey).toBe(primaryKey); + }; + + await assertPrimaryKey('someField', true); + }); +}); +describe.skipIf(process.env['DB_DIALECT'] === 'sqlite')('primary key not in sqlite', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase({}); + + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should change primary key', async () => { + const User = db.collection({ + name: 'users', + autoGenId: false, + timestamps: false, + }); + + await db.sync(); + + const getTableInfo = async () => { + return await db.sequelize.getQueryInterface().describeTable(User.getTableNameWithSchema()); + }; + + const assertPrimaryKey = async (fieldName, primaryKey) => { + const tableInfo = await getTableInfo(); + const field = User.model.rawAttributes[fieldName].field; + expect(tableInfo[field].primaryKey).toBe(primaryKey); + }; + + // add a field as primary key + User.setField('name', { type: 'string', name: 'name', primaryKey: true }); + await db.sync(); + + await assertPrimaryKey('name', true); + // use another field as primary key + User.setField('name', { type: 'string', name: 'name', primaryKey: false }); + await db.sync(); + await assertPrimaryKey('name', false); + + User.setField('fullName', { type: 'string', name: 'fullName', primaryKey: true }); + await db.sync(); + await assertPrimaryKey('fullName', true); + }); +}); diff --git a/packages/core/database/src/__tests__/sync/unique-index.test.ts b/packages/core/database/src/__tests__/sync/unique-index.test.ts new file mode 100644 index 000000000..31194a5ad --- /dev/null +++ b/packages/core/database/src/__tests__/sync/unique-index.test.ts @@ -0,0 +1,112 @@ +import { Database, mockDatabase } from '../../index'; +import { waitSecond } from '@nocobase/test'; + +describe('unique index', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase({}); + + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should sync multiple column unique index', async () => { + const User = db.collection({ + name: 'users', + indexes: [ + { + unique: true, + fields: ['userName', 'userEmail'], + }, + ], + fields: [ + { type: 'string', name: 'userName', defaultValue: 0 }, + { type: 'string', name: 'userEmail' }, + ], + }); + + await db.sync(); + + expect(async () => { + await User.repository.create({ + values: { + userName: 'test', + userEmail: 'test@nocobase.com', + }, + }); + }).not.toThrow(); + + await waitSecond(1000); + expect(async () => { + await User.repository.create({ + values: { + userName: 'test', + userEmail: 'test123@nocobase.com', + }, + }); + }).not.toThrow(); + + await waitSecond(1000); + + await expect( + User.repository.create({ + values: { + userName: 'test', + userEmail: 'test@nocobase.com', + }, + }), + ).rejects.toThrow(); + }); + + it('should sync unique index', async () => { + const User = db.collection({ + name: 'users', + fields: [ + { type: 'string', name: 'userName', unique: true }, + { type: 'string', name: 'userEmail' }, + ], + }); + + await db.sync(); + + const findFieldIndex = (indexes, fieldName) => { + const columnName = User.model.rawAttributes[fieldName].field; + + return indexes.find((index) => { + const indexField = index.fields; + if (!indexField) { + return false; + } + + if (typeof indexField == 'string') { + return indexField === columnName; + } + + return indexField.length == 1 && indexField[0].attribute === columnName; + }); + }; + + const userTableInfo: any = await db.sequelize.getQueryInterface().showIndex(User.getTableNameWithSchema()); + + const nameUniqueIndex = findFieldIndex(userTableInfo, 'userName'); + expect(nameUniqueIndex).toBeDefined(); + const emailUniqueIndex = findFieldIndex(userTableInfo, 'userEmail'); + expect(emailUniqueIndex).toBeUndefined(); + + User.setField('userName', { type: 'string', name: 'userName' }); + User.setField('userEmail', { type: 'string', name: 'userEmail', unique: true }); + + await db.sync(); + + const userTableInfo2: any = await db.sequelize.getQueryInterface().showIndex(User.getTableNameWithSchema()); + + const nameUniqueIndex2 = findFieldIndex(userTableInfo2, 'userName'); + expect(nameUniqueIndex2).toBeUndefined(); + const emailUniqueIndex2 = findFieldIndex(userTableInfo2, 'userEmail'); + expect(emailUniqueIndex2).toBeDefined(); + }); +}); diff --git a/packages/core/database/src/__tests__/update-associations-source-key.test.ts b/packages/core/database/src/__tests__/update-associations-source-key.test.ts new file mode 100644 index 000000000..0b1119c5b --- /dev/null +++ b/packages/core/database/src/__tests__/update-associations-source-key.test.ts @@ -0,0 +1,233 @@ +import { Collection } from '../collection'; +import Database from '../database'; +import { mockDatabase } from '../mock-database'; + +describe('update associations', () => { + let db: Database; + let User: Collection; + let Post: Collection; + + beforeEach(async () => { + db = mockDatabase(); + await db.clean({ drop: true }); + User = db.collection({ + name: 'users', + autoGenId: true, + timestamps: false, + fields: [ + { type: 'string', name: 'name' }, + { + type: 'hasMany', + name: 'posts', + target: 'posts', + foreignKey: 'userName', + sourceKey: 'name', + targetKey: 'title', + }, + ], + }); + + Post = db.collection({ + name: 'posts', + autoGenId: true, + timestamps: false, + fields: [ + { type: 'string', name: 'title' }, + { type: 'belongsTo', name: 'user', target: 'users', foreignKey: 'userName', targetKey: 'name' }, + ], + }); + await db.sync(); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should create user with posts', async () => { + await Post.repository.create({ + values: { + title: 'post2', + user: { + name: 'user1', + }, + }, + }); + + expect(await User.repository.count()).toBe(1); + }); + + it('should create user with posts', async () => { + await User.repository.create({ + values: { + name: 'user1', + posts: [ + { + title: 'post1', + }, + ], + }, + }); + await User.repository.create({ + values: { + name: 'user2', + posts: [ + { + title: 'post1', + }, + ], + }, + }); + + expect(await Post.repository.count()).toBe(2); + }); +}); + +describe('update associations', () => { + let db: Database; + let User: Collection; + let Post: Collection; + + beforeEach(async () => { + db = mockDatabase(); + await db.clean({ drop: true }); + User = db.collection({ + name: 'users', + autoGenId: false, + timestamps: false, + fields: [ + { type: 'string', name: 'name', primaryKey: true }, + { + type: 'hasMany', + name: 'posts', + target: 'posts', + foreignKey: 'userName', + sourceKey: 'name', + targetKey: 'title', + }, + ], + }); + + Post = db.collection({ + name: 'posts', + autoGenId: false, + timestamps: false, + fields: [ + { type: 'string', name: 'title', primaryKey: true }, + { type: 'belongsTo', name: 'user', target: 'users', foreignKey: 'userName', targetKey: 'name' }, + ], + }); + await db.sync(); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should create user with posts', async () => { + await Post.repository.create({ + values: { + title: 'post1', + }, + }); + await Post.repository.create({ + values: { + title: 'post2', + }, + }); + await User.repository.create({ + values: { + name: 'user1', + posts: [ + { + title: 'post1', + }, + ], + }, + }); + await User.repository.create({ + values: { + name: 'user2', + posts: [ + { + title: 'post2', + }, + ], + }, + }); + + expect(await Post.repository.count()).toBe(2); + }); +}); + +describe('update associations', () => { + let db: Database; + let Table: Collection; + let Field: Collection; + + beforeEach(async () => { + db = mockDatabase(); + await db.clean({ drop: true }); + Table = db.collection({ + name: 'table', + autoGenId: false, + timestamps: false, + fields: [ + { type: 'string', name: 'key', primaryKey: true }, + { type: 'string', name: 'name', unique: true }, + { + type: 'hasMany', + name: 'fields', + target: 'field', + foreignKey: 'table_name', + sourceKey: 'name', + targetKey: 'name', + }, + ], + }); + + Field = db.collection({ + name: 'field', + autoGenId: false, + timestamps: false, + fields: [ + { type: 'string', name: 'key', primaryKey: true }, + { type: 'string', name: 'name', unique: true }, + ], + }); + await db.sync(); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should create table with fields', async () => { + await Field.repository.create({ + values: { + key: 'f1', + name: 'n1', + }, + }); + await Field.repository.create({ + values: { + key: 'f2', + name: 'n2', + }, + }); + await Table.repository.create({ + values: { + key: 'tk1', + name: 'tn1', + fields: [ + { + key: 'f1', + name: 'n1', + }, + ], + }, + }); + const field = await Field.repository.findById('f1'); + expect(await Table.repository.count()).toBe(1); + expect(field.table_name).toBe('tn1'); + }); +}); diff --git a/packages/core/database/src/__tests__/update-associations.test.ts b/packages/core/database/src/__tests__/update-associations.test.ts index b9537cc1b..c2c6c7874 100644 --- a/packages/core/database/src/__tests__/update-associations.test.ts +++ b/packages/core/database/src/__tests__/update-associations.test.ts @@ -232,6 +232,8 @@ describe('update associations', () => { await db.close(); }); it('user.posts', async () => { + await User.model.create({ name: 'user01' }); + await User.model.create({ name: 'user02' }); const user1 = await User.model.create({ name: 'user1' }); await updateAssociations(user1, { posts: { diff --git a/packages/core/database/src/collection.ts b/packages/core/database/src/collection.ts index b33526745..9d9506463 100644 --- a/packages/core/database/src/collection.ts +++ b/packages/core/database/src/collection.ts @@ -151,12 +151,14 @@ export class Collection< } get filterTargetKey() { - const targetKey = lodash.get(this.options, 'filterTargetKey', this.model.primaryKeyAttribute); - if (!targetKey && this.model.rawAttributes['id']) { - return 'id'; + const targetKey = this.options?.filterTargetKey; + if (targetKey && this.model.getAttributes()[targetKey]) { + return targetKey; } - - return targetKey; + if (this.model.primaryKeyAttributes.length > 1) { + return null; + } + return this.model.primaryKeyAttribute; } get name() { @@ -270,6 +272,9 @@ export class Collection< return this.fields.get(name); } + getFields() { + return [...this.fields.values()]; + } addField(name: string, options: FieldOptions): Field { return this.setField(name, options); } @@ -300,6 +305,12 @@ export class Collection< } } + correctOptions(options) { + if (options.primaryKey && options.autoIncrement) { + delete options.defaultValue; + } + } + @EnsureAtomicity setField(name: string, options: FieldOptions): Field { checkIdentifier(name); @@ -327,6 +338,7 @@ export class Collection< } } + this.correctOptions(options); this.emit('field.beforeAdd', name, options, { collection: this }); const field = database.buildField( @@ -353,6 +365,11 @@ export class Collection< this.fields.set(name, field); this.emit('field.afterAdd', field); + this.db.emit('field.afterAdd', { + collection: this, + field, + }); + // refresh children models if (this.isParent()) { for (const child of this.context.database.inheritanceMap.getChildren(this.name, { @@ -423,11 +440,6 @@ export class Collection< return; } - if (this.model.primaryKeyAttributes.includes(this.name)) { - // 主键不能删除 - return; - } - if (this.model.options.timestamps !== false) { // timestamps 相关字段不删除 let timestampsFields = ['createdAt', 'updatedAt', 'deletedAt']; @@ -469,14 +481,27 @@ export class Collection< })) && columnReferencesCount == 1 ) { - const queryInterface = this.db.sequelize.getQueryInterface(); - await queryInterface.removeColumn(this.getTableNameWithSchema(), field.columnName(), options); + const columns = await this.model.sequelize + .getQueryInterface() + .describeTable(this.getTableNameWithSchema(), options); + + if (Object.keys(columns).length == 1) { + // remove table if only one column left + await this.removeFromDb({ + ...options, + cascade: true, + dropCollection: false, + }); + } else { + const queryInterface = this.db.sequelize.getQueryInterface(); + await queryInterface.removeColumn(this.getTableNameWithSchema(), field.columnName(), options); + } } field.remove(); } - async removeFromDb(options?: QueryInterfaceDropTableOptions) { + async removeFromDb(options?: QueryInterfaceDropTableOptions & { dropCollection?: boolean }) { if ( !this.isView() && (await this.existsInDb({ @@ -487,7 +512,9 @@ export class Collection< await queryInterface.dropTable(this.getTableNameWithSchema(), options); } - return this.remove(); + if (options?.dropCollection !== false) { + return this.remove(); + } } async existsInDb(options?: Transactionable) { diff --git a/packages/core/database/src/database.ts b/packages/core/database/src/database.ts index 4f3e75a3c..e0726dfdd 100644 --- a/packages/core/database/src/database.ts +++ b/packages/core/database/src/database.ts @@ -299,6 +299,9 @@ export class Database extends EventEmitter implements AsyncEmitter { this.sequelize.beforeDefine((model, opts) => { if (this.options.tablePrefix) { + if (opts.tableName && opts.tableName.startsWith(this.options.tablePrefix)) { + return; + } opts.tableName = `${this.options.tablePrefix}${opts.tableName || opts.modelName || opts.name.plural}`; } }); @@ -577,7 +580,7 @@ export class Database extends EventEmitter implements AsyncEmitter { } hasCollection(name: string): boolean { - return this.collections.has(name); + return !!this.getCollection(name); } removeCollection(name: string) { @@ -703,6 +706,10 @@ export class Database extends EventEmitter implements AsyncEmitter { options.field = snakeCase(options.field); } + if (Object.prototype.hasOwnProperty.call(options, 'defaultValue') && options.defaultValue === null) { + delete options.defaultValue; + } + return new Field(options, context); } diff --git a/packages/core/database/src/eager-loading/eager-loading-tree.ts b/packages/core/database/src/eager-loading/eager-loading-tree.ts index a53ee9ce1..615b81104 100644 --- a/packages/core/database/src/eager-loading/eager-loading-tree.ts +++ b/packages/core/database/src/eager-loading/eager-loading-tree.ts @@ -98,6 +98,12 @@ export class EagerLoadingTree { ? eagerLoadingTreeParent.model.associations[include.association] : include.association; + if (!association) { + throw new Error( + `Association "${include.association}" not found in model "${eagerLoadingTreeParent.model.name}"`, + ); + } + const associationType = association.associationType; const child = buildNode({ diff --git a/packages/core/database/src/errors/zero-column-table-error.ts b/packages/core/database/src/errors/zero-column-table-error.ts new file mode 100644 index 000000000..b7906fcf4 --- /dev/null +++ b/packages/core/database/src/errors/zero-column-table-error.ts @@ -0,0 +1 @@ +export class ZeroColumnTableError extends Error {} diff --git a/packages/core/database/src/fields/array-field.ts b/packages/core/database/src/fields/array-field.ts index 2cf264589..153904f62 100644 --- a/packages/core/database/src/fields/array-field.ts +++ b/packages/core/database/src/fields/array-field.ts @@ -11,9 +11,12 @@ export class ArrayField extends Field { } sortValue = (model) => { - const oldValue = model.get(this.options.name); + let oldValue = model.get(this.options.name); if (oldValue) { + if (typeof oldValue === 'string') { + oldValue = JSON.parse(oldValue); + } const newValue = oldValue.sort(); model.set(this.options.name, newValue); } diff --git a/packages/core/database/src/fields/belongs-to-field.ts b/packages/core/database/src/fields/belongs-to-field.ts index e8f32e9f7..38eab8ef9 100644 --- a/packages/core/database/src/fields/belongs-to-field.ts +++ b/packages/core/database/src/fields/belongs-to-field.ts @@ -1,4 +1,4 @@ -import { omit } from 'lodash'; +import lodash, { omit } from 'lodash'; import { BelongsToOptions as SequelizeBelongsToOptions, Utils } from 'sequelize'; import { Reference } from '../features/ReferencesMap'; import { checkIdentifier } from '../utils'; @@ -32,6 +32,35 @@ export class BelongsToField extends RelationField { return BelongsToField.toReference(this.database, association, this.options.onDelete); } + checkAssociationKeys() { + let { foreignKey, targetKey } = this.options; + + if (!targetKey) { + targetKey = this.TargetModel.primaryKeyAttribute; + } + + if (!foreignKey) { + foreignKey = lodash.camelCase(`${this.name}_${targetKey}`); + } + + const targetKeyAttribute = this.TargetModel.rawAttributes[targetKey]; + const foreignKeyAttribute = this.collection.model.rawAttributes[foreignKey]; + + if (!foreignKeyAttribute || !targetKeyAttribute) { + // skip check if foreign key not exists + return; + } + + const foreignKeyType = foreignKeyAttribute.type.constructor.toString(); + const targetKeyType = targetKeyAttribute.type.constructor.toString(); + + if (!this.keyPairsTypeMatched(foreignKeyType, targetKeyType)) { + throw new Error( + `Foreign key "${foreignKey}" type "${foreignKeyType}" does not match target key "${targetKey}" type "${targetKeyType}" in belongs to relation "${this.name}" of collection "${this.collection.name}"`, + ); + } + } + bind() { const { database, collection } = this.context; const Target = this.TargetModel; @@ -43,6 +72,8 @@ export class BelongsToField extends RelationField { return false; } + this.checkAssociationKeys(); + if (collection.model.associations[this.name]) { delete collection.model.associations[this.name]; } @@ -64,6 +95,11 @@ export class BelongsToField extends RelationField { this.options.foreignKey = association.foreignKey; } + if (!this.options.targetKey) { + // @ts-ignore + this.options.targetKey = association.targetKey; + } + try { checkIdentifier(this.options.foreignKey); } catch (error) { diff --git a/packages/core/database/src/fields/belongs-to-many-field.ts b/packages/core/database/src/fields/belongs-to-many-field.ts index 7ba7fece9..b69a68e7a 100644 --- a/packages/core/database/src/fields/belongs-to-many-field.ts +++ b/packages/core/database/src/fields/belongs-to-many-field.ts @@ -1,5 +1,5 @@ import { omit } from 'lodash'; -import { BelongsToManyOptions as SequelizeBelongsToManyOptions, Utils } from 'sequelize'; +import { AssociationScope, BelongsToManyOptions as SequelizeBelongsToManyOptions, Utils } from 'sequelize'; import { Collection } from '../collection'; import { Reference } from '../features/ReferencesMap'; import { checkIdentifier } from '../utils'; @@ -50,6 +50,60 @@ export class BelongsToManyField extends RelationField { ]; } + checkAssociationKeys(database) { + let { foreignKey, sourceKey, otherKey, targetKey } = this.options; + + const through = this.through; + const throughCollection = database.getCollection(through); + + if (!throughCollection) { + // skip check if through collection not found + return; + } + + if (!sourceKey) { + sourceKey = this.collection.model.primaryKeyAttribute; + } + + if (!foreignKey) { + foreignKey = Utils.camelize([Utils.singularize(this.collection.model.name), sourceKey].join('_')); + } + + if (!targetKey) { + targetKey = this.TargetModel.primaryKeyAttribute; + } + + if (!otherKey) { + otherKey = Utils.camelize([Utils.singularize(this.TargetModel.name), targetKey].join('_')); + } + + const foreignKeyAttribute = throughCollection.model.rawAttributes[foreignKey]; + const otherKeyAttribute = throughCollection.model.rawAttributes[otherKey]; + const sourceKeyAttribute = this.collection.model.rawAttributes[sourceKey]; + const targetKeyAttribute = this.TargetModel.rawAttributes[targetKey]; + + if (!foreignKeyAttribute || !otherKeyAttribute || !sourceKeyAttribute || !targetKeyAttribute) { + return; + } + + const foreignKeyType = foreignKeyAttribute.type.constructor.toString(); + const otherKeyType = otherKeyAttribute.type.constructor.toString(); + const sourceKeyType = sourceKeyAttribute.type.constructor.toString(); + const targetKeyType = targetKeyAttribute.type.constructor.toString(); + + if (!this.keyPairsTypeMatched(foreignKeyType, sourceKeyType)) { + throw new Error( + `Foreign key "${foreignKey}" type "${foreignKeyType}" does not match source key "${sourceKey}" type "${sourceKeyType}" in belongs to many relation "${this.name}" of collection "${this.collection.name}"`, + ); + } + + if (!this.keyPairsTypeMatched(otherKeyType, targetKeyType)) { + throw new Error( + `Other key "${otherKey}" type "${otherKeyType}" does not match target key "${targetKey}" type "${targetKeyType}" in belongs to many relation "${this.name}" of collection "${this.collection.name}"`, + ); + } + } + bind() { const { database, collection } = this.context; @@ -60,6 +114,16 @@ export class BelongsToManyField extends RelationField { return false; } + if (!this.collection.model.primaryKeyAttribute) { + throw new Error(`Collection model ${this.collection.model.name} has no primary key attribute`); + } + + if (!Target.primaryKeyAttribute) { + throw new Error(`Target model ${Target.name} has no primary key attribute`); + } + + this.checkAssociationKeys(database); + const through = this.through; let Through: Collection; @@ -93,7 +157,12 @@ export class BelongsToManyField extends RelationField { constraints: false, ...omit(this.options, ['name', 'type', 'target']), as: this.name, - through: Through.model, + through: { + model: Through.model, + scope: this.options.throughScope, + paranoid: this.options.throughParanoid, + unique: this.options.throughUnique, + }, }; const association = collection.model.belongsToMany(Target, belongsToManyOptions); @@ -113,6 +182,10 @@ export class BelongsToManyField extends RelationField { this.options.otherKey = association.otherKey; } + if (!this.options.targetKey) { + this.options.targetKey = association.targetKey; + } + try { checkIdentifier(this.options.foreignKey); checkIdentifier(this.options.otherKey); @@ -157,4 +230,7 @@ export interface BelongsToManyFieldOptions type: 'belongsToMany'; target?: string; through?: string; + throughScope?: AssociationScope; + throughUnique?: boolean; + throughParanoid?: boolean; } diff --git a/packages/core/database/src/fields/date-field.ts b/packages/core/database/src/fields/date-field.ts index 3a8a989bc..e740129f9 100644 --- a/packages/core/database/src/fields/date-field.ts +++ b/packages/core/database/src/fields/date-field.ts @@ -23,6 +23,26 @@ export class DateField extends Field { const props = this.getProps(); return props.gmt; } + + bind() { + super.bind(); + + if (this.options.interface === 'createdAt') { + const { model } = this.context.collection; + // @ts-ignore + model._timestampAttributes.createdAt = this.name; + // @ts-ignore + model.refreshAttributes(); + } + + if (this.options.interface === 'updatedAt') { + const { model } = this.context.collection; + // @ts-ignore + model._timestampAttributes.updatedAt = this.name; + // @ts-ignore + model.refreshAttributes(); + } + } } export interface DateFieldOptions extends BaseColumnFieldOptions { diff --git a/packages/core/database/src/fields/has-many-field.ts b/packages/core/database/src/fields/has-many-field.ts index 541d12ede..f266af9f6 100644 --- a/packages/core/database/src/fields/has-many-field.ts +++ b/packages/core/database/src/fields/has-many-field.ts @@ -98,6 +98,34 @@ export class HasManyField extends RelationField { }; } + checkAssociationKeys() { + let { foreignKey, sourceKey } = this.options; + + if (!sourceKey) { + sourceKey = this.collection.model.primaryKeyAttribute; + } + + if (!foreignKey) { + foreignKey = Utils.camelize([Utils.singularize(this.name), this.collection.model.primaryKeyAttribute].join('_')); + } + + const foreignKeyAttribute = this.TargetModel.rawAttributes[foreignKey]; + const sourceKeyAttribute = this.collection.model.rawAttributes[sourceKey]; + + if (!foreignKeyAttribute || !sourceKeyAttribute) { + return; + } + + const foreignKeyType = foreignKeyAttribute.type.constructor.toString(); + const sourceKeyType = sourceKeyAttribute.type.constructor.toString(); + + if (!this.keyPairsTypeMatched(foreignKeyType, sourceKeyType)) { + throw new Error( + `Foreign key "${foreignKey}" type "${foreignKeyType}" does not match source key "${sourceKey}" type "${sourceKeyType}" in has many relation "${this.name}" of collection "${this.collection.name}"`, + ); + } + } + bind() { const { database, collection } = this.context; const Target = this.TargetModel; @@ -106,6 +134,8 @@ export class HasManyField extends RelationField { return false; } + this.checkAssociationKeys(); + if (collection.model.associations[this.name]) { delete collection.model.associations[this.name]; } @@ -127,6 +157,11 @@ export class HasManyField extends RelationField { this.options.foreignKey = association.foreignKey; } + if (!this.options.sourceKey) { + // @ts-ignore + this.options.sourceKey = association.sourceKey; + } + try { checkIdentifier(this.options.foreignKey); } catch (error) { diff --git a/packages/core/database/src/fields/has-one-field.ts b/packages/core/database/src/fields/has-one-field.ts index 7e8a6404c..b6ef32c1f 100644 --- a/packages/core/database/src/fields/has-one-field.ts +++ b/packages/core/database/src/fields/has-one-field.ts @@ -107,6 +107,34 @@ export class HasOneField extends RelationField { }; } + checkAssociationKeys() { + let { foreignKey, sourceKey } = this.options; + + if (!sourceKey) { + sourceKey = this.collection.model.primaryKeyAttribute; + } + + if (!foreignKey) { + foreignKey = Utils.camelize([Utils.singularize(this.name), this.collection.model.primaryKeyAttribute].join('_')); + } + + const foreignKeyAttribute = this.TargetModel.rawAttributes[foreignKey]; + const sourceKeyAttribute = this.collection.model.rawAttributes[sourceKey]; + + if (!foreignKeyAttribute || !sourceKeyAttribute) { + return; + } + + const foreignKeyType = foreignKeyAttribute.type.constructor.toString(); + const sourceKeyType = sourceKeyAttribute.type.constructor.toString(); + + if (!this.keyPairsTypeMatched(foreignKeyType, sourceKeyType)) { + throw new Error( + `Foreign key "${foreignKey}" type "${foreignKeyType}" does not match source key "${sourceKey}" type "${sourceKeyType}" in has one relation "${this.name}" of collection "${this.collection.name}"`, + ); + } + } + bind() { const { database, collection } = this.context; const Target = this.TargetModel; @@ -116,6 +144,8 @@ export class HasOneField extends RelationField { return false; } + this.checkAssociationKeys(); + const association = collection.model.hasOne(Target, { constraints: false, ...omit(this.options, ['name', 'type', 'target', 'onDelete']), @@ -130,6 +160,11 @@ export class HasOneField extends RelationField { this.options.foreignKey = association.foreignKey; } + if (!this.options.sourceKey) { + // @ts-ignore + this.options.sourceKey = association.sourceKey; + } + try { checkIdentifier(this.options.foreignKey); } catch (error) { diff --git a/packages/core/database/src/fields/relation-field.ts b/packages/core/database/src/fields/relation-field.ts index 0e975d90f..c83bebecf 100644 --- a/packages/core/database/src/fields/relation-field.ts +++ b/packages/core/database/src/fields/relation-field.ts @@ -35,6 +35,25 @@ export abstract class RelationField extends Field { return this.context.database.sequelize.models[this.target]; } + keyPairsTypeMatched(type1, type2) { + type1 = type1.toLowerCase(); + type2 = type2.toLowerCase(); + + const numberTypeGroups = ['integer', 'bigint', 'decimal', 'float', 'real', 'double', 'smallint', 'tinyint']; + const stringTypeGroups = ['string', 'char', 'text']; + + // if types are in the same group then they are matched + if (numberTypeGroups.includes(type1) && numberTypeGroups.includes(type2)) { + return true; + } + + if (stringTypeGroups.includes(type1) && stringTypeGroups.includes(type2)) { + return true; + } + + return type1 === type2; + } + protected clearAccessors() { const { collection } = this.context; const association = collection.model.associations[this.name]; diff --git a/packages/core/database/src/inherited-collection.ts b/packages/core/database/src/inherited-collection.ts index 5fe6e01be..0615fd974 100644 --- a/packages/core/database/src/inherited-collection.ts +++ b/packages/core/database/src/inherited-collection.ts @@ -35,37 +35,24 @@ export class InheritedCollection extends Collection { } } - protected bindParents() { - this.setParents(this.options.inherits); - this.setParentFields(); - this.setFields(this.options.fields, false); - this.db.inheritanceMap.setInheritance(this.name, this.options.inherits); - } - - protected setParents(inherits: string | string[]) { - this.parents = lodash.castArray(inherits).map((name) => { - const existCollection = this.db.collections.get(name); - if (!existCollection) { - throw new ParentCollectionNotFound(name); - } - - return existCollection; - }); - } - - protected setParentFields() { - for (const [name, fieldOptions] of this.parentFields()) { - this.setField(name, { - ...fieldOptions, - inherit: true, - }); - } - } - getParents() { return this.parents; } + getFlatParents() { + // get parents as flat array + const parents = []; + for (const parent of this.parents) { + if (parent.isInherited()) { + parents.push(...(parent).getFlatParents()); + } + + parents.push(parent); + } + + return parents; + } + parentFields() { const fields = new Map(); @@ -104,6 +91,33 @@ export class InheritedCollection extends Collection { isInherited() { return true; } + + protected bindParents() { + this.setParents(this.options.inherits); + this.setParentFields(); + this.setFields(this.options.fields, false); + this.db.inheritanceMap.setInheritance(this.name, this.options.inherits); + } + + protected setParents(inherits: string | string[]) { + this.parents = lodash.castArray(inherits).map((name) => { + const existCollection = this.db.collections.get(name); + if (!existCollection) { + throw new ParentCollectionNotFound(name); + } + + return existCollection; + }); + } + + protected setParentFields() { + for (const [name, fieldOptions] of this.parentFields()) { + this.setField(name, { + ...fieldOptions, + inherit: true, + }); + } + } } class ParentCollectionNotFound extends Error { diff --git a/packages/core/database/src/inherited-sync-runner.ts b/packages/core/database/src/inherited-sync-runner.ts new file mode 100644 index 000000000..7fed9ead0 --- /dev/null +++ b/packages/core/database/src/inherited-sync-runner.ts @@ -0,0 +1,183 @@ +import { InheritedCollection } from './inherited-collection'; +import lodash from 'lodash'; + +export class InheritedSyncRunner { + static async syncInheritModel(model: any, options: any) { + const { transaction } = options; + + const inheritedCollection = model.collection as InheritedCollection; + const db = inheritedCollection.context.database; + + const dialect = db.sequelize.getDialect(); + + const queryInterface = db.sequelize.getQueryInterface(); + + if (dialect != 'postgres') { + throw new Error('Inherit model is only supported on postgres'); + } + + const parents = inheritedCollection.parents; + + if (!parents) { + throw new Error( + `Inherit model ${inheritedCollection.name} can't be created without parents, parents option is ${lodash + .castArray(inheritedCollection.options.inherits) + .join(', ')}`, + ); + } + + for (const parent of parents) { + if (Object.keys(parent.model.rawAttributes).length === 0) { + throw new Error( + `can't inherit from collection ${parent.options.name} because it has no attributes, please define at least one attribute in parent collection`, + ); + } + } + const tableName = inheritedCollection.getTableNameWithSchema(); + const attributes = model.tableAttributes; + + const childAttributes = lodash.pickBy(attributes, (value) => { + return !value.inherit; + }); + + if ( + !(await inheritedCollection.existsInDb({ + transaction, + })) + ) { + let maxSequenceVal = 0; + let maxSequenceName; + + // find max sequence + if (childAttributes.id && childAttributes.id.autoIncrement) { + for (const parent of parents) { + const sequenceNameResult = await queryInterface.sequelize.query( + `SELECT column_default + FROM information_schema.columns + WHERE table_name = '${parent.model.tableName}' + and table_schema = '${parent.collectionSchema()}' + and "column_name" = 'id';`, + { + transaction, + }, + ); + + if (!sequenceNameResult[0].length) { + continue; + } + + const columnDefault = sequenceNameResult[0][0]['column_default']; + + if (!columnDefault) { + throw new Error(`Can't find sequence name of parent collection ${parent.options.name}`); + } + + const regex = new RegExp(/nextval\('(.*)'::regclass\)/); + const match = regex.exec(columnDefault); + + const sequenceName = match[1]; + const sequenceCurrentValResult = await queryInterface.sequelize.query( + `select last_value + from ${sequenceName}`, + { + transaction, + }, + ); + + const sequenceCurrentVal = parseInt(sequenceCurrentValResult[0][0]['last_value']); + + if (sequenceCurrentVal > maxSequenceVal) { + maxSequenceName = sequenceName; + maxSequenceVal = sequenceCurrentVal; + } + } + } + + await this.createTable(tableName, childAttributes, options, model, parents); + + // if we have max sequence, set it to child table + if (maxSequenceName) { + const parentsDeep = Array.from(db.inheritanceMap.getParents(inheritedCollection.name)).map((parent) => + db.getCollection(parent).getTableNameWithSchema(), + ); + + const sequenceTables = [...parentsDeep, tableName]; + + for (const sequenceTable of sequenceTables) { + const tableName = sequenceTable.tableName; + const schemaName = sequenceTable.schema; + + const idColumnSql = `SELECT column_name + FROM information_schema.columns + WHERE table_name = '${tableName}' + and column_name = 'id' + and table_schema = '${schemaName}'; + `; + + const idColumnQuery = await queryInterface.sequelize.query(idColumnSql, { + transaction, + }); + + if (idColumnQuery[0].length == 0) { + continue; + } + + await queryInterface.sequelize.query( + `alter table ${db.utils.quoteTable(sequenceTable)} + alter column id set default nextval('${maxSequenceName}')`, + { + transaction, + }, + ); + } + } + } + + if (options.alter) { + const columns = await queryInterface.describeTable(tableName, options); + + for (const attribute in childAttributes) { + const columnName = childAttributes[attribute].field; + + if (!columns[columnName]) { + await queryInterface.addColumn(tableName, columnName, childAttributes[columnName], options); + } + } + } + } + + static async createTable(tableName, attributes, options, model, parents) { + let sql = ''; + + options = { ...options }; + + if (options && options.uniqueKeys) { + lodash.forOwn(options.uniqueKeys, (uniqueKey) => { + if (uniqueKey.customIndex === undefined) { + uniqueKey.customIndex = true; + } + }); + } + + if (model) { + options.uniqueKeys = options.uniqueKeys || model.uniqueKeys; + } + + const queryGenerator = model.queryGenerator; + + attributes = lodash.mapValues(attributes, (attribute) => model.sequelize.normalizeAttribute(attribute)); + + attributes = queryGenerator.attributesToSQL(attributes, { table: tableName, context: 'createTable' }); + + sql = `${queryGenerator.createTableQuery(tableName, attributes, options)}`.replace( + ';', + ` INHERITS (${parents + .map((t) => { + return t.getTableNameWithSchema(); + }) + .join(', ')});`, + ); + + return await model.sequelize.query(sql, options); + } +} diff --git a/packages/core/database/src/mock-database.ts b/packages/core/database/src/mock-database.ts index 44862fe0f..52c7ade1f 100644 --- a/packages/core/database/src/mock-database.ts +++ b/packages/core/database/src/mock-database.ts @@ -1,7 +1,7 @@ -import { merge, uid } from '@nocobase/utils'; +import { merge } from '@nocobase/utils'; import { customAlphabet } from 'nanoid'; import fetch from 'node-fetch'; -import path, { resolve } from 'path'; +import path from 'path'; import { Database, IDatabaseOptions } from './database'; export class MockDatabase extends Database { @@ -23,7 +23,7 @@ export function getConfigByEnv() { port: process.env.DB_PORT, dialect: process.env.DB_DIALECT || 'sqlite', logging: process.env.DB_LOGGING === 'on' ? customLogger : false, - storage: resolve(process.cwd(), `storage/db-test/db-${uid()}.sqlite`), + storage: process.env.DB_STORAGE, define: { charset: 'utf8mb4', collate: 'utf8mb4_unicode_ci', diff --git a/packages/core/database/src/model.ts b/packages/core/database/src/model.ts index 7114a7d63..7642cbafe 100644 --- a/packages/core/database/src/model.ts +++ b/packages/core/database/src/model.ts @@ -31,67 +31,14 @@ export class Model { - return !value.inherit; + if (collectionSyncOptions) { + await collectionSyncOptions(this.model, options); + return; + } + + await this.handleSchema(options); + + try { + await this.handleZeroColumnModel(options); + } catch (e) { + if (e instanceof ZeroColumnTableError) { + return; + } + + throw e; + } + + const syncResult = await this.performSync(options); + const columns = await this.queryInterface.describeTable(this.tableName, options); + + await this.handlePrimaryKey(columns, options); + await this.handleDefaultValues(columns, options); + await this.handleUniqueIndex(options); + + return syncResult; + } + + async handlePrimaryKey(columns, options) { + if (!this.database.inDialect('postgres')) { + return; + } + + try { + const columnsBePrimaryKey = Object.keys(columns) + .filter((key) => { + return columns[key].primaryKey == true; + }) + .sort(); + + const columnsWillBePrimaryKey = Object.keys(this.rawAttributes) + .filter((key) => { + return this.rawAttributes[key].primaryKey == true; + }) + .map((key) => { + return this.rawAttributes[key].field; + }) + .sort(); + + if (columnsWillBePrimaryKey.length == 0) { + // skip if no primary key + return; + } + + if (JSON.stringify(columnsBePrimaryKey) != JSON.stringify(columnsWillBePrimaryKey)) { + await this.queryInterface.addConstraint(this.tableName, { + type: 'primary key', + fields: columnsWillBePrimaryKey, + name: `${this.collection.tableName()}_${columnsWillBePrimaryKey.join('_')}_pk`, + transaction: options?.transaction, + }); + } + } catch (e) { + if (e.message.includes('No description found')) { + return; + } + } + } + + async handleDefaultValues(columns, options) { + const isJSONColumn = (column) => { + return ['JSON', 'JSONB'].includes(column.type); + }; + + for (const columnName in columns) { + const column = columns[columnName]; + const isPrimaryKey = () => { + const attribute = this.findAttributeByColumnName(columnName); + return (attribute && attribute.primaryKey) || column.primaryKey; + }; + + if (isPrimaryKey()) continue; + + if (await this.isParentColumn(columnName, options)) continue; + + const currentAttribute = this.findAttributeByColumnName(columnName); + if (!currentAttribute) continue; + + const attributeDefaultValue = + isPlainObject(currentAttribute.defaultValue) && isJSONColumn(column) + ? JSON.stringify(currentAttribute.defaultValue) + : currentAttribute.defaultValue; + const columnDefaultValue = columns[columnName].defaultValue; + + if (columnDefaultValue === null && attributeDefaultValue === undefined) continue; + + if (columnDefaultValue === 'NULL' && attributeDefaultValue === null) continue; + + if (columnDefaultValue != attributeDefaultValue) { + const changeAttribute = { + ...currentAttribute, + defaultValue: attributeDefaultValue, + }; + + if (this.database.inDialect('postgres')) { + // @ts-ignore + const query = this.queryInterface.queryGenerator.attributesToSQL( + { + // @ts-ignore + [columnName]: this.queryInterface.normalizeAttribute(changeAttribute), + }, + { + context: 'changeColumn', + table: this.tableName, + }, + ); + + // @ts-ignore + const sql = this.queryInterface.queryGenerator.changeColumnQuery(this.tableName, query); + // remove alter type sql + const regex = /;ALTER TABLE "[^"]+"(\."[^"]+")? ALTER COLUMN "[^"]+" TYPE [^;]+;?$/; + + await this.sequelize.query(sql.replace(regex, ''), options); + } else { + await this.queryInterface.changeColumn(this.tableName, columnName, changeAttribute, options); + } + } + } + } + + async handleUniqueIndex(options) { + const existsIndexes: any = await this.queryInterface.showIndex(this.collection.getTableNameWithSchema(), options); + const existsUniqueIndexes = existsIndexes.filter((index) => index.unique); + + const uniqueAttributes = Object.keys(this.rawAttributes).filter((key) => { + return this.rawAttributes[key].unique == true; }); - if ( - !(await inheritedCollection.existsInDb({ - transaction, - })) - ) { - let maxSequenceVal = 0; - let maxSequenceName; + // remove unique index that not in model + for (const existUniqueIndex of existsUniqueIndexes) { + const isSingleField = existUniqueIndex.fields.length == 1; + if (!isSingleField) continue; - // find max sequence - if (childAttributes.id && childAttributes.id.autoIncrement) { - for (const parent of parents) { - const sequenceNameResult = await queryInterface.sequelize.query( - `SELECT column_default - FROM information_schema.columns - WHERE table_name = '${parent.model.tableName}' - and table_schema = '${parent.collectionSchema()}' - and "column_name" = 'id';`, - { - transaction, - }, - ); + const columnName = existUniqueIndex.fields[0].attribute; - if (!sequenceNameResult[0].length) { - continue; - } + const currentAttribute = this.findAttributeByColumnName(columnName); - const columnDefault = sequenceNameResult[0][0]['column_default']; - - if (!columnDefault) { - throw new Error(`Can't find sequence name of parent collection ${parent.options.name}`); - } - - const regex = new RegExp(/nextval\('(.*)'::regclass\)/); - const match = regex.exec(columnDefault); - - const sequenceName = match[1]; - const sequenceCurrentValResult = await queryInterface.sequelize.query( - `select last_value - from ${sequenceName}`, - { - transaction, - }, - ); - - const sequenceCurrentVal = parseInt(sequenceCurrentValResult[0][0]['last_value']); - - if (sequenceCurrentVal > maxSequenceVal) { - maxSequenceName = sequenceName; - maxSequenceVal = sequenceCurrentVal; + if (!currentAttribute || (!currentAttribute.unique && !currentAttribute.primaryKey)) { + if (this.database.inDialect('postgres')) { + // @ts-ignore + const constraints = await this.queryInterface.showConstraint(this.tableName, existUniqueIndex.name, options); + if (constraints.some((c) => c.constraintName === existUniqueIndex.name)) { + await this.queryInterface.removeConstraint(this.tableName, existUniqueIndex.name, options); } } - } - await this.createTable(tableName, childAttributes, options, model, parents); + if (this.database.inDialect('sqlite')) { + const changeAttribute = { + ...currentAttribute, + unique: false, + }; - // if we have max sequence, set it to child table - if (maxSequenceName) { - const parentsDeep = Array.from(db.inheritanceMap.getParents(inheritedCollection.name)).map((parent) => - db.getCollection(parent).getTableNameWithSchema(), - ); - - const sequenceTables = [...parentsDeep, tableName]; - - for (const sequenceTable of sequenceTables) { - const tableName = sequenceTable.tableName; - const schemaName = sequenceTable.schema; - - const idColumnSql = `SELECT column_name - FROM information_schema.columns - WHERE table_name = '${tableName}' - and column_name = 'id' - and table_schema = '${schemaName}'; - `; - - const idColumnQuery = await queryInterface.sequelize.query(idColumnSql, { - transaction, - }); - - if (idColumnQuery[0].length == 0) { - continue; - } - - await queryInterface.sequelize.query( - `alter table ${db.utils.quoteTable(sequenceTable)} - alter column id set default nextval('${maxSequenceName}')`, - { - transaction, - }, - ); + await this.queryInterface.changeColumn(this.tableName, columnName, changeAttribute, options); + } else { + await this.queryInterface.removeIndex(this.tableName, existUniqueIndex.name, options); } } } - if (options.alter) { - const columns = await queryInterface.describeTable(tableName, options); + // add unique index that not in database + for (const uniqueAttribute of uniqueAttributes) { + // check index exists or not + const indexExists = existsUniqueIndexes.find((index) => { + return index.fields.length == 1 && index.fields[0].attribute == this.rawAttributes[uniqueAttribute].field; + }); - for (const attribute in childAttributes) { - const columnName = childAttributes[attribute].field; + if (!indexExists) { + await this.queryInterface.addIndex(this.tableName, [this.rawAttributes[uniqueAttribute].field], { + unique: true, + transaction: options?.transaction, + }); + } + } + } - if (!columns[columnName]) { - await queryInterface.addColumn(tableName, columnName, childAttributes[columnName], options); + async getColumns(options) { + return await this.queryInterface.describeTable(this.tableName, options); + } + + async isParentColumn(columnName: string, options) { + if (this.collection.isInherited()) { + const parentCollections = (this.collection).getFlatParents(); + for (const parentCollection of parentCollections) { + let parentColumns = this.tableDescMap[parentCollection.name]; + if (!parentColumns) { + parentColumns = await this.queryInterface.describeTable(parentCollection.getTableNameWithSchema(), options); + this.tableDescMap[parentCollection.name] = parentColumns; + } + + if (parentColumns[columnName]) { + return true; + } + } + } + + return false; + } + + async removeUnusedColumns(columns, options) { + // remove columns that not in model + for (const columnName in columns) { + const currentAttribute = this.findAttributeByColumnName(columnName); + if (!currentAttribute) { + let shouldDelete = true; + + if (await this.isParentColumn(columnName, options)) { + shouldDelete = false; + } + + if (shouldDelete) { + await this.queryInterface.removeColumn(this.model.getTableName(), columnName, options); + continue; } } } } - static async createTable(tableName, attributes, options, model, parents) { - let sql = ''; + findAttributeByColumnName(columnName: string): any { + return Object.values(this.rawAttributes).find((attribute) => { + // @ts-ignore + return attribute.field == columnName; + }); + } - options = { ...options }; + async performSync(options) { + return this.collection.isInherited() + ? await InheritedSyncRunner.syncInheritModel(this.model, options) + : await SequelizeModel.sync.call(this.model, options); + } - if (options && options.uniqueKeys) { - lodash.forOwn(options.uniqueKeys, (uniqueKey) => { - if (uniqueKey.customIndex === undefined) { - uniqueKey.customIndex = true; - } + async handleZeroColumnModel(options) { + // @ts-ignore + if (Object.keys(this.model.tableAttributes).length === 0) { + if (this.database.inDialect('sqlite', 'mysql', 'mariadb', 'postgres')) { + throw new ZeroColumnTableError( + `Zero-column tables aren't supported in ${this.database.sequelize.getDialect()}`, + ); + } + + const queryInterface = this.queryInterface; + // @ts-ignore + if (!queryInterface.patched) { + const oldDescribeTable = queryInterface.describeTable; + queryInterface.describeTable = async function (...args) { + try { + return await oldDescribeTable.call(this, ...args); + } catch (err) { + if (err.message.includes('No description found for')) { + return []; + } else { + throw err; + } + } + }; + // @ts-ignore + queryInterface.patched = true; + } + } + } + + async handleSchema(options) { + // @ts-ignore + const _schema = this.model._schema; + + if (_schema && _schema != 'public') { + await this.sequelize.query(`CREATE SCHEMA IF NOT EXISTS "${_schema}";`, { + raw: true, + transaction: options?.transaction, }); } - - if (model) { - options.uniqueKeys = options.uniqueKeys || model.uniqueKeys; - } - - const queryGenerator = model.queryGenerator; - - attributes = lodash.mapValues(attributes, (attribute) => model.sequelize.normalizeAttribute(attribute)); - - attributes = queryGenerator.attributesToSQL(attributes, { table: tableName, context: 'createTable' }); - - sql = `${queryGenerator.createTableQuery(tableName, attributes, options)}`.replace( - ';', - ` INHERITS (${parents - .map((t) => { - return t.getTableNameWithSchema(); - }) - .join(', ')});`, - ); - - return await model.sequelize.query(sql, options); } } diff --git a/packages/core/database/src/update-associations.ts b/packages/core/database/src/update-associations.ts index 0ee753735..3076bc9ce 100644 --- a/packages/core/database/src/update-associations.ts +++ b/packages/core/database/src/update-associations.ts @@ -9,6 +9,7 @@ import { ModelStatic, Transactionable, } from 'sequelize'; +import Database from './database'; import { Model } from './model'; import { UpdateGuard } from './update-guard'; @@ -285,6 +286,22 @@ export async function updateSingleAssociation( return await removeAssociation(); } + // @ts-ignore + if (association.associationType === 'HasOne' && !model.get(association.sourceKeyAttribute)) { + // @ts-ignore + throw new Error(`The source key ${association.sourceKeyAttribute} is not set in ${model.constructor.name}`); + } + + const checkBelongsToForeignKeyValue = () => { + // @ts-ignore + if (association.associationType === 'BelongsTo' && !model.get(association.foreignKey)) { + throw new Error( + // @ts-ignore + `The target key ${association.targetKey} is not set in ${association.target.name}`, + ); + } + }; + if (isStringOrNumber(value)) { await model[setAccessor](value, { context, transaction }); return true; @@ -352,6 +369,9 @@ export async function updateSingleAssociation( if (association.targetKey) { model.setDataValue(association.foreignKey, instance[dataKey]); } + + // must have foreign key value + checkBelongsToForeignKeyValue(); } /** @@ -390,6 +410,12 @@ export async function updateMultipleAssociation( return; } + // @ts-ignore + if (association.associationType === 'HasMany' && !model.get(association.sourceKeyAttribute)) { + // @ts-ignore + throw new Error(`The source key ${association.sourceKeyAttribute} is not set in ${model.constructor.name}`); + } + if (isStringOrNumber(value)) { await model[setAccessor](value, { transaction, context, individualHooks: true }); return; @@ -427,10 +453,17 @@ export async function updateMultipleAssociation( await model[setAccessor](setItems, { transaction, context, individualHooks: true }); const newItems = []; - + const pk = association.target.primaryKeyAttribute; + const tmpKey = association['options']?.['targetKey']; + let targetKey = pk; + const db = model.constructor['database'] as Database; + if (tmpKey !== pk) { + const targetKeyFieldOptions = db.getFieldByPath(`${association.target.name}.${tmpKey}`)?.options; + if (targetKeyFieldOptions?.unique) { + targetKey = tmpKey; + } + } for (const item of objectItems) { - const pk = association.target.primaryKeyAttribute; - const through = (association).through ? (association).through.model.name : null; const accessorOptions = { @@ -444,7 +477,7 @@ export async function updateMultipleAssociation( accessorOptions['through'] = throughValue; } - if (isUndefinedOrNull(item[pk])) { + if (isUndefinedOrNull(item[targetKey])) { // create new record const instance = await model[createAccessor](item, accessorOptions); @@ -457,15 +490,28 @@ export async function updateMultipleAssociation( newItems.push(instance); } else { // set & update record - const instance = await association.target.findByPk(item[pk], { + const where = { + [targetKey]: item[targetKey], + }; + let instance = await association.target.findOne({ + where, transaction, }); if (!instance) { + // create new record + instance = await model[createAccessor](item, accessorOptions); + await updateAssociations(instance, item, { + ...options, + transaction, + associationContext: association, + updateAssociationValues: keys, + }); + newItems.push(instance); continue; } const addAccessor = association.accessors.add; - await model[addAccessor](item[pk], accessorOptions); + await model[addAccessor](instance[association.target.primaryKeyAttribute], accessorOptions); if (!recursive) { continue; diff --git a/packages/core/database/src/view/field-type-map.ts b/packages/core/database/src/view/field-type-map.ts index 44270f1d5..7ca352ed3 100644 --- a/packages/core/database/src/view/field-type-map.ts +++ b/packages/core/database/src/view/field-type-map.ts @@ -7,9 +7,9 @@ const postgres = { oid: 'string', name: 'string', - smallint: 'integer', - integer: 'integer', - bigint: 'bigInt', + smallint: ['integer', 'sort'], + integer: ['integer', 'sort'], + bigint: ['bigInt', 'sort'], decimal: 'decimal', numeric: 'float', real: 'float', @@ -33,13 +33,13 @@ const postgres = { }; const mysql = { - smallint: ['integer', 'boolean'], - tinyint: ['integer', 'boolean'], - mediumint: ['integer', 'boolean'], + smallint: ['integer', 'boolean', 'sort'], + tinyint: ['integer', 'boolean', 'sort'], + mediumint: ['integer', 'boolean', 'sort'], - 'smallint unsigned': ['integer', 'boolean'], - 'tinyint unsigned': ['integer', 'boolean'], - 'mediumint unsigned': ['integer', 'boolean'], + 'smallint unsigned': ['integer', 'boolean', 'sort'], + 'tinyint unsigned': ['integer', 'boolean', 'sort'], + 'mediumint unsigned': ['integer', 'boolean', 'sort'], char: 'string', date: 'date', @@ -47,11 +47,11 @@ const mysql = { varchar: 'string', text: 'text', longtext: 'text', - int: 'integer', - 'int unsigned': 'integer', - integer: 'integer', - bigint: 'bigInt', - 'bigint unsigned': 'bigInt', + int: ['integer', 'sort'], + 'int unsigned': ['integer', 'sort'], + integer: ['integer', 'sort'], + bigint: ['bigInt', 'sort'], + 'bigint unsigned': ['bigInt', 'sort'], float: 'float', double: 'float', boolean: 'boolean', diff --git a/packages/core/resourcer/src/__tests__/utils.test.ts b/packages/core/resourcer/src/__tests__/utils.test.ts index 4d4af1c5b..047d5cd4e 100644 --- a/packages/core/resourcer/src/__tests__/utils.test.ts +++ b/packages/core/resourcer/src/__tests__/utils.test.ts @@ -164,6 +164,19 @@ describe('utils', () => { }); }); + it('decode path', () => { + const params = parseRequest({ + path: '/posts/%E7%9A%84%E6%B3%95%E5%9B%BD%E9%98%9F/comments', + method: 'POST', + }); + expect(params).toMatchObject({ + resourceName: 'comments', + associatedName: 'posts', + associatedIndex: '的法国队', + actionName: 'create', + }); + }); + it('store action', () => { const params = parseRequest({ path: '/posts/1/comments', diff --git a/packages/core/resourcer/src/resourcer.ts b/packages/core/resourcer/src/resourcer.ts index 2cc259265..c223ffd87 100644 --- a/packages/core/resourcer/src/resourcer.ts +++ b/packages/core/resourcer/src/resourcer.ts @@ -14,6 +14,8 @@ export interface ResourcerContext { } export interface KoaMiddlewareOptions { + skipIfDataSourceExists?: boolean; + /** * 前缀 */ @@ -262,9 +264,17 @@ export class Resourcer { this.middlewares.add(middlewares, options); } - restApiMiddleware({ prefix, accessors }: KoaMiddlewareOptions = {}) { + restApiMiddleware({ prefix, accessors, skipIfDataSourceExists = false }: KoaMiddlewareOptions = {}) { return async (ctx: ResourcerContext, next: () => Promise) => { + if (skipIfDataSourceExists) { + const dataSource = ctx.get('x-data-source'); + if (dataSource) { + return next(); + } + } + ctx.resourcer = this; + let params = parseRequest( { path: ctx.request.path, @@ -279,6 +289,7 @@ export class Resourcer { if (!params) { return next(); } + try { const resource = this.getResource(getNameByParams(params)); @@ -300,8 +311,10 @@ export class Resourcer { return next(); } } + // action 需要 clone 之后再赋给 ctx ctx.action = this.getAction(getNameByParams(params), params.actionName).clone(); + ctx.action.setContext(ctx); ctx.action.actionName = params.actionName; ctx.action.resourceOf = params.associatedIndex; @@ -325,6 +338,7 @@ export class Resourcer { } return compose(ctx.action.getHandlers())(ctx, next); } catch (error) { + console.log(error); return next(); } }; diff --git a/packages/core/resourcer/src/utils.ts b/packages/core/resourcer/src/utils.ts index aa9955b4f..97e30adcd 100644 --- a/packages/core/resourcer/src/utils.ts +++ b/packages/core/resourcer/src/utils.ts @@ -7,6 +7,7 @@ import { ResourceType } from './resource'; export interface ParseRequest { path: string; method: string; + namespace?: string; // 资源类型 type?: ResourceType; } @@ -58,7 +59,8 @@ export function parseRequest(request: ParseRequest, options: ParseOptions = {}): }; const keys = []; const regexp = pathToRegexp('/resourcer/{:associatedName.}?:resourceName{\\::actionName}', keys); - const matches = regexp.exec(request.path); + const reqPath = decodeURI(request.path); + const matches = regexp.exec(reqPath); if (matches) { const params = {}; keys.forEach((obj, index) => { @@ -162,7 +164,7 @@ export function parseRequest(request: ParseRequest, options: ParseOptions = {}): for (const path in defaults[type]) { const keys = []; const regexp = pathToRegexp(`${prefix}${path}`, keys, {}); - const matches = regexp.exec(request.path); + const matches = regexp.exec(reqPath); if (!matches) { continue; } diff --git a/packages/core/sdk/src/APIClient.ts b/packages/core/sdk/src/APIClient.ts index 73a345d4d..300644aae 100644 --- a/packages/core/sdk/src/APIClient.ts +++ b/packages/core/sdk/src/APIClient.ts @@ -1,4 +1,4 @@ -import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; +import axios, { AxiosInstance, AxiosRequestConfig, AxiosRequestHeaders, AxiosResponse } from 'axios'; import qs from 'qs'; import getSubAppName from './getSubAppName'; @@ -271,14 +271,14 @@ export class APIClient { } request, D = any>(config: AxiosRequestConfig | ResourceActionOptions): Promise { - const { resource, resourceOf, action, params } = config as any; + const { resource, resourceOf, action, params, headers } = config as any; if (resource) { - return this.resource(resource, resourceOf)[action](params); + return this.resource(resource, resourceOf, headers)[action](params); } return this.axios.request(config); } - resource(name: string, of?: any): IResource { + resource(name: string, of?: any, headers?: AxiosRequestHeaders): IResource { const target = {}; const handler = { get: (_: any, actionName: string) => { @@ -309,6 +309,7 @@ export class APIClient { return await this.request({ ...config, ...opts, + headers, }); }; }, diff --git a/packages/core/server/package.json b/packages/core/server/package.json index 8381b52b9..328a32b59 100644 --- a/packages/core/server/package.json +++ b/packages/core/server/package.json @@ -15,6 +15,7 @@ "@nocobase/auth": "0.19.0-alpha.9", "@nocobase/cache": "0.19.0-alpha.9", "@nocobase/database": "0.19.0-alpha.9", + "@nocobase/data-source-manager": "0.19.0-alpha.9", "@nocobase/evaluators": "0.19.0-alpha.9", "@nocobase/logger": "0.19.0-alpha.9", "@nocobase/resourcer": "0.19.0-alpha.9", diff --git a/packages/core/server/src/__tests__/main-data-source.test.ts b/packages/core/server/src/__tests__/main-data-source.test.ts new file mode 100644 index 000000000..84ccf72b0 --- /dev/null +++ b/packages/core/server/src/__tests__/main-data-source.test.ts @@ -0,0 +1,32 @@ +import { Application } from '@nocobase/server'; + +describe('MainDataSource', () => { + let app: Application; + + beforeEach(async () => { + app = new Application({ + database: { + dialect: 'sqlite', + storage: ':memory:', + logging: false, + }, + resourcer: { + prefix: '/api', + }, + acl: false, + dataWrapping: false, + registerActions: false, + }); + }); + + afterEach(async () => { + await app.destroy(); + }); + + it('should create main data source when create application', async () => { + const dataSourceManager = app.dataSourceManager; + const mainDataSource = dataSourceManager.dataSources.get('main'); + + expect(mainDataSource).toBeTruthy(); + }); +}); diff --git a/packages/core/server/src/__tests__/pm.test.ts b/packages/core/server/src/__tests__/pm.test.ts index 94cf8ce6f..6f9ca9fd5 100644 --- a/packages/core/server/src/__tests__/pm.test.ts +++ b/packages/core/server/src/__tests__/pm.test.ts @@ -2,6 +2,7 @@ import Plugin from '../plugin'; import { PluginManager } from '../plugin-manager'; import { vi } from 'vitest'; import { MockServer, mockServer } from '@nocobase/test'; + describe('pm', () => { let app: MockServer; afterEach(async () => { @@ -454,7 +455,7 @@ describe('pm', () => { await app.cleanDb(); await app.load(); await app.install(); - await app.pm.repository.create({ + const plugin = await app.pm.repository.create({ values: { name: 'Plugin1', }, diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index 2ea1a2cc2..975427fa8 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -1,4 +1,3 @@ -import { ACL } from '@nocobase/acl'; import { registerActions } from '@nocobase/actions'; import { actions as authActions, AuthManager, AuthManagerOptions } from '@nocobase/auth'; import { Cache, CacheManager, CacheManagerOptions } from '@nocobase/cache'; @@ -18,7 +17,7 @@ import { applyMixins, AsyncEmitter, importModule, Toposort, ToposortOptions } fr import { Command, CommandOptions, ParseOptions } from 'commander'; import { randomUUID } from 'crypto'; import glob from 'glob'; -import { IncomingMessage, Server, ServerResponse } from 'http'; +import { IncomingMessage, ServerResponse } from 'http'; import { i18n, InitOptions } from 'i18next'; import Koa, { DefaultContext as KoaDefaultContext, DefaultState as KoaDefaultState } from 'koa'; import compose from 'koa-compose'; @@ -46,7 +45,9 @@ import { Locale } from './locale'; import { Plugin } from './plugin'; import { InstallOptions, PluginManager } from './plugin-manager'; +import { DataSourceManager, SequelizeDataSource } from '@nocobase/data-source-manager'; import packageJson from '../package.json'; +import { MainDataSource } from './main-data-source'; export type PluginType = string | typeof Plugin; export type PluginConfiguration = PluginType | [PluginType, any]; @@ -138,7 +139,6 @@ export type MaintainingCommandStatus = { }; export class Application extends Koa implements AsyncEmitter { - public listenServer: Server; declare middleware: any; stopped = false; ready = false; @@ -185,10 +185,17 @@ export class Application exten return this._cronJobManager; } - protected _db: Database; + get mainDataSource() { + return this.dataSourceManager?.dataSources.get('main') as SequelizeDataSource; + } - get db() { - return this._db; + get db(): Database { + if (!this.mainDataSource) { + return null; + } + + // @ts-ignore + return this.mainDataSource.collectionManager.db; } protected _logger: SystemLogger; @@ -197,10 +204,8 @@ export class Application exten return this._logger; } - protected _resourcer: Resourcer; - get resourcer() { - return this._resourcer; + return this.mainDataSource.resourceManager; } protected _cacheManager: CacheManager; @@ -237,10 +242,8 @@ export class Application exten return this._pm; } - protected _acl: ACL; - get acl() { - return this._acl; + return this.mainDataSource.acl; } protected _authManager: AuthManager; @@ -279,8 +282,10 @@ export class Application exten return this.options.name || 'main'; } - isMaintaining() { - return this._maintaining; + protected _dataSourceManager: DataSourceManager; + + get dataSourceManager() { + return this._dataSourceManager; } getMaintaining() { @@ -383,11 +388,13 @@ export class Application exten await this.telemetry.shutdown(); } - const oldDb = this._db; + const oldDb = this.db; + this.init(); if (!oldDb.closed()) { await oldDb.close(); } + this._loaded = false; } @@ -408,8 +415,10 @@ export class Application exten await this.telemetry.shutdown(); } - const oldDb = this._db; + const oldDb = this.db; + this.init(); + if (!oldDb.closed()) { await oldDb.close(); } @@ -437,6 +446,10 @@ export class Application exten await this.pm.load(options); + if (options?.sync) { + await this.db.sync(); + } + this.setMaintainingMessage('emit afterLoad'); if (options?.hooks !== false) { await this.emitAsync('afterLoad', this, options); @@ -487,6 +500,10 @@ export class Application exten return await this.runAsCLI([command, ...args], { from: 'user' }); } + async runCommandThrowError(command: string, ...args: any[]) { + return await this.runAsCLI([command, ...args], { from: 'user', throwError: true }); + } + createCli() { const command = new AppCommand('nocobase') .usage('[command] [options]') @@ -781,9 +798,11 @@ export class Application exten await this.reInit(); await this.db.sync(); await this.load({ hooks: false }); + this.log.debug('emit beforeInstall', { method: 'install' }); this.setMaintainingMessage('call beforeInstall hook...'); await this.emitAsync('beforeInstall', this, options); + // await app.db.sync(); await this.pm.install(); await this.version.update(); @@ -804,6 +823,7 @@ export class Application exten // await this.pm.install(options); // this.log.debug('update version', { method: 'install' }); // await this.version.update(); + this.log.debug('emit afterInstall', { method: 'install' }); this.setMaintainingMessage('call afterInstall hook...'); await this.emitAsync('afterInstall', this, options); @@ -838,8 +858,8 @@ export class Application exten await migrator3.beforeLoad.up(); // load other plugins // TODO:改成约定式 - await this.load(); - await this.db.sync(); + await this.load({ sync: true }); + // await this.db.sync(); await migrator3.afterSync.up(); // upgrade plugins await this.pm.upgrade(); @@ -914,23 +934,20 @@ export class Application exten this.middleware = new Toposort(); this.plugins = new Map(); - this._acl = createACL(); + + if (this.db) { + this.db.removeAllListeners(); + } + + this.createMainDataSource(options); this._cronJobManager = new CronJobManager(this); - if (this._db) { - // MaxListenersExceededWarning - this._db.removeAllListeners(); - } - - this._db = this.createDatabase(options); - - this._resourcer = createResourcer(options); this._cli = this.createCli(); this._i18n = createI18n(options); - this.context.db = this._db; - // this.context.logger = this._logger; - this.context.resourcer = this._resourcer; + this.context.db = this.db; + + this.context.resourcer = this.resourcer; this.context.cacheManager = this._cacheManager; this.context.cache = this._cache; @@ -958,10 +975,11 @@ export class Application exten actions: authActions, }); - this._resourcer.use(this._authManager.middleware(), { tag: 'auth' }); + this._dataSourceManager.use(this._authManager.middleware(), { tag: 'auth' }); + this.resourcer.use(this._authManager.middleware(), { tag: 'auth' }); if (this.options.acl !== false) { - this._resourcer.use(this._acl.middleware(), { tag: 'acl', after: ['auth'] }); + this.resourcer.use(this.acl.middleware(), { tag: 'acl', after: ['auth'] }); } this._locales = new Locale(createAppProxy(this)); @@ -981,6 +999,19 @@ export class Application exten this._version = new ApplicationVersion(this); } + protected createMainDataSource(options: ApplicationOptions) { + const mainDataSourceInstance = new MainDataSource({ + name: 'main', + database: this.createDatabase(options), + acl: createACL(), + resourceManager: createResourcer(options), + }); + + this._dataSourceManager = new DataSourceManager(); + + this.dataSourceManager.dataSources.set('main', mainDataSourceInstance); + } + protected createDatabase(options: ApplicationOptions) { const sqlLogger = this.createLogger({ filename: 'sql', diff --git a/packages/core/server/src/commands/db-sync.ts b/packages/core/server/src/commands/db-sync.ts index eabd994f3..ad3ec7e57 100644 --- a/packages/core/server/src/commands/db-sync.ts +++ b/packages/core/server/src/commands/db-sync.ts @@ -4,9 +4,17 @@ export default (app: Application) => { app .command('db:sync') .auth() + .preload() .action(async (...cliArgs) => { const [opts] = cliArgs; console.log('db sync...'); + + const Collection = app.db.getCollection('collections'); + if (Collection) { + // @ts-ignore + await Collection.repository.load(); + } + const force = false; await app.db.sync({ force, diff --git a/packages/core/server/src/commands/start.ts b/packages/core/server/src/commands/start.ts index c08d9596b..f363db613 100644 --- a/packages/core/server/src/commands/start.ts +++ b/packages/core/server/src/commands/start.ts @@ -9,13 +9,14 @@ export default (app: Application) => { .option('--quickstart') .action(async (...cliArgs) => { const [options] = cliArgs; - console.log('options', options); + console.log('start options', options); if (options.quickstart) { if (await app.isInstalled()) { await app.upgrade(); } else { await app.install(); } + app['_started'] = true; await app.restart(); app.log.info('app has been started'); diff --git a/packages/core/server/src/helper.ts b/packages/core/server/src/helper.ts index 8d2e2c08b..cbc3f3993 100644 --- a/packages/core/server/src/helper.ts +++ b/packages/core/server/src/helper.ts @@ -1,21 +1,20 @@ import cors from '@koa/cors'; -import Database from '@nocobase/database'; +import { requestLogger } from '@nocobase/logger'; import { Resourcer } from '@nocobase/resourcer'; -import { postPerfHooksWrap, prePerfHooksWrap, uid } from '@nocobase/utils'; +import { uid } from '@nocobase/utils'; import { Command } from 'commander'; +import { randomUUID } from 'crypto'; import fs from 'fs'; import i18next from 'i18next'; import bodyParser from 'koa-bodyparser'; import { resolve } from 'path'; +import { createHistogram, RecordableHistogram } from 'perf_hooks'; import Application, { ApplicationOptions } from './application'; import { parseVariables } from './middlewares'; import { dateTemplate } from './middlewares/data-template'; import { dataWrapping } from './middlewares/data-wrapping'; import { db2resource } from './middlewares/db2resource'; import { i18n } from './middlewares/i18n'; -import { requestLogger } from '@nocobase/logger'; -import { randomUUID } from 'crypto'; -import { createHistogram, RecordableHistogram } from 'perf_hooks'; export function createI18n(options: ApplicationOptions) { const instance = i18next.createInstance(); @@ -29,14 +28,6 @@ export function createI18n(options: ApplicationOptions) { return instance; } -export function createDatabase(options: ApplicationOptions) { - if (options.database instanceof Database) { - return options.database; - } else { - return new Database(options.database); - } -} - export function createResourcer(options: ApplicationOptions) { return new Resourcer({ ...options.resourcer }); } @@ -46,7 +37,9 @@ export function registerMiddlewares(app: Application, options: ApplicationOption app.context.reqId = randomUUID(); await next(); }); + app.use(requestLogger(app.name, options.logger?.request), { tag: 'logger' }); + app.use( cors({ exposeHeaders: ['content-disposition'], @@ -95,7 +88,8 @@ export function registerMiddlewares(app: Application, options: ApplicationOption app.resourcer.use(dateTemplate, { tag: 'dateTemplate', after: 'acl' }); app.use(db2resource, { tag: 'db2resource', after: 'dataWrapping' }); - app.use(app.resourcer.restApiMiddleware(), { tag: 'restApi', after: 'db2resource' }); + app.use(app.resourcer.restApiMiddleware({ skipIfDataSourceExists: true }), { tag: 'restApi', after: 'db2resource' }); + app.use(app.dataSourceManager.middleware(), { tag: 'dataSource', after: 'restApi' }); } export const createAppProxy = (app: Application) => { diff --git a/packages/core/server/src/helpers/multiple-instance-manager.ts b/packages/core/server/src/helpers/multiple-instance-manager.ts new file mode 100644 index 000000000..2c237bacf --- /dev/null +++ b/packages/core/server/src/helpers/multiple-instance-manager.ts @@ -0,0 +1,13 @@ +export class MultipleInstanceManager { + map: Map = new Map(); + + constructor() {} + + get(key: string) { + return this.map.get(key); + } + + set(key: string, value: Item) { + this.map.set(key, value); + } +} diff --git a/packages/core/server/src/main-data-source.ts b/packages/core/server/src/main-data-source.ts new file mode 100644 index 000000000..266b990aa --- /dev/null +++ b/packages/core/server/src/main-data-source.ts @@ -0,0 +1,19 @@ +import { DataSourceOptions, SequelizeDataSource } from '@nocobase/data-source-manager'; + +export class MainDataSource extends SequelizeDataSource { + init(options: DataSourceOptions = {}) { + const { acl, resourceManager, database } = options; + + this.acl = acl; + this.resourceManager = resourceManager; + + this.collectionManager = this.createCollectionManager({ + collectionManager: { + database, + collectionsFilter: (collection) => { + return collection.options.loadedFromCollectionManager; + }, + }, + }); + } +} diff --git a/packages/core/server/src/middlewares/db2resource.ts b/packages/core/server/src/middlewares/db2resource.ts index 15dde35da..2946b0249 100644 --- a/packages/core/server/src/middlewares/db2resource.ts +++ b/packages/core/server/src/middlewares/db2resource.ts @@ -1,9 +1,14 @@ import Database from '@nocobase/database'; import { getNameByParams, parseRequest, ResourcerContext, ResourceType } from '@nocobase/resourcer'; -export function db2resource(ctx: ResourcerContext & { db: Database }, next: () => Promise) { +export async function db2resource(ctx: ResourcerContext & { db: Database }, next: () => Promise) { + const dataSource = ctx.get('x-data-source'); + if (dataSource) { + return next(); + } const resourcer = ctx.resourcer; const database = ctx.db; + const params = parseRequest( { path: ctx.request.path, @@ -14,30 +19,44 @@ export function db2resource(ctx: ResourcerContext & { db: Database }, next: () = accessors: resourcer.options.accessors, }, ); + if (!params) { return next(); } const resourceName = getNameByParams(params); + // 如果资源名称未被定义 if (resourcer.isDefined(resourceName)) { return next(); } - const [collectionName, fieldName] = resourceName.split('.'); + + const splitResult = resourceName.split('.'); + + let collectionName = splitResult[0]; + const fieldName = splitResult[1]; + + if (collectionName.includes('@')) { + collectionName = collectionName.split('@')[1]; + } + // 如果经过加载后是已经定义的表 if (!database.hasCollection(collectionName)) { return next(); } + const collection = database.getCollection(collectionName); let resourceType: ResourceType = 'single'; if (fieldName && collection.hasField(fieldName)) { const field = collection.getField(fieldName); resourceType = field.type; } + resourcer.define({ type: resourceType, name: resourceName, }); + return next(); } diff --git a/packages/core/test/package.json b/packages/core/test/package.json index 73ad93837..de87a33d3 100644 --- a/packages/core/test/package.json +++ b/packages/core/test/package.json @@ -41,7 +41,7 @@ "dependencies": { "@faker-js/faker": "8.1.0", "@nocobase/server": "0.19.0-alpha.9", - "@playwright/test": "^1.40.1", + "@playwright/test": "^1.42.1", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", "@types/supertest": "^2.0.11", diff --git a/packages/core/test/src/e2e/defineConfig.ts b/packages/core/test/src/e2e/defineConfig.ts index dd50c9980..e802c573c 100644 --- a/packages/core/test/src/e2e/defineConfig.ts +++ b/packages/core/test/src/e2e/defineConfig.ts @@ -2,10 +2,10 @@ import { devices, defineConfig as playwrightDefineConfig, type PlaywrightTestCon export const defineConfig = (config?: PlaywrightTestConfig) => { return playwrightDefineConfig({ - timeout: process.env.CI ? 5 * 60 * 1000 : 30 * 1000, + timeout: 5 * 60 * 1000, expect: { - timeout: process.env.CI ? 1 * 60 * 1000 : 5000, + timeout: 10 * 1000, }, // Look for test files in the "tests" directory, relative to this configuration file. diff --git a/packages/core/test/src/e2e/e2eUtils.ts b/packages/core/test/src/e2e/e2eUtils.ts index 80fa2cdaf..b6409575f 100644 --- a/packages/core/test/src/e2e/e2eUtils.ts +++ b/packages/core/test/src/e2e/e2eUtils.ts @@ -692,7 +692,7 @@ const updateRole = async (roleSetting: AclRoleSetting) => { const headers = getHeaders(state); const name = roleSetting.name; - const result = await api.post(`/api/users/1/roles:update?filterByTk=${name}`, { + const result = await api.post(`/api/roles:update?filterByTk=${name}`, { headers, data: { ...roleSetting }, }); diff --git a/packages/core/test/src/e2e/templatesOfCollection.ts b/packages/core/test/src/e2e/templatesOfCollection.ts index 83b4fcace..28a4ddc57 100644 --- a/packages/core/test/src/e2e/templatesOfCollection.ts +++ b/packages/core/test/src/e2e/templatesOfCollection.ts @@ -286,6 +286,10 @@ export const generalWithBasic: CollectionSetting[] = [ name: 'icon', interface: 'icon', }, + { + name: 'sort', + interface: 'sort', + }, ], }, ]; @@ -403,6 +407,10 @@ export const generalWithAssociation: CollectionSetting[] = [ interface: 'm2m', target: 'users', }, + { + name: 'id', + interface: 'id', + }, ], }, ]; diff --git a/packages/core/test/src/e2e/templatesOfPage.ts b/packages/core/test/src/e2e/templatesOfPage.ts index 4c087cc90..a87be8b28 100644 --- a/packages/core/test/src/e2e/templatesOfPage.ts +++ b/packages/core/test/src/e2e/templatesOfPage.ts @@ -2765,7 +2765,7 @@ export const oneEmptyTableBlockWithCustomizeActions: PageConfig = { version: '2.0', type: 'void', 'x-component': 'Grid', - 'x-initializer': 'CreateFormBulkEditBlockInitializers', + 'x-initializer': 'BulkEditBlockInitializers', 'x-uid': '3gjmo60w4de', 'x-async': false, 'x-index': 1, diff --git a/packages/core/test/src/server/mockServer.ts b/packages/core/test/src/server/mockServer.ts index 902222984..a86878a04 100644 --- a/packages/core/test/src/server/mockServer.ts +++ b/packages/core/test/src/server/mockServer.ts @@ -1,5 +1,5 @@ import { mockDatabase } from '@nocobase/database'; -import Application, { AppSupervisor, ApplicationOptions, Gateway, PluginManager } from '@nocobase/server'; +import Application, { ApplicationOptions, AppSupervisor, Gateway, PluginManager } from '@nocobase/server'; import jwt from 'jsonwebtoken'; import qs from 'qs'; import supertest, { SuperAgentTest } from 'supertest'; @@ -89,16 +89,6 @@ export class MockServer extends Application { await this.runCommand('start', '--quickstart'); } - protected createDatabase(options: ApplicationOptions) { - const oldDatabase = this._db; - - const databaseOptions = oldDatabase ? oldDatabase.options : options?.database || {}; - const database = mockDatabase(databaseOptions); - database.setContext({ app: this }); - - return database; - } - async destroy(options: any = {}): Promise { await super.destroy(options); @@ -187,6 +177,16 @@ export class MockServer extends Application { }); return proxy as any; } + + protected createDatabase(options: ApplicationOptions) { + const oldDatabase = this.db; + + const databaseOptions = oldDatabase ? oldDatabase.options : options?.database || {}; + const database = mockDatabase(databaseOptions); + database.setContext({ app: this }); + + return database; + } } export function mockServer(options: ApplicationOptions = {}) { @@ -243,13 +243,13 @@ export async function createMockServer( if (beforeInstall) { await beforeInstall(app); } - await app.runCommand('install', '-f'); + await app.runCommandThrowError('install', '-f'); } if (version) { await app.version.update(version); } if (!skipStart) { - await app.runCommand('start'); + await app.runCommandThrowError('start'); } return app; } diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/NewRole.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/NewRole.tsx new file mode 100644 index 000000000..103ca00e3 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-acl/src/client/NewRole.tsx @@ -0,0 +1,93 @@ +import { SchemaComponent, useActionContext, useRequest } from '@nocobase/client'; +import React from 'react'; +import { useACLTranslation } from './locale'; +import { uid } from '@formily/shared'; + +export const NewRole: React.FC = () => { + const { t } = useACLTranslation(); + return ( + + Promise.resolve({ + data: { + name: `r_${uid()}`, + snippets: ['!ui.*', '!pm', '!pm.*'], + }, + }), + { ...options, refreshDeps: [ctx.visible] }, + ); + }, + }, + title: t('New role'), + properties: { + title: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + }, + name: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + description: + '{{t("Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.")}}', + }, + default: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + title: '', + 'x-content': '{{t("Default role")}}', + }, + footer: { + type: 'void', + 'x-component': 'Action.Drawer.Footer', + properties: { + cancel: { + title: '{{t("Cancel")}}', + 'x-component': 'Action', + 'x-component-props': { + useAction: '{{ cm.useCancelAction }}', + }, + }, + submit: { + title: '{{t("Submit")}}', + 'x-component': 'Action', + 'x-component-props': { + type: 'primary', + useAction: '{{ cm.useCreateAction }}', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }} + /> + ); +}; diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/RolesManagement.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/RolesManagement.tsx new file mode 100644 index 000000000..49b164ad0 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-acl/src/client/RolesManagement.tsx @@ -0,0 +1,110 @@ +import React, { useState } from 'react'; +import { Card, Row, Col, Tabs, Divider } from 'antd'; +import { CollectionProvider, CollectionProvider_deprecated, ResourceActionProvider, usePlugin } from '@nocobase/client'; +import { ISchema, Schema } from '@formily/react'; +import { RolesMenu } from './RolesMenu'; +import { useACLTranslation } from './locale'; +import ACLPlugin from '.'; +import { RolesManagerContext } from './RolesManagerProvider'; +import { Permissions } from './permissions/Permissions'; +import { NewRole } from './NewRole'; + +const collection = { + name: 'roles', + filterTargetKey: 'name', + targetKey: 'name', + fields: [ + { + type: 'string', + name: 'title', + interface: 'input', + uiSchema: { + title: '{{t("Role display name")}}', + type: 'number', + 'x-component': 'Input', + required: true, + } as ISchema, + }, + { + type: 'string', + name: 'name', + interface: 'input', + uiSchema: { + title: '{{t("Role UID")}}', + type: 'string', + 'x-component': 'Input', + } as ISchema, + }, + { + type: 'boolean', + name: 'default', + interface: 'boolean', + uiSchema: { + title: '{{t("Default role")}}', + type: 'boolean', + 'x-component': 'Checkbox', + } as ISchema, + }, + ], +}; + +export const RolesManagement: React.FC = () => { + const { t } = useACLTranslation(); + const aclPlugin = usePlugin(ACLPlugin); + const [activeKey, setActiveKey] = React.useState('permissions'); + const tabs = Array.from(aclPlugin.rolesManager.list()).map(([name, item]) => ({ + key: name, + label: Schema.compile(item.title, { t }), + children: item.Component ? React.createElement(item.Component, { active: activeKey === name }) : null, + })); + const [role, setRole] = useState(null); + + return ( + + + +
+ + + + + + + + + + + + setActiveKey(key)} + items={[ + { + key: 'permissions', + label: t('Permissions'), + children: , + }, + ...tabs, + ]} + /> + + + + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/RolesManagerProvider.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/RolesManagerProvider.tsx new file mode 100644 index 000000000..2e7a1805d --- /dev/null +++ b/packages/plugins/@nocobase/plugin-acl/src/client/RolesManagerProvider.tsx @@ -0,0 +1,8 @@ +import { createContext } from 'react'; + +export const RolesManagerContext = createContext<{ + role: any; + setRole: (role: any) => void; +}>({ + role: null, +} as any); diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/RolesMenu.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/RolesMenu.tsx new file mode 100644 index 000000000..c1cb3b7c4 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-acl/src/client/RolesMenu.tsx @@ -0,0 +1,129 @@ +import { + ActionContextProvider, + RecordProvider, + SchemaComponent, + useAPIClient, + useResourceActionContext, +} from '@nocobase/client'; +import { Menu, Empty, Dropdown, App, Tag, Row, Col } from 'antd'; +import { TagOutlined, MoreOutlined } from '@ant-design/icons'; +import React, { useContext, useEffect } from 'react'; +import { useACLTranslation } from './locale'; +import { Schema } from '@formily/react'; +import { RolesManagerContext } from './RolesManagerProvider'; +import { roleEditSchema } from './schemas/roles'; + +export const RolesMenu: React.FC & { + Item: React.FC<{ item: any; onEdit: () => void }>; +} = () => { + const { t } = useACLTranslation(); + const { data } = useResourceActionContext(); + const [visible, setVisible] = React.useState(false); + const [record, setRecord] = React.useState(null); + const { role, setRole } = useContext(RolesManagerContext); + const items = (data?.data || []).map((item: any) => ({ + key: item.name, + label: ( + { + setVisible(true); + setRecord(item); + }} + /> + ), + })); + + const handleSelect = ({ key }) => { + setRole((data?.data || []).find((item: any) => item.name === key)); + }; + + useEffect(() => { + if (!data?.data.length) { + return; + } + setRole(data?.data[0]); + }, [data, setRole]); + + return ( + <> + {items.length ? ( + + ) : ( + + )} + + + + + + + ); +}; + +RolesMenu.Item = function DepartmentTreeItem({ item, onEdit }) { + const { t } = useACLTranslation(); + const { refreshAsync } = useResourceActionContext(); + const { modal, message } = App.useApp(); + const api = useAPIClient(); + const deleteDepartment = () => { + modal.confirm({ + title: t('Delete'), + content: t('Are you sure you want to delete it?'), + onOk: async () => { + await api.resource('roles').destroy({ filterByTk: item.name }); + message.success(t('Deleted successfully')); + await refreshAsync(); + }, + }); + }; + const handleClick = ({ key, domEvent }) => { + domEvent.stopPropagation(); + switch (key) { + case 'edit': + onEdit(); + break; + case 'delete': + deleteDepartment(); + } + }; + return ( + + + + + {Schema.compile(item.title, { t })} + + + + {item.default ? ( + + {t('Default')} + + ) : null} + + + + + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/configure.test.ts b/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/configure.test.ts index cc7c3bc0f..9d1214011 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/configure.test.ts +++ b/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/configure.test.ts @@ -63,8 +63,13 @@ test('allows to confgiure plugins ', async ({ page, mockPage, mockRole, updateRo }, roleData); await page.reload(); await page.getByTestId('plugin-settings-button').click(); - await page.getByLabel('acl').click(); - await page.getByLabel(`action-Action.Link-Configure-roles-${roleData.name}`).click(); + await page.getByRole('link', { name: 'Users & Permissions' }).click(); + await page.getByText('Roles & Permissions').click(); + await page + .getByRole('menuitem', { name: `${roleData.name}` }) + .locator('span') + .nth(1) + .click(); await expect(page.getByRole('tab').getByText('Plugin settings permissions')).toBeVisible(); await updateRole({ name: roleData.name, @@ -128,22 +133,20 @@ test('plugin settings permissions', async ({ page, mockPage, mockRole, updateRol }, roleData); await page.reload(); await page.getByTestId('plugin-settings-button').hover(); - await expect(page.getByLabel('acl')).toBeVisible(); + await expect(page.getByRole('link', { name: 'Users & Permissions' })).toBeVisible(); await expect(page.getByLabel('auth')).not.toBeVisible(); await expect(page.getByLabel('collection-manager')).not.toBeVisible(); - await page.getByLabel('acl').click(); + await page.getByRole('link', { name: 'Users & Permissions' }).click(); await expect(page.getByRole('menuitem', { name: 'login Authentication' })).not.toBeVisible(); await expect(page.getByRole('menuitem', { name: 'database Collection manager' })).not.toBeVisible(); await updateRole({ name: roleData.name, - snippets: ['pm', 'pm.*', 'pm.auth.authenticators', 'pm.collection-manager', 'pm.collection-manager.collections'], + snippets: ['pm', 'pm.*', 'pm.auth.authenticators'], }); await page.reload(); await page.getByTestId('plugin-settings-button').hover(); - await expect(page.getByLabel('acl')).toBeVisible(); - await expect(page.getByLabel('auth')).toBeVisible(); - await expect(page.getByLabel('collection-manager')).toBeVisible(); - await page.getByLabel('acl').click(); + await expect(page.getByRole('link', { name: 'Users & Permissions' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Authentication' })).toBeVisible(); + await page.getByRole('link', { name: 'Users & Permissions' }).click(); await expect(page.getByRole('menuitem', { name: 'login Authentication' })).toBeVisible(); - await expect(page.getByRole('menuitem', { name: 'database Collection manager' })).toBeVisible(); }); diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/menu.test.ts b/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/menu.test.ts index e1d0304e4..589e85fba 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/menu.test.ts +++ b/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/menu.test.ts @@ -21,8 +21,13 @@ test('menu permission ', async ({ page, mockPage, mockRole, updateRole }) => { await expect(page.getByLabel('page1')).toBeVisible(); await expect(page.getByLabel('page2')).not.toBeVisible(); await page.getByTestId('plugin-settings-button').hover(); - await page.getByLabel('acl').click(); - await page.getByLabel(`action-Action.Link-Configure-roles-${roleData.name}`).click(); + await page.getByRole('link', { name: 'Users & Permissions' }).click(); + await page.getByText('Roles & Permissions').click(); + await page + .getByRole('menuitem', { name: `${roleData.name}` }) + .locator('span') + .nth(1) + .click(); await page.getByRole('tab').getByText('Menu permissions').click(); await page.waitForSelector('.ant-table'); await expect(page.getByRole('row', { name: 'page1' }).locator('.ant-checkbox-input')).toBeChecked({ checked: true }); @@ -33,8 +38,14 @@ test('menu permission ', async ({ page, mockPage, mockRole, updateRole }) => { await expect(page.getByLabel('page2')).toBeVisible(); await expect(page.getByLabel('page1')).not.toBeVisible(); await page.getByTestId('plugin-settings-button').hover(); - await page.getByLabel('acl').click(); - await page.getByLabel(`action-Action.Link-Configure-roles-${roleData.name}`).click(); + await page.getByRole('link', { name: 'Users & Permissions' }).click(); + await page.getByText('Roles & Permissions').click(); + await page.getByText(`${roleData.name}`).click(); + await page + .getByRole('menuitem', { name: `${roleData.name}` }) + .locator('span') + .nth(1) + .click(); await page.getByRole('tab').getByText('Menu permissions').click(); await page.waitForSelector('.ant-table'); await expect(page.getByRole('row', { name: 'page1' }).locator('.ant-checkbox-input')).toBeChecked({ checked: false }); diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/utils.ts b/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/utils.ts index 54857149e..216e4e0f1 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/utils.ts +++ b/packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/utils.ts @@ -48,6 +48,7 @@ export const oneTableBlock: PageConfig = { showIndex: true, collection: 'general', disableTemplate: false, + dataSource: 'main', }, _isJSONSchemaObject: true, properties: { diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/index.ts b/packages/plugins/@nocobase/plugin-acl/src/client/index.ts index ed9c6076e..8418f4a60 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/client/index.ts +++ b/packages/plugins/@nocobase/plugin-acl/src/client/index.ts @@ -1,9 +1,20 @@ import { Plugin } from '@nocobase/client'; +import { RolesManagement } from './RolesManagement'; +import { RolesManager } from './roles-manager'; + +class ACLPlugin extends Plugin { + rolesManager = new RolesManager(); -class AclPlugin extends Plugin { async load() { - // code + this.app.pluginSettingsManager.add('users-permissions.roles', { + title: '{{t("Roles & Permissions")}}', + icon: 'LockOutlined', + Component: RolesManagement, + aclSnippet: 'pm.roles', + sort: 3, + }); } } -export default AclPlugin; +export default ACLPlugin; +export { RolesManagerContext } from './RolesManagerProvider'; diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/locale.ts b/packages/plugins/@nocobase/plugin-acl/src/client/locale.ts new file mode 100644 index 000000000..e2996e284 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-acl/src/client/locale.ts @@ -0,0 +1,5 @@ +import { useTranslation } from 'react-i18next'; + +export function useACLTranslation() { + return useTranslation(['acl', 'client'], { nsMode: 'fallback' }); +} diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/permissions/ActionPermissions.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/ActionPermissions.tsx new file mode 100644 index 000000000..d174bdf58 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/ActionPermissions.tsx @@ -0,0 +1,181 @@ +import { + CollectionContext, + CollectionProvider, + CollectionProvider_deprecated, + ResourceActionContext, + SchemaComponent, + useAPIClient, + useActionContext, + useFilterFieldOptions, + useFilterFieldProps, + useCollectionRecord, + useRecord, + useRequest, + useResourceActionContext, +} from '@nocobase/client'; +import React, { useContext, useEffect } from 'react'; +import { roleCollectionsSchema } from '../schemas/roles'; +import { RolesManagerContext } from '../RolesManagerProvider'; +import { ISchema } from '@formily/react'; +import { RolesResourcesActions } from './RolesResourcesActions'; +import { useForm } from '@formily/react'; + +const collection = { + name: 'collections', + targetKey: 'name', + filterTargetKey: 'name', + fields: [ + { + type: 'integer', + name: 'title', + interface: 'input', + uiSchema: { + title: '{{t("Collection display name")}}', + type: 'number', + 'x-component': 'Input', + required: true, + } as ISchema, + }, + { + type: 'string', + name: 'name', + interface: 'input', + uiSchema: { + title: '{{t("Collection name")}}', + type: 'string', + 'x-component': 'Input', + } as ISchema, + }, + { + type: 'string', + name: 'type', + interface: 'input', + uiSchema: { + title: '{{t("Resource type")}}', + type: 'string', + 'x-component': 'Select', + enum: [ + { label: '{{t("Collection")}}', value: 'collection', color: 'green' }, + { label: '{{t("Association")}}', value: 'association', color: 'blue' }, + ], + } as ISchema, + }, + { + type: 'string', + name: 'usingConfig', + interface: 'input', + uiSchema: { + title: '{{t("Permission policy")}}', + type: 'string', + 'x-component': 'Select', + enum: [ + { label: '{{t("Individual")}}', value: 'resourceAction', color: 'orange' }, + { label: '{{t("General")}}', value: 'strategy', color: 'default' }, + ], + } as ISchema, + }, + { + type: 'hasMany', + name: 'fields', + target: 'fields', + collectionName: 'collections', + sourceKey: 'name', + targetKey: 'name', + uiSchema: {}, + }, + ], +}; + +const useFilterActionProps = () => { + const collection = useContext(CollectionContext); + const options = useFilterFieldOptions(collection.fields); + const service = useResourceActionContext(); + return useFilterFieldProps({ + options: options.filter((option) => ['title', 'name'].includes(option.name)), + params: service.state?.params?.[0] || service.params, + service, + }); +}; + +const useSaveRoleResourceAction = () => { + const form = useForm(); + const api = useAPIClient(); + const record = useRecord(); + const ctx = useActionContext(); + const { refresh } = useResourceActionContext(); + return { + async run() { + await api.resource('roles.resources', record.roleName)[record.exists ? 'update' : 'create']({ + filterByTk: record.name, + values: { + ...form.values, + name: record.name, + }, + }); + ctx.setVisible(false); + refresh(); + }, + }; +}; + +const useRoleResourceValues = (options: any) => { + const record = useRecord(); + const { visible } = useActionContext(); + const result = useRequest( + { + resource: 'roles.resources', + resourceOf: record.roleName, + action: 'get', + params: { + appends: ['actions', 'actions.scope'], + filterByTk: record.name, + }, + }, + { ...options, manual: true }, + ); + useEffect(() => { + if (!record.exists) { + options.onSuccess({ + data: {}, + }); + return; + } + if (visible) { + result.run(); + } + }, [visible, record.exists]); + return result; +}; + +export const ActionPermissions: React.FC<{ + active: boolean; +}> = ({ active }) => { + const { role } = useContext(RolesManagerContext); + const service = useRequest( + { + resource: 'roles.collections', + resourceOf: role.name, + action: 'list', + params: { + pageSize: 20, + filter: { hidden: { $isFalsy: true } }, + sort: ['sort'], + }, + }, + { + ready: !!role && active, + refreshDeps: [role?.name], + }, + ); + return ( + + + + + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/permissions/AvailableActions.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/AvailableActions.tsx new file mode 100644 index 000000000..d12367bf7 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/AvailableActions.tsx @@ -0,0 +1,22 @@ +import { useRequest } from '@nocobase/client'; +import { Spin } from 'antd'; +import React, { createContext, useContext } from 'react'; + +const AvailableActionsContext = createContext([]); + +export const AvailableActionsProvider: React.FC = (props) => { + const { data, loading } = useRequest<{ + data: any[]; + }>({ + resource: 'availableActions', + action: 'list', + }); + if (loading) { + return ; + } + return {props.children}; +}; + +export const useAvailableActions = () => { + return useContext(AvailableActionsContext); +}; diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/permissions/GeneralPermissions.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/GeneralPermissions.tsx new file mode 100644 index 000000000..376262ab1 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/GeneralPermissions.tsx @@ -0,0 +1,129 @@ +import { onFormValuesChange, createForm, Form } from '@formily/core'; +import { connect } from '@formily/react'; +import { SchemaComponent, useAPIClient, useRequest } from '@nocobase/client'; +import { Checkbox, message } from 'antd'; +import uniq from 'lodash/uniq'; +import React, { useContext, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { RolesManagerContext } from '../RolesManagerProvider'; +import { StrategyActions } from './StrategyActions'; +import { useACLTranslation } from '../locale'; +import { uid } from '@formily/shared'; +import { useMemoizedFn } from 'ahooks'; + +const SnippetCheckboxGroup = connect((props) => { + const { t } = useTranslation(); + return ( + { + const snippets = ['ui.*', 'pm', 'pm.*', 'app']; + const disallowSnippets = snippets.map((key) => `!${key}`); + const value = uniq([...(props.value || []), ...values]) + .filter((key) => key && !disallowSnippets.includes(key)) + .map((key) => { + if (!snippets.includes(key) || values?.includes(key)) { + return key; + } + return `!${key}`; + }); + for (const key of snippets) { + if (!value.includes(key) && !value.includes(`!${key}`)) { + value.push(`!${key}`); + } + } + props.onChange(value); + }} + > +
+ {t('Allows to configure interface')} +
+
+ {t('Allows to install, activate, disable plugins')} +
+
+ {t('Allows to configure plugins')} +
+
+ {t('Allows to clear cache, reboot application')} +
+
+ ); +}); + +export const GeneralPermissions: React.FC<{ + active: boolean; +}> = ({ active }) => { + const { role, setRole } = useContext(RolesManagerContext); + const { t } = useACLTranslation(); + const api = useAPIClient(); + const { data } = useRequest( + () => + api + .resource('roles') + .get({ + filterByTk: role?.name, + }) + .then((res) => { + const record = res?.data?.data; + record.snippets?.forEach((key: string) => { + record[key] = true; + }); + return record; + }), + { + ready: active, + refreshDeps: [role?.name], + }, + ); + const update = useMemoizedFn(async (form: Form) => { + await api.resource('roles').update({ + filterByTk: role.name, + values: form.values, + }); + setRole({ ...role, ...form.values }); + message.success(t('Saved successfully')); + }); + const form = useMemo(() => { + return createForm({ + values: data, + effects() { + onFormValuesChange(async (form) => { + await update(form); + }); + }, + }); + }, [data, update]); + + return ( + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/permissions/MenuItemsProvider.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/MenuItemsProvider.tsx new file mode 100644 index 000000000..08f73b241 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/MenuItemsProvider.tsx @@ -0,0 +1,53 @@ +import { useAdminSchemaUid, useRequest } from '@nocobase/client'; +import { Spin } from 'antd'; +import React, { createContext, useContext } from 'react'; + +const MenuItemsContext = createContext(null); + +export const toItems = (properties = {}) => { + const items = []; + for (const key in properties) { + if (Object.prototype.hasOwnProperty.call(properties, key)) { + const element = properties[key]; + const item = { + title: element.title, + uid: element['x-uid'], + }; + if (element.properties) { + item['children'] = toItems(element.properties); + } + items.push(item); + } + } + return items; +}; + +export const useMenuItems = () => { + return useContext(MenuItemsContext); +}; + +export const MenuItemsProvider = (props) => { + const adminSchemaUid = useAdminSchemaUid(); + const options = { + url: `uiSchemas:getProperties/${adminSchemaUid}`, + }; + const service = useRequest<{ + data: { + properties: any; + }; + }>(options); + if (service.loading) { + return ; + } + const items = toItems(service.data?.data?.properties); + return ( + + {props.children} + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/permissions/MenuPermissions.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/MenuPermissions.tsx new file mode 100644 index 000000000..836b7427b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/MenuPermissions.tsx @@ -0,0 +1,157 @@ +import { Checkbox, message, Table } from 'antd'; +import { uniq } from 'lodash'; +import React, { useContext, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useStyles } from './style'; +import { useAPIClient, useCollectionRecord, useRequest } from '@nocobase/client'; +import { RolesManagerContext } from '../RolesManagerProvider'; +import { useMenuItems } from './MenuItemsProvider'; + +const findUids = (items) => { + if (!Array.isArray(items)) { + return []; + } + const uids = []; + for (const item of items) { + uids.push(item.uid); + uids.push(...findUids(item.children)); + } + return uids; +}; +const getParentUids = (tree, func, path = []) => { + if (!tree) return []; + for (const data of tree) { + path.push(data.uid); + if (func(data)) return path; + if (data.children) { + const findChildren = getParentUids(data.children, func, path); + if (findChildren.length) return findChildren; + } + path.pop(); + } + return []; +}; +const getChildrenUids = (data = [], arr = []) => { + for (const item of data) { + arr.push(item.uid); + if (item.children && item.children.length) getChildrenUids(item.children, arr); + } + return arr; +}; + +export const MenuPermissions: React.FC<{ + active: boolean; +}> = ({ active }) => { + const { styles } = useStyles(); + const { role } = useContext(RolesManagerContext); + const api = useAPIClient(); + const { items } = useMenuItems(); + const { t } = useTranslation(); + const allUids = findUids(items); + const [uids, setUids] = useState([]); + const { loading, refresh } = useRequest( + { + resource: 'roles.menuUiSchemas', + resourceOf: role.name, + action: 'list', + params: { + paginate: false, + }, + }, + { + ready: !!role && active, + refreshDeps: [role?.name], + onSuccess(data) { + setUids(data?.data?.map((schema) => schema['x-uid']) || []); + }, + }, + ); + const resource = api.resource('roles.menuUiSchemas', role.name); + const allChecked = allUids.length === uids.length; + + const handleChange = async (checked, schema) => { + const parentUids = getParentUids(items, (data) => data.uid === schema.uid); + const childrenUids = getChildrenUids(schema?.children, []); + if (checked) { + const totalUids = childrenUids.concat(schema.uid); + const newUids = uids.filter((v) => !totalUids.includes(v)); + setUids([...newUids]); + await resource.remove({ + values: totalUids, + }); + } else { + const totalUids = childrenUids.concat(parentUids); + setUids((prev) => { + return uniq([...prev, ...totalUids]); + }); + await resource.add({ + values: totalUids, + }); + } + message.success(t('Saved successfully')); + }; + + const translateTitle = (menus: any[]) => { + return menus.map((menu) => { + const title = t(menu.title); + if (menu.children) { + return { + ...menu, + title, + children: translateTitle(menu.children), + }; + } + return { + ...menu, + title, + }; + }); + }; + + return ( +
+ { + if (allChecked) { + await resource.set({ + values: [], + }); + } else { + await resource.set({ + values: allUids, + }); + } + refresh(); + message.success(t('Saved successfully')); + }} + />{' '} + {t('Accessible')} + + ), + render: (_, schema) => { + const checked = uids.includes(schema.uid); + return handleChange(checked, schema)} />; + }, + }, + ]} + dataSource={translateTitle(items)} + /> + ); +}; diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/permissions/Permissions.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/Permissions.tsx new file mode 100644 index 000000000..2f3163524 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/Permissions.tsx @@ -0,0 +1,87 @@ +import React, { useContext, useEffect, useMemo } from 'react'; +import { Tabs } from 'antd'; +import { useApp } from '@nocobase/client'; +import { useACLTranslation } from '../locale'; +import { GeneralPermissions } from './GeneralPermissions'; +import { AvailableActionsProvider } from './AvailableActions'; +import { ActionPermissions } from './ActionPermissions'; +import { MenuPermissions } from './MenuPermissions'; +import { MenuItemsProvider } from './MenuItemsProvider'; +import { PluginPermissions } from './PluginPermissions'; +import { RolesManagerContext } from '../RolesManagerProvider'; + +const TabLayout: React.FC = (props) => { + return
{props.children}
; +}; + +export const Permissions: React.FC<{ active: boolean }> = ({ active }) => { + const { t } = useACLTranslation(); + const [activeKey, setActiveKey] = React.useState('general'); + const { role } = useContext(RolesManagerContext); + const pm = role?.snippets?.includes('pm.*'); + const app = useApp(); + const DataSourcePermissionManager = app.getComponent('DataSourcePermissionManager'); + + const items = useMemo( + () => [ + { + key: 'general', + label: t('General permissions'), + children: ( + + + + ), + }, + { + key: 'menu', + label: t('Menu permissions'), + children: ( + + + + + + ), + }, + ...(pm + ? [ + { + key: 'plugin', + label: t('Plugin settings permissions'), + children: ( + + + + ), + }, + ] + : []), + ...(DataSourcePermissionManager + ? [ + { + key: 'dataSource', + label: t('Data source permissions'), + children: ( + + + + + + ), + }, + ] + : []), + ], + [pm, activeKey, active, t], + ); + + useEffect(() => { + setActiveKey('general'); + }, [role?.name]); + return ( + + setActiveKey(key)} items={items} /> + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/permissions/PluginPermissions.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/PluginPermissions.tsx new file mode 100644 index 000000000..eb32f9ab7 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/PluginPermissions.tsx @@ -0,0 +1,140 @@ +import { useAPIClient, useApp, useCompile, useCollectionRecord, useRequest } from '@nocobase/client'; +import { Checkbox, message, Table } from 'antd'; +import React, { useContext, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useStyles } from './style'; +import { RolesManagerContext } from '../RolesManagerProvider'; +import lodash from 'lodash'; + +const getParentKeys = (tree, func, path = []) => { + if (!tree) return []; + for (const data of tree) { + path.push(data.key); + if (func(data)) return path; + if (data.children) { + const findChildren = getParentKeys(data.children, func, path); + if (findChildren.length) return findChildren; + } + path.pop(); + } + return []; +}; +const getChildrenKeys = (data = [], arr = []) => { + for (const item of data) { + arr.push(item.aclSnippet); + if (item.children && item.children.length) getChildrenKeys(item.children, arr); + } + return arr; +}; + +export const PluginPermissions: React.FC<{ + active: boolean; +}> = ({ active }) => { + const app = useApp(); + const { styles } = useStyles(); + const { role } = useContext(RolesManagerContext); + const api = useAPIClient(); + const compile = useCompile(); + const settings = app.pluginSettingsManager.getList(false); + const allAclSnippets = app.pluginSettingsManager.getAclSnippets(); + const [snippets, setSnippets] = useState([]); + const allChecked = useMemo( + () => snippets.includes('pm.*') && snippets.every((item) => !item.startsWith('!pm.')), + [snippets], + ); + + const { t } = useTranslation(); + const { loading, refresh } = useRequest( + { + resource: 'roles.snippets', + resourceOf: role.name, + action: 'list', + params: { + paginate: false, + }, + }, + { + ready: !!role && active, + refreshDeps: [role?.name], + onSuccess(data) { + setSnippets(data?.data || []); + }, + }, + ); + const resource = api.resource('roles.snippets', role.name); + const handleChange = async (checked, record) => { + const childrenKeys = getChildrenKeys(record?.children, []); + const totalKeys = childrenKeys.concat(record.aclSnippet); + if (!checked) { + await resource.remove({ + values: totalKeys.map((v) => '!' + v), + }); + refresh(); + } else { + await resource.add({ + values: totalKeys.map((v) => '!' + v), + }); + refresh(); + } + message.success(t('Saved successfully')); + }; + return ( +
{ + return compile(value); + }, + }, + { + dataIndex: 'accessible', + title: ( + <> + { + const values = allAclSnippets.map((v) => '!' + v); + if (!allChecked) { + await resource.remove({ + values, + }); + } else { + await resource.add({ + values, + }); + } + refresh(); + message.success(t('Saved successfully')); + }} + />{' '} + {t('Accessible')} + + ), + render: (_, record) => { + const checked = !snippets.includes('!' + record.aclSnippet); + return handleChange(checked, record)} />; + }, + }, + ]} + dataSource={settings + .filter((v) => { + return v.isTopLevel !== false; + }) + .map((v) => { + if (v.showTabs !== false) { + return v; + } + return lodash.omit(v, 'children'); + })} + /> + ); +}; diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/permissions/RolesResourcesActions.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/RolesResourcesActions.tsx new file mode 100644 index 000000000..f7392965f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/RolesResourcesActions.tsx @@ -0,0 +1,229 @@ +import { FormItem, FormLayout } from '@formily/antd-v5'; +import { ArrayField } from '@formily/core'; +import { connect, useField, useForm } from '@formily/react'; +import { + useCollectionManager, + useCollectionManager_deprecated, + useCompile, + useCollectionRecord, + useRecord, +} from '@nocobase/client'; +import { Checkbox, Table, Tag } from 'antd'; +import { isEmpty } from 'lodash'; +import React, { createContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useAvailableActions } from './AvailableActions'; +import { ScopeSelect } from './ScopeSelect'; +import { useStyles } from './style'; + +const toActionMap = (arr: any[]) => { + const obj = {}; + arr?.forEach?.((action) => { + if (action.name) { + obj[action.name] = action; + obj[action.name]['scope'] = isEmpty(action.scope) ? null : action.scope; + } + }); + return obj; +}; + +export const RoleResourceCollectionContext = createContext({}); + +export const RolesResourcesActions = connect((props) => { + const { styles } = useStyles(); + // const { onChange } = props; + const onChange = (values) => { + const items = values.map((item) => { + return { + ...item, + scope: isEmpty(item.scope) ? null : item.scope, + }; + }); + props.onChange(items); + }; + const form = useForm(); + const roleCollection = useRecord(); + const availableActions = useAvailableActions(); + const { getCollection, getCollectionFields } = useCollectionManager_deprecated(); + const collection = getCollection(roleCollection.collectionName); + const collectionFields = getCollectionFields(roleCollection.collectionName); + const compile = useCompile(); + const { t } = useTranslation(); + const field = useField(); + const actionMap: any = toActionMap(field.value || []); + const inAction = (actionName, fieldName) => { + const action = actionMap?.[actionName]; + if (!action) { + return false; + } + return action?.fields?.includes(fieldName); + }; + const availableActionsWithFields = availableActions.filter((action) => action.allowConfigureFields); + const fieldPermissions = collectionFields + ?.filter((field) => field.interface) + ?.map((field) => { + const permission = { ...field }; + for (const action of availableActionsWithFields) { + permission[action.name] = inAction(action.name, field.name); + } + return permission; + }); + const toggleAction = (actionName: string) => { + if (actionMap[actionName]) { + delete actionMap[actionName]; + } else { + actionMap[actionName] = { + name: actionName, + fields: collectionFields?.filter((field) => field.interface)?.map?.((item) => item.name), + }; + } + onChange(Object.values(actionMap)); + }; + const setScope = (actionName, scope) => { + if (!actionMap[actionName]) { + toggleAction(actionName); + actionMap[actionName]['scope'] = scope; + } else { + actionMap[actionName]['scope'] = scope; + onChange(Object.values(actionMap)); + } + }; + const allChecked = {}; + for (const action of availableActionsWithFields) { + allChecked[action.name] = + collectionFields?.filter((field) => field.interface)?.length === actionMap?.[action.name]?.fields?.length; + } + return ( +
+ + + +
compile(value), + }, + { + dataIndex: 'onNewRecord', + title: t('Action type'), + render: (onNewRecord) => + onNewRecord ? ( + {t('Action on new records')} + ) : ( + {t('Action on existing records')} + ), + }, + { + dataIndex: 'enabled', + title: t('Allow'), + render: (enabled, action) => ( + { + toggleAction(action.name); + }} + /> + ), + }, + { + dataIndex: 'scope', + title: t('Data scope'), + render: (value, action) => + !action.onNewRecord && ( + { + setScope(action.name, scope); + }} + /> + ), + }, + ]} + dataSource={availableActions?.map((item) => { + let enabled = false; + let scope = null; + if (actionMap[item.name]) { + enabled = true; + if (!item.onNewRecord) { + scope = actionMap[item.name]['scope']; + } + } + return { + ...item, + enabled, + scope, + }; + })} + /> + + +
compile(value), + }, + ...availableActionsWithFields.map((action) => { + const checked = allChecked?.[action.name]; + return { + dataIndex: action.name, + title: ( + <> + { + const item = actionMap[action.name] || { + name: action.name, + }; + if (checked) { + item.fields = []; + } else { + item.fields = collectionFields?.map?.((item) => item.name); + } + actionMap[action.name] = item; + onChange(Object.values(actionMap)); + }} + />{' '} + {compile(action.displayName)} + + ), + render: (checked, field) => ( + { + const item = actionMap[action.name] || { + name: action.name, + }; + const fields: string[] = item.fields || []; + if (checked) { + const index = fields.indexOf(field.name); + fields.splice(index, 1); + } else { + fields.push(field.name); + } + item.fields = fields; + actionMap[action.name] = item; + onChange(Object.values(actionMap)); + }} + /> + ), + }; + }), + ]} + /> + + + + + ); +}); diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/permissions/ScopeSelect.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/ScopeSelect.tsx new file mode 100644 index 000000000..e96113f2b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/ScopeSelect.tsx @@ -0,0 +1,44 @@ +import { createForm } from '@formily/core'; +import React, { createContext, useContext, useMemo, useState } from 'react'; +import { FormProvider, SchemaComponent } from '@nocobase/client'; +import { scopesSchema } from '../schemas/scopes'; + +const RolesResourcesScopesSelectedRowKeysContext = createContext(null); + +const RolesResourcesScopesSelectedRowKeysProvider: React.FC = (props) => { + const [keys, setKeys] = useState([]); + return ( + + {props.children} + + ); +}; + +export const useRolesResourcesScopesSelectedRowKeys = () => { + return useContext(RolesResourcesScopesSelectedRowKeysContext); +}; + +export const ScopeSelect = (props) => { + const form = useMemo( + () => + createForm({ + values: { + scope: props.value, + }, + }), + [], + ); + return ( + + + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-acl/src/client/permissions/StrategyActions.tsx b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/StrategyActions.tsx new file mode 100644 index 000000000..097b7a884 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-acl/src/client/permissions/StrategyActions.tsx @@ -0,0 +1,121 @@ +import { ArrayField } from '@formily/core'; +import { connect, useField } from '@formily/react'; +import { Checkbox, Select, Table, Tag } from 'antd'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useCompile } from '@nocobase/client'; +import { useAvailableActions } from './AvailableActions'; + +const toScopes = (value) => { + if (!value) { + return {}; + } + const scopes = {}; + value?.forEach?.((item) => { + const [name, scope] = item.split(':'); + scopes[name] = scope || 'all'; + }); + return scopes; +}; + +const toFieldValue = (scopes) => { + const values = []; + for (const name in scopes) { + if (Object.prototype.hasOwnProperty.call(scopes, name)) { + const scope = scopes[name]; + if (scope === 'all') { + values.push(name); + } else { + values.push(`${name}:${scope}`); + } + } + } + return values; +}; + +export const StrategyActions = connect((props) => { + const { onChange } = props; + const availableActions = useAvailableActions(); + const compile = useCompile(); + const { t } = useTranslation(); + const field = useField(); + const scopes = toScopes(field.value); + return ( +
+
compile(value), + }, + { + dataIndex: 'onNewRecord', + title: t('Action type'), + render: (onNewRecord) => + onNewRecord ? ( + {t('Action on new records')} + ) : ( + {t('Action on existing records')} + ), + }, + { + dataIndex: 'enabled', + title: t('Allow'), + render: (enabled, action) => ( + { + if (enabled) { + delete scopes[action.name]; + } else { + scopes[action.name] = 'all'; + } + onChange(toFieldValue(scopes)); + }} + /> + ), + }, + { + dataIndex: 'scope', + title: t('Data scope'), + render: (scope, action) => + !action.onNewRecord && ( + { + const interfaceConfig = getInterface(value); + handleFieldChange( + { + interface: value, + uiSchema: { title: record?.uiSchema?.title, ...interfaceConfig?.default?.uiSchema }, + }, + record.name, + ); + }} + onDropdownVisibleChange={(open) => { + if (open && record?.possibleTypes) { + const targetRecord = Object.values(form.values)?.[0]?.[field.index]; + const targetType = targetRecord?.type || record.type; + const newOptions = getInterfaceOptions(initOptions, targetType); + setOptions(newOptions); + } + }} + > + {options.map((group) => ( + + {group.children.map((item) => ( + + {compile(item.label)} + + ))} + + ))} + + ); + }, + { displayName: 'CollectionFieldInterfaceSelect' }, +); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/CollectionName.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/CollectionName.tsx new file mode 100644 index 000000000..8dedad30a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/CollectionName.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { LoadingOutlined } from '@ant-design/icons'; +import { connect, mapProps, mapReadPretty } from '@formily/react'; +import { Input as AntdInput } from 'antd'; +import cls from 'classnames'; +import { useCollectionRecord } from '@nocobase/client'; +import { usePrefixCls } from '@formily/antd-v5/esm/__builtins__'; + +const ReadPretty = (props) => { + const prefixCls = usePrefixCls('description-input', props); + const { + data: { name, tableName }, + } = useCollectionRecord() as any; + return ( +
+ {name !== tableName ? ( + <> + {name} ({tableName}) + + ) : ( + props.value + )} +
+ ); +}; +export const CollectionName = Object.assign( + connect( + AntdInput, + mapProps((props, field) => { + return { + ...props, + suffix: {field?.['loading'] || field?.['validating'] ? : props.suffix}, + }; + }), + mapReadPretty(ReadPretty), + ), +); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/FieldTitleInput.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/FieldTitleInput.tsx new file mode 100644 index 000000000..78fd5373f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/FieldTitleInput.tsx @@ -0,0 +1,41 @@ +import { observer } from '@formily/react'; +import { Input } from 'antd'; +import { debounce } from 'lodash'; +import React, { useState, useMemo, useEffect } from 'react'; +import { useRecord } from '@nocobase/client'; + +export const FieldTitleInput = observer((props: any) => { + const { value, handleFieldChange } = props; + const record = useRecord(); + const [titleValue, setTitleValue] = useState(value); + // 实时更新 + const handleRealTimeChange = (newValue: string) => { + setTitleValue(newValue); + }; + + // 防抖操作 + const debouncedHandleFieldChange = useMemo(() => { + return debounce((newTitle: string) => { + handleFieldChange( + { + uiSchema: { + ...record?.uiSchema, + title: newTitle, + }, + }, + record.name, + ); + }, 1000); + }, [handleFieldChange, record]); + + // 统一处理函数,实时更新+防抖 + const handleChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + handleRealTimeChange(newValue); + debouncedHandleFieldChange(newValue); + }; + useEffect(() => { + setTitleValue(value); + }, [value]); + return ; +}); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/FieldType.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/FieldType.tsx new file mode 100644 index 000000000..eb5f4175b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/FieldType.tsx @@ -0,0 +1,31 @@ +import { observer } from '@formily/react'; +import { useRecord } from '@nocobase/client'; +import { Select, Tag } from 'antd'; +import React from 'react'; + +export const FieldType = observer( + (props: any) => { + const { value, handleFieldChange, onChange } = props; + const record = useRecord(); + const item = record; + return !item?.possibleTypes ? ( + {value} + ) : ( +
+ + ) + ); +}; diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/index.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/index.tsx new file mode 100644 index 000000000..84ca05760 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/index.tsx @@ -0,0 +1,282 @@ +import React, { useEffect, useState } from 'react'; +import { Select } from 'antd'; +import { observer, useForm, useField } from '@formily/react'; +import { useParams } from 'react-router-dom'; +import { useRecord, useCompile, useAPIClient, useCollectionManager_deprecated } from '@nocobase/client'; +import { useRemoteCollectionContext } from '../CollectionFields'; + +const supportTypes = ['string', 'bigInt', 'integer', 'uuid', 'uid']; + +export const SourceKey = observer( + (props: any) => { + const { name: dataSourceKey } = useParams(); + const { collectionName, sourceKey, name } = useRecord(); + const field: any = useField(); + const compile = useCompile(); + const { getCollection } = useCollectionManager_deprecated(); + const options = getCollection(collectionName || name, dataSourceKey) + .fields?.filter((v) => { + return v.primaryKey || v.unique; + }) + .map((k) => { + return { + value: k.name, + label: compile(k.uiSchema?.title || k.title || k.name), + }; + }); + useEffect(() => { + field.initialValue = options?.[0]?.value || sourceKey; + }, []); + return ( +
+ { + const { target, type, through } = form.values; + const effectField = ['belongsTo'].includes(type) + ? collectionName + : ['belongsToMany'].includes(type) + ? through + : target; + if (effectField && open) { + const { data } = await api.request({ + url: `dataSourcesCollections/${dataSourceKey}.${effectField}/fields:list`, + params: { + paginate: false, + filter: { + $or: [{ 'interface.$not': null }, { 'options.source.$notEmpty': true }], + }, + sort: ['sort'], + }, + }); + setOptions( + data.data + ?.filter((v) => { + return supportTypes.includes(v.type); + }) + .map((k) => { + return { + value: k.name, + label: compile(k.uiSchema?.title || k.title || k.name), + }; + }), + ); + } + }} + onChange={(value) => { + props?.onChange?.(value); + setInitialValue(value); + }} + /> +
+ ); + }, + { displayName: 'ForeignKey' }, +); +export const TargetKey = observer( + (props: any) => { + const { value, disabled } = props; + const { targetKey, target, type } = useRecord(); + const { name: dataSourceKey } = useParams(); + const { getCollection } = useCollectionManager_deprecated(); + const api = useAPIClient(); + const [options, setOptions] = useState([]); + const [initialValue, setInitialValue] = useState(value || targetKey); + const form = useForm(); + const compile = useCompile(); + const field: any = useField(); + field.required = true; + useEffect(() => { + if (target) { + setOptions( + getCollection(target, dataSourceKey) + .fields?.filter((v) => { + if (type !== 'hasMany') { + return v.primaryKey || v.unique; + } + return supportTypes.includes(v.type); + }) + .map((k) => { + return { + value: k.name, + label: compile(k?.uiSchema?.title || k.title || k.name), + }; + }), + ); + } + }, []); + return ( +
+ +
+ ); + }, + { displayName: 'SourceCollection' }, +); + +export const ThroughCollection = observer( + (props: any) => { + const { disabled } = props; + const compile = useCompile(); + const [options, setOptions] = useState([]); + const { name: dataSourceKey } = useParams(); + const field: any = useField(); + const { getCollections } = useCollectionManager_deprecated(dataSourceKey); + const record = useRecord(); + const value = record[field.props.name]; + + const loadCollections = () => { + const filteredItems = getCollections().filter((item) => { + const isAutoCreateAndThrough = item.autoCreate && item.isThrough; + if (isAutoCreateAndThrough) { + return false; + } + return true; + }); + return filteredItems.map((item) => ({ + label: compile(item.title || item.name), + value: item.name, + })); + }; + useEffect(() => { + const data = loadCollections(); + setOptions(data); + }, []); + return ( +
+
compile(value), + }, + { + dataIndex: 'onNewRecord', + title: t('Action type'), + render: (onNewRecord) => + onNewRecord ? ( + {t('Action on new records')} + ) : ( + {t('Action on existing records')} + ), + }, + { + dataIndex: 'enabled', + title: t('Allow'), + render: (enabled, action) => ( + { + toggleAction(action.name); + }} + /> + ), + }, + { + dataIndex: 'scope', + title: t('Data scope'), + render: (value, action) => + !action.onNewRecord && ( + { + setScope(action.name, scope); + }} + /> + ), + }, + ]} + dataSource={availableActions?.map((item) => { + let enabled = false; + let scope = null; + if (actionMap[item.name]) { + enabled = true; + if (!item.onNewRecord) { + scope = actionMap[item.name]['scope']; + } + } + return { + ...item, + enabled, + scope, + }; + })} + /> + + +
compile(value), + }, + ...availableActionsWithFields.map((action) => { + const checked = allChecked?.[action.name]; + return { + dataIndex: action.name, + title: ( + <> + { + const item = actionMap[action.name] || { + name: action.name, + }; + if (checked) { + item.fields = []; + } else { + item.fields = collectionFields?.map?.((item) => item.name); + } + actionMap[action.name] = item; + onChange(Object.values(actionMap)); + }} + />{' '} + {compile(action.displayName)} + + ), + render: (checked, field) => ( + { + const item = actionMap[action.name] || { + name: action.name, + }; + const fields: string[] = item.fields || []; + if (checked) { + const index = fields.indexOf(field.name); + fields.splice(index, 1); + } else { + fields.push(field.name); + } + item.fields = fields; + actionMap[action.name] = item; + onChange(Object.values(actionMap)); + }} + /> + ), + }; + }), + ]} + /> + + + + + ); +}); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/PermissionManager/ScopeSelect.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/PermissionManager/ScopeSelect.tsx new file mode 100644 index 000000000..aaa2c09d1 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/PermissionManager/ScopeSelect.tsx @@ -0,0 +1,47 @@ +import { createForm } from '@formily/core'; +import React, { createContext, useContext, useMemo, useState } from 'react'; +import { FormProvider, SchemaComponent, useRecord } from '@nocobase/client'; +import { getScopesSchema } from './schemas/scopes'; + +const RolesResourcesScopesSelectedRowKeysContext = createContext(null); + +const RolesResourcesScopesSelectedRowKeysProvider: React.FC = (props) => { + const [keys, setKeys] = useState([]); + return ( + + {props.children} + + ); +}; + +export const useRolesResourcesScopesSelectedRowKeys = () => { + return useContext(RolesResourcesScopesSelectedRowKeysContext); +}; + +export const ScopeSelect = (props) => { + const form = useMemo( + () => + createForm({ + values: { + scope: props.value, + }, + }), + [], + ); + + const { key } = useRecord(); + const scopesSchema = getScopesSchema(key); + return ( + + + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/PermissionManager/StrategyActions.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/PermissionManager/StrategyActions.tsx new file mode 100644 index 000000000..2743e5db9 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/PermissionManager/StrategyActions.tsx @@ -0,0 +1,119 @@ +import { ArrayField } from '@formily/core'; +import { connect, useField } from '@formily/react'; +import { Checkbox, Select, Table, Tag } from 'antd'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useCompile } from '@nocobase/client'; +import { useAvailableActions } from './DataSourceTable'; + +const toScopes = (value) => { + if (!value) { + return {}; + } + const scopes = {}; + value?.forEach?.((item) => { + const [name, scope] = item.split(':'); + scopes[name] = scope || 'all'; + }); + return scopes; +}; + +const toFieldValue = (scopes) => { + const values = []; + for (const name in scopes) { + if (Object.prototype.hasOwnProperty.call(scopes, name)) { + const scope = scopes[name]; + if (scope === 'all') { + values.push(name); + } else { + values.push(`${name}:${scope}`); + } + } + } + return values; +}; + +export const StrategyActions = connect((props) => { + const { onChange } = props; + const availableActions = useAvailableActions(); + const compile = useCompile(); + const { t } = useTranslation(); + const field = useField(); + const scopes = toScopes(field.value); + return ( +
+
compile(value), + }, + { + dataIndex: 'onNewRecord', + title: t('Action type'), + render: (onNewRecord) => + onNewRecord ? ( + {t('Action on new records')} + ) : ( + {t('Action on existing records')} + ), + }, + { + dataIndex: 'enabled', + title: t('Allow'), + render: (enabled, action) => ( + { + if (enabled) { + delete scopes[action.name]; + } else { + scopes[action.name] = 'all'; + } + onChange(toFieldValue(scopes)); + }} + /> + ), + }, + { + dataIndex: 'scope', + title: t('Data scope'), + render: (scope, action) => + !action.onNewRecord && ( + { + return ( + + {label} + + + ); + }} + /> + + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Card.Designer.tsx b/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Card.Designer.tsx index 2f1c30035..0ff5e8b56 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Card.Designer.tsx +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Card.Designer.tsx @@ -114,7 +114,7 @@ export const kanbanCardInitializers: SchemaInitializer = new SchemaInitializer({ divider: true, title: '{{t("Display association fields")}}', name: 'displayAssociationFields', - checkChildrenLength: true, + hideIfNoChildren: true, useChildren() { const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Card.tsx b/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Card.tsx index 3f58489c6..23274e4a8 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Card.tsx +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Card.tsx @@ -1,16 +1,23 @@ import { css } from '@emotion/css'; import { FormLayout } from '@formily/antd-v5'; import { observer, RecursionField, useFieldSchema } from '@formily/react'; +import { + ActionContextProvider, + DndContext, + RecordProvider, + SchemaComponentOptions, + useCollectionParentRecordData, +} from '@nocobase/client'; import { Card } from 'antd'; import cls from 'classnames'; import React, { useContext, useState } from 'react'; -import { ActionContextProvider, DndContext, RecordProvider, SchemaComponentOptions } from '@nocobase/client'; import { KanbanCardContext } from './context'; export const KanbanCard: any = observer( (props: any) => { const { setDisableCardDrag, cardViewerSchema, card, cardField, columnIndex, cardIndex } = useContext(KanbanCardContext); + const parentRecordData = useCollectionParentRecordData(); const fieldSchema = useFieldSchema(); const [visible, setVisible] = useState(false); return ( @@ -93,7 +100,7 @@ export const KanbanCard: any = observer( setVisible, }} > - + { - const { name, title } = useCollection(); + const { name, title } = useCollection_deprecated(); const template = useSchemaTemplate(); return ( diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Settings.tsx b/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Settings.tsx index 20ff847ea..bd654df14 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Settings.tsx +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Settings.tsx @@ -2,7 +2,7 @@ import { useField, useFieldSchema } from '@formily/react'; import { useFormBlockContext, SchemaSettingsDataScope, - useCollection, + useCollection_deprecated, useDesignable, SchemaSettings, FixedBlockDesignerItem, @@ -12,7 +12,7 @@ import { } from '@nocobase/client'; import { useKanbanBlockContext } from './KanbanBlockProvider'; export const kanbanSettings = new SchemaSettings({ - name: 'KanbanSettings', + name: 'blockSettings:kanban', items: [ { name: 'title', @@ -22,7 +22,7 @@ export const kanbanSettings = new SchemaSettings({ name: 'dataScope', Component: SchemaSettingsDataScope, useComponentProps() { - const { name } = useCollection(); + const { name } = useCollection_deprecated(); const fieldSchema = useFieldSchema(); const { form } = useFormBlockContext(); const field = useField(); @@ -57,7 +57,7 @@ export const kanbanSettings = new SchemaSettings({ name: 'template', Component: SchemaSettingsTemplate, useComponentProps() { - const { name } = useCollection(); + const { name } = useCollection_deprecated(); return { componentName: 'Kanban', collectionName: name, diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.tsx b/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.tsx index fdcd2a8a5..e40fd5aaf 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.tsx +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.tsx @@ -1,8 +1,14 @@ import { ArrayField } from '@formily/core'; import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react'; +import { + RecordProvider, + SchemaComponentOptions, + useCreateActionProps as useCAP, + useCollectionParentRecordData, + useProps, +} from '@nocobase/client'; import { Spin, Tag } from 'antd'; import React, { useContext, useMemo, useState } from 'react'; -import { SchemaComponentOptions, RecordProvider, useCreateActionProps as useCAP, useProps } from '@nocobase/client'; import { Board } from './board'; import { KanbanCardContext, KanbanColumnContext } from './context'; import { useStyles } from './style'; @@ -19,7 +25,7 @@ const useCreateActionProps = () => { }; }; -export const toColumns = (groupField: any, dataSource: Array = []) => { +export const toColumns = (groupField: any, dataSource: Array = [], primaryKey) => { const columns = { __unknown__: { id: '__unknown__', @@ -39,7 +45,7 @@ export const toColumns = (groupField: any, dataSource: Array = []) => { dataSource.forEach((ds) => { const value = ds[groupField.name]; if (value && columns[value]) { - columns[value].cards.push(ds); + columns[value].cards.push({ ...ds, id: ds[primaryKey] }); } else { columns.__unknown__.cards.push(ds); } @@ -54,6 +60,7 @@ export const Kanban: any = observer( (props: any) => { const { styles } = useStyles(); const { groupField, onCardDragEnd, dataSource, setDataSource, ...restProps } = useProps(props); + const parentRecordData = useCollectionParentRecordData(); const field = useField(); const fieldSchema = useFieldSchema(); const [disableCardDrag, setDisableCardDrag] = useState(false); @@ -105,7 +112,7 @@ export const Kanban: any = observer( const cardIndex = column?.cards?.indexOf(card); return ( schemas.card && ( - + { + const form = useForm(); + const { t } = useTranslation(); + + return ( + + + + { + form.setValuesIn('dragSortBy', null); + }, + }, + 'x-decorator': 'FormItem', + }, + dragSortBy: { + title: t('Sorting field', { ns: NAMESPACE }), + required: true, + description: t( + 'Used for sorting kanban cards, only sorting fields corresponding to grouping fields can be selected', + { ns: NAMESPACE }, + ), + 'x-component': CreateAndSelectSort, + 'x-component-props': { + objectValue: true, + fieldNames: { label: 'label', value: 'value' }, + sortFields, + }, + 'x-decorator': 'FormItem', + 'x-reactions': [ + (field) => { + field.dataSource = sortFields.map((v) => { + return { + ...v, + disabled: v.scopeKey !== field.form.values?.groupField?.value, + }; + }); + field.groupField = field.form.values?.groupField; + field.setComponentProps({ + dataSource: item.dataSource, + collectionName: item.name, + collectionFields, + sortFields: sortFields, + }); + }, + { + dependencies: ['.groupField'], + fulfill: { + schema: { + 'x-component-props': '{{$form.values}}', + }, + }, + }, + ], + }, + }, + }} + /> + + + + ); +}; export const KanbanBlockInitializer = () => { const { insert } = useSchemaInitializer(); const { t } = useTranslation(); - const { getCollectionFields } = useCollectionManager(); - const options = useContext(SchemaOptionsContext); - const api = useAPIClient(); + const { getCollectionFields } = useCollectionManager_deprecated(); const { theme } = useGlobalTheme(); const itemConfig = useSchemaInitializerItem(); - + const options = useContext(SchemaOptionsContext); + const api = useAPIClient(); return ( } onCreateBlockSchema={async ({ item }) => { - const collectionFields = getCollectionFields(item.name); + const { data } = await api.resource('collections.fields', item.name).list({ paginate: false }); + const targetFields = getCollectionFields(item.name, item.dataSource); + const collectionFields = item.dataSource === 'main' ? data.data : targetFields; const fields = collectionFields ?.filter((field) => ['select', 'radioGroup'].includes(field.interface)) ?.map((field) => { @@ -45,56 +129,41 @@ export const KanbanBlockInitializer = () => { }, }; }); + const sortFields = collectionFields + ?.filter((field) => ['sort'].includes(field.interface)) + ?.map((field) => { + return { + label: field?.uiSchema?.title, + value: field.name, + scopeKey: field.scopeKey, + uiSchema: { + ...field.uiSchema, + name: field.name, + }, + }; + }); const values = await FormDialog( t('Create kanban block'), - () => { - return ( - - - - - - ); - }, + , theme, ).open({ initialValues: {}, }); - const sortName = `${values.groupField.value}_sort`; - const exists = collectionFields?.find((field) => field.name === sortName); - if (!exists) { - await api.resource('collections.fields', item.name).create({ - values: { - type: 'sort', - name: sortName, - hidden: true, - scopeKey: values.groupField.value, - }, - }); - } insert( createKanbanBlockSchema({ + sortField: values.dragSortBy, groupField: values.groupField.value, collection: item.name, + dataSource: item.dataSource, params: { - sort: [sortName], + sort: [values.dragSortBy], paginate: false, }, }), diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockProvider.tsx b/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockProvider.tsx index 0b717e5fd..806baaa70 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockProvider.tsx +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockProvider.tsx @@ -5,8 +5,8 @@ import uniq from 'lodash/uniq'; import React, { createContext, useContext, useEffect, useState } from 'react'; import { useACLRoleContext, - useCollection, - useCollectionManager, + useCollection_deprecated, + useCollectionManager_deprecated, FixedBlockWrapper, BlockProvider, useBlockRequestContext, @@ -16,7 +16,7 @@ import { toColumns } from './Kanban'; export const KanbanBlockContext = createContext({}); const useGroupField = (props) => { - const { getField } = useCollection(); + const { getField } = useCollection_deprecated(); const { groupField } = props; if (typeof groupField === 'string') { return getField(groupField); @@ -49,6 +49,7 @@ const InternalKanbanBlockProvider = (props) => { resource, groupField, fixedBlock: field?.decoratorProps?.fixedBlock, + sortField: props?.sortField, }} > {props.children} @@ -75,7 +76,7 @@ const recursiveProperties = (schema: Schema, component = 'CollectionField', asso }; const useAssociationNames = (collection) => { - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(collection.dataSource); const collectionFields = getCollectionFields(collection); const associationFields = new Set(); for (const collectionField of collectionFields) { @@ -136,10 +137,11 @@ export const useKanbanBlockProps = () => { const field = useField(); const ctx = useKanbanBlockContext(); const [dataSource, setDataSource] = useState([]); + const primaryKey = useCollection_deprecated().getPrimaryKey(); useEffect(() => { if (!ctx?.service?.loading) { - field.value = toColumns(ctx.groupField, ctx?.service?.data?.data); - setDataSource(toColumns(ctx.groupField, ctx?.service?.data?.data)); + field.value = toColumns(ctx.groupField, ctx?.service?.data?.data, primaryKey); + setDataSource(toColumns(ctx.groupField, ctx?.service?.data?.data, primaryKey)); } // field.loading = ctx?.service?.loading; }, [ctx?.service?.loading]); @@ -155,7 +157,7 @@ export const useKanbanBlockProps = () => { const targetCard = destinationColumn?.cards?.[toPosition]; const values = { sourceId: sourceCard.id, - sortField: `${groupField.name}_sort`, + sortField: ctx?.sortField || `${groupField.name}_sort`, }; if (targetCard) { values['targetId'] = targetCard.id; diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/__e2e__/schemaInitailizer.test.ts b/packages/plugins/@nocobase/plugin-kanban/src/client/__e2e__/schemaInitailizer.test.ts index cfca6862e..4d368857f 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/__e2e__/schemaInitailizer.test.ts +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/__e2e__/schemaInitailizer.test.ts @@ -13,7 +13,9 @@ test.describe('blockInitializers should add kanban block', () => { await page.getByRole('menuitem', { name: 'General' }).click(); await page.getByLabel('block-item-Select-Grouping field').locator('.ant-select').click(); await page.getByRole('option', { name: 'Single select' }).click(); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByLabel('block-item-Sorting field').getByTestId('select-single').click(); + await page.getByRole('option', { name: 'singleSelect_sort (Group' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); await expect(page.getByLabel('block-item-CardItem-general-kanban')).toBeVisible(); }); }); @@ -62,9 +64,7 @@ test.describe('configure fields', () => { //修改标题字段 await page.getByLabel('schema-initializer-ActionBar-KanbanActionInitializers-general').click(); await page.getByLabel('block-item-CollectionField-general-kanban-general.manyToOne').hover(); - await page - .getByLabel('designer-schema-settings-CollectionField-FormItem.Designer-general-general.manyToOne') - .hover(); + await page.getByTestId('card-1').getByLabel('designer-schema-settings-').hover(); await page.getByText('Title field').click(); await page.getByRole('option', { name: 'Username' }).click(); //显示新的标题字段符合预期 @@ -108,6 +108,9 @@ test.describe('configure actions', () => { await page.getByRole('menuitem', { name: 'ID', exact: true }).click(); await page.getByRole('menuitem', { name: 'Created at' }).getByRole('switch').click(); await page.getByRole('menuitem', { name: 'Single Select' }).getByRole('switch').click(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('block-item-Kanban.Card-general-kanban')).toBeVisible(); }); }); diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/__e2e__/schemaSettings.test.ts b/packages/plugins/@nocobase/plugin-kanban/src/client/__e2e__/schemaSettings.test.ts index 18cebe83e..97f332502 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/__e2e__/schemaSettings.test.ts +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/__e2e__/schemaSettings.test.ts @@ -3,28 +3,6 @@ import { oneEmptyKanbanBlock } from './utils'; //看板的区块参数配置 test.describe('configure setting', () => { - test('fix block ', async ({ page, mockPage, mockRecords }) => { - const nocoPage = await mockPage(oneEmptyKanbanBlock).waitForInit(); - await mockRecords('general', 100); - await nocoPage.goto(); - await expect(page.getByLabel('block-item-CardItem-general-kanban')).toBeVisible(); - //固定区块 - await page.getByLabel('block-item-CardItem-general-kanban').hover(); - await page.getByLabel('designer-schema-settings-CardItem-Kanban.Designer-general').hover(); - await page.getByRole('menuitem', { name: 'Fix block' }).click(); - // 等待重渲染完成 - await page.waitForTimeout(1000); - const kanbanBoard = page.getByLabel('block-item-CardItem-general-kanban'); - const kanbanBoardHeight = await kanbanBoard.evaluate((element) => { - const computedStyle = window.getComputedStyle(element); - return parseInt(computedStyle.height, 10); - }); - const windowHeight = await page.evaluate(() => { - return window.innerHeight; - }); - const expectedHeight = windowHeight - 147; - expect(kanbanBoardHeight).toBe(expectedHeight); - }); test('set the data scope', async ({ page, mockPage, mockRecords }) => { const nocoPage = await mockPage(oneEmptyKanbanBlock).waitForInit(); await mockRecords('general', 10); diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/index.tsx b/packages/plugins/@nocobase/plugin-kanban/src/client/index.tsx index 07bf896fc..c4191a629 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/index.tsx @@ -1,11 +1,5 @@ -import React, { useContext } from 'react'; -import { - Plugin, - Action, - CurrentAppInfoProvider, - SchemaComponentOptions, - CollectionManagerContext, -} from '@nocobase/client'; +import React from 'react'; +import { Plugin, Action, CurrentAppInfoProvider, SchemaComponentOptions } from '@nocobase/client'; import { Kanban } from './Kanban'; import { KanbanCard } from './Kanban.Card'; import { KanbanCardDesigner, kanbanCardInitializers } from './Kanban.Card.Designer'; @@ -25,14 +19,13 @@ Kanban.Designer = KanbanDesigner; const KanbanV2 = Kanban; const KanbanPluginProvider = React.memo((props) => { - const ctx = useContext(CollectionManagerContext); return ( - {props.children} + {props.children} ); diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/utils.ts b/packages/plugins/@nocobase/plugin-kanban/src/client/utils.ts index 1d435d05a..9962a2484 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/utils.ts +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/utils.ts @@ -2,7 +2,7 @@ import { ISchema } from '@formily/react'; import { uid } from '@formily/shared'; export const createKanbanBlockSchema = (options) => { - const { collection, resource, groupField, ...others } = options; + const { collection, resource, groupField, sortField, ...others } = options; const schema: ISchema = { type: 'void', 'x-acl-action': `${resource || collection}:list`, @@ -12,6 +12,7 @@ export const createKanbanBlockSchema = (options) => { resource: resource || collection, action: 'list', groupField, + sortField, params: { paginate: false, }, diff --git a/packages/plugins/@nocobase/plugin-kanban/src/locale/en-US.ts b/packages/plugins/@nocobase/plugin-kanban/src/locale/en-US.ts deleted file mode 100644 index 2d905d194..000000000 --- a/packages/plugins/@nocobase/plugin-kanban/src/locale/en-US.ts +++ /dev/null @@ -1,3 +0,0 @@ -const locale = {}; - -export default locale; diff --git a/packages/plugins/@nocobase/plugin-kanban/src/locale/fr-FR.ts b/packages/plugins/@nocobase/plugin-kanban/src/locale/fr-FR.ts deleted file mode 100644 index 2d905d194..000000000 --- a/packages/plugins/@nocobase/plugin-kanban/src/locale/fr-FR.ts +++ /dev/null @@ -1,3 +0,0 @@ -const locale = {}; - -export default locale; diff --git a/packages/plugins/@nocobase/plugin-kanban/src/locale/ko_KR.ts b/packages/plugins/@nocobase/plugin-kanban/src/locale/ko_KR.ts deleted file mode 100644 index 43c8832ea..000000000 --- a/packages/plugins/@nocobase/plugin-kanban/src/locale/ko_KR.ts +++ /dev/null @@ -1,54 +0,0 @@ -const locale = { - "Map": "지도", - "Map-based geometry": "지도 기반 지오메트리", - "Map type": "지도 유형", - "Point": "포인트", - "Line": "라인", - "Circle": "서클", - "Polygon": "폴리곤", - "Access key": "액세스 키", - "securityJsCode or serviceHost": "securityJsCode 또는 serviceHost", - "AMap": "AMap(高德地图)", - "Google Maps": "Google 지도", - "Clear": "지우기", - "Click to select the starting point and double-click to end the drawing": "시작점을 선택하려면 클릭하고, 그리기를 종료하려면 더블 클릭하세요", - "Clear the canvas": "캔버스 지우기", - "Are you sure to clear the canvas?": "캔버스를 지우시겠습니까?", - "Confirm": "확인", - "Cancel": "취소", - "Enter keywords to search": "검색할 키워드를 입력하세요 (반드시 시/도를 포함해야 함)", - "The AccessKey is incorrect, please check it": "액세스 키가 잘못되었습니다. 확인해주세요", - "Please configure the AMap securityCode or serviceHost correctly": "AMap securityCode 또는 serviceHost를 올바르게 구성하세요", - "Map Manager": "지도 관리자", - "Configuration": "구성", - "Saved successfully": "성공적으로 저장됨", - "Saved failed": "저장 실패", - "Edit": "편집", - "Save": "저장", - "Please configure the AccessKey and SecurityJsCode first": "먼저 AccessKey 및 SecurityJsCode를 구성하세요", - "Go to the configuration page": "구성 페이지로 이동", - "Zoom": "확대/축소", - "Set default zoom level": "기본 확대/축소 수준 설정", - "The default zoom level of the map": "지도의 기본 확대/축소 수준", - "Edit field title": "필드 제목 편집", - "Field title": "필드 제목", - "Edit tooltip": "툴팁 편집", - "Delete field": "필드 삭제", - "Required": "필수", - "Pattern": "패턴", - "Operator": "연산자", - "Editable": "편집 가능", - "Readonly": "읽기 전용 (편집 금지)", - "Easy-reading": "읽기 전용 (읽기 모드)", - "Edit description": "설명 편집", - "Map field": "지도 필드", - "Marker field": "마커 필드", - "Load google maps failed, Please check the Api key and refresh the page": - "Google 지도를 로드하지 못했습니다. API 키를 확인하고 페이지를 새로 고침하세요", - "Create map block": "지도 블록 생성", - "Start point": "시작 지점", - "End point": "끝 지점", - "Concatenation order field": "연결 순서 필드" -}; - -export default locale; diff --git a/packages/plugins/@nocobase/plugin-kanban/src/locale/pt-BR.ts b/packages/plugins/@nocobase/plugin-kanban/src/locale/pt-BR.ts deleted file mode 100644 index 1f7d9da07..000000000 --- a/packages/plugins/@nocobase/plugin-kanban/src/locale/pt-BR.ts +++ /dev/null @@ -1,51 +0,0 @@ -const locale = { - 'Map-based geometry': 'Geometria baseada em mapa', - 'Map type': 'Tipo de mapa', - Point: 'Ponto', - Line: 'Linha', - Circle: 'Círculo', - Polygon: 'Polígono', - 'Access key': 'Chave de acesso', - 'securityJsCode or serviceHost': 'securityJsCode ou serviceHost', - AMap: 'AMap', - 'Google Maps': 'Google Maps', - Clear: 'Limpar', - 'Click to select the starting point and double-click to end the drawing': - 'Clique para selecionar o ponto de partida e dê um duplo clique para terminar a criação', - 'Clear the canvas': 'Limpar a tela', - 'Are you sure to clear the canvas?': 'Tem certeza de que deseja limpar a tela?', - Confirm: 'Confirmar', - Cancel: 'Cancelar', - 'Enter keywords to search': 'Digite palavras-chave para buscar', - 'The AccessKey is incorrect, please check it': 'A chave de acesso está incorreta, por favor verifique', - 'Please configure the AMap securityCode or serviceHost correctly': - 'Por favor, configure o securityCode ou serviceHost do AMap corretamente', - 'Map Manager': 'Gerenciador de Mapa', - Configuration: 'Configuração', - 'Saved successfully': 'Salvo com sucesso', - 'Saved failed': 'Falha ao salvar', - Edit: 'Editar', - Save: 'Salvar', - 'Please configure the AccessKey and SecurityJsCode first': - 'Por favor, configure a AccessKey e o SecurityJsCode primeiro', - 'Go to the configuration page': 'Ir para a página de configuração', - Zoom: 'Zoom', - 'Set default zoom level': 'Definir nível de zoom padrão', - 'The default zoom level of the map': 'O nível de zoom padrão do mapa', - // Designer - 'Edit field title': 'Editar título do campo', - 'Field title': 'Título do campo', - 'Edit tooltip': 'Editar dica de ferramenta', - 'Delete field': 'Excluir campo', - Required: 'Obrigatório', - Pattern: 'Padrão', - Operator: 'Operador', - Editable: 'Editável', - Readonly: 'Somente leitura', - 'Easy-reading': 'Apenas leitura (Modo de Leitura)', - 'Edit description': 'Editar descrição', - 'Map field': 'Campo de mapa', - 'Marker field': 'Campo de marcador', -}; - -export default locale; diff --git a/packages/plugins/@nocobase/plugin-kanban/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-kanban/src/locale/zh-CN.json new file mode 100644 index 000000000..d3e67103f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-kanban/src/locale/zh-CN.json @@ -0,0 +1,8 @@ +{ + "Used for sorting kanban cards, only sorting fields corresponding to grouping fields can be selected": "用于看板卡片的排序,只能选择与分组字段相对应的排序字段", + "Global sorting": "全局排序", + "Create sort field": "创建排序字段", + "Convert the following integer fields to sorting fields": "将以下整数字段转为排序字段", + "Sorting field":"排序字段", + "Grouped sorting based on":"基于分组字段" +} diff --git a/packages/plugins/@nocobase/plugin-kanban/src/locale/zh-CN.ts b/packages/plugins/@nocobase/plugin-kanban/src/locale/zh-CN.ts deleted file mode 100644 index 8c6d1f960..000000000 --- a/packages/plugins/@nocobase/plugin-kanban/src/locale/zh-CN.ts +++ /dev/null @@ -1,55 +0,0 @@ -const locale = { - Map: '地图', - 'Map-based geometry': '基于地图的几何图形', - 'Map type': '地图类型', - Point: '点', - Line: '线', - Circle: '圆', - Polygon: '多边形', - 'Access key': '访问密钥', - 'securityJsCode or serviceHost': 'securityJsCode 或 serviceHost', - AMap: '高德地图', - 'Google Maps': '谷歌地图', - Clear: '清空', - 'Click to select the starting point and double-click to end the drawing': '点击选择起点,双击结束绘制', - 'Clear the canvas': '清空画布', - 'Are you sure to clear the canvas?': '您确定要清空画布吗?', - Confirm: '确定', - Cancel: '取消', - 'Enter keywords to search': '输入地方名关键字搜索(必须包含省/市)', - 'The AccessKey is incorrect, please check it': '访问密钥不正确,请检查', - 'Please configure the AMap securityCode or serviceHost correctly': '请正确配置高德地图 securityCode 或 serviceHost', - 'Map Manager': '地图管理', - Configuration: '配置', - 'Saved successfully': '保存成功', - 'Saved failed': '保存失败', - Edit: '编辑', - Save: '保存', - 'Please configure the AccessKey and SecurityJsCode first': '请先配置 AccessKey 和 SecurityJsCode', - 'Go to the configuration page': '前往配置页面', - Zoom: '缩放', - 'Set default zoom level': '设置默认缩放级别', - 'The default zoom level of the map': '地图默认缩放级别', - // Designer - 'Edit field title': '编辑字段标题', - 'Field title': '字段标题', - 'Edit tooltip': '编辑提示信息', - 'Delete field': '删除字段', - Required: '必填', - Pattern: '模式', - Operator: '运算符', - Editable: '可编辑', - Readonly: '只读(禁止编辑)', - 'Easy-reading': '只读(阅读模式)', - 'Edit description': '编辑描述', - 'Map field': '地图字段', - 'Marker field': '标记字段', - 'Load google maps failed, Please check the Api key and refresh the page': - '加载谷歌地图失败,请检查 Api key 并刷新页面', - 'Create map block': '创建地图区块', - 'Start point': '起点', - 'End point': '终点', - 'Concatenation order field': '连接顺序字段', -}; - -export default locale; diff --git a/packages/plugins/@nocobase/plugin-localization-management/src/client/schemas/localization.tsx b/packages/plugins/@nocobase/plugin-localization-management/src/client/schemas/localization.tsx index e8d4252cb..eb7d1840e 100644 --- a/packages/plugins/@nocobase/plugin-localization-management/src/client/schemas/localization.tsx +++ b/packages/plugins/@nocobase/plugin-localization-management/src/client/schemas/localization.tsx @@ -62,7 +62,7 @@ export const localizationSchema: ISchema = { }, }, }, - 'x-component': 'CollectionProvider', + 'x-component': 'CollectionProvider_deprecated', 'x-component-props': { collection, }, diff --git a/packages/plugins/@nocobase/plugin-localization-management/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-localization-management/src/server/plugin.ts index 333718159..8e4adafe7 100644 --- a/packages/plugins/@nocobase/plugin-localization-management/src/server/plugin.ts +++ b/packages/plugins/@nocobase/plugin-localization-management/src/server/plugin.ts @@ -72,7 +72,7 @@ export class LocalizationManagementPlugin extends Plugin { actions: ['localization:*', 'localizationTexts:*', 'localizationTranslations:*'], }); - this.db.on('afterSave', async (instance: Model) => { + this.db.on('afterSave', async (instance: Model, options) => { const module = `resources.${NAMESPACE_COLLECTIONS}`; const model = instance.constructor as typeof Model; const collection = model.collection; @@ -90,7 +90,7 @@ export class LocalizationManagementPlugin extends Plugin { textsFromDB.forEach((text) => { texts.push({ text, module }); }); - texts = await this.resources.filterExists(texts); + texts = await this.resources.filterExists(texts, options?.transaction); this.db .getModel('localizationTexts') .bulkCreate( @@ -98,8 +98,11 @@ export class LocalizationManagementPlugin extends Plugin { module, text, })), + { + transaction: options?.transaction, + }, ) - .then((newTexts) => this.resources.updateCacheTexts(newTexts)) + .then((newTexts) => this.resources.updateCacheTexts(newTexts, options?.transaction)) .catch((err) => {}); }); diff --git a/packages/plugins/@nocobase/plugin-localization-management/src/server/resources.ts b/packages/plugins/@nocobase/plugin-localization-management/src/server/resources.ts index 12b52c036..7304277e6 100644 --- a/packages/plugins/@nocobase/plugin-localization-management/src/server/resources.ts +++ b/packages/plugins/@nocobase/plugin-localization-management/src/server/resources.ts @@ -1,5 +1,5 @@ import { Cache } from '@nocobase/cache'; -import { Database } from '@nocobase/database'; +import { Database, Transaction } from '@nocobase/database'; export default class Resources { cache: Cache; @@ -10,11 +10,12 @@ export default class Resources { this.db = db; } - async getTexts() { + async getTexts(transaction?: Transaction) { return await this.cache.wrap(`texts`, async () => { return await this.db.getRepository('localizationTexts').find({ fields: ['id', 'module', 'text'], raw: true, + transaction, }); }); } @@ -50,20 +51,20 @@ export default class Resources { return resources; } - async filterExists(texts: { text: string; module: string }[]) { - const existTexts = await this.getTexts(); + async filterExists(texts: { text: string; module: string }[], transaction?: Transaction) { + const existTexts = await this.getTexts(transaction); return texts.filter((text) => { return !existTexts.find((item: any) => item.text === text.text && item.module === text.module); }); } - async updateCacheTexts(texts: any[]) { + async updateCacheTexts(texts: any[], transaction?: Transaction) { const newTexts = texts.map((text) => ({ id: text.id, module: text.module, text: text.text, })); - const existTexts = await this.getTexts(); + const existTexts = await this.getTexts(transaction); await this.cache.set(`texts`, [...existTexts, ...newTexts]); } diff --git a/packages/plugins/@nocobase/plugin-map/src/client/block/MapActionInitializers.tsx b/packages/plugins/@nocobase/plugin-map/src/client/block/MapActionInitializers.tsx index 8c8517d65..18a9433b6 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/block/MapActionInitializers.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/block/MapActionInitializers.tsx @@ -1,4 +1,4 @@ -import { SchemaInitializer, useCollection } from '@nocobase/client'; +import { SchemaInitializer, useCollection_deprecated } from '@nocobase/client'; // 表格操作配置 export const mapActionInitializers: SchemaInitializer = new SchemaInitializer({ @@ -34,7 +34,7 @@ export const mapActionInitializers: SchemaInitializer = new SchemaInitializer({ }, }, useVisible() { - const collection = useCollection(); + const collection = useCollection_deprecated(); return collection.template !== 'sql'; }, }, @@ -52,7 +52,7 @@ export const mapActionInitializers: SchemaInitializer = new SchemaInitializer({ name: 'divider', type: 'divider', useVisible() { - const collection = useCollection(); + const collection = useCollection_deprecated(); return collection.template !== 'sql'; }, }, @@ -62,7 +62,7 @@ export const mapActionInitializers: SchemaInitializer = new SchemaInitializer({ name: 'customize', children: [], useVisible() { - const collection = useCollection(); + const collection = useCollection_deprecated(); return collection.template !== 'sql'; }, }, diff --git a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.Settings.tsx b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.Settings.tsx new file mode 100644 index 000000000..0060527ce --- /dev/null +++ b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.Settings.tsx @@ -0,0 +1,249 @@ +import { ISchema, useField, useFieldSchema } from '@formily/react'; +import { + FilterBlockType, + FixedBlockDesignerItem, + SchemaSettingsBlockTitleItem, + SchemaSettingsCascaderItem, + SchemaSettingsConnectDataBlocks, + SchemaSettingsDataScope, + SchemaSettingsDefaultSortingRules, + SchemaSettingsModalItem, + SchemaSettingsSelectItem, + SchemaSettingsTemplate, + mergeFilter, + useCollection, + useCollectionManager, + useDesignable, + useFormBlockContext, + SchemaSettings, + useCollectionManager_deprecated, +} from '@nocobase/client'; +import lodash from 'lodash'; +import { useMapTranslation } from '../locale'; +import { useMapBlockContext } from './MapBlockProvider'; +import { findNestedOption } from './utils'; + +export const mapBlockSettings = new SchemaSettings({ + name: 'blockSettings:map', + items: [ + { + name: 'title', + Component: SchemaSettingsBlockTitleItem, + }, + { + name: 'fixBlock', + Component: FixedBlockDesignerItem, + }, + { + name: 'mapField', + Component: SchemaSettingsCascaderItem, + useComponentProps() { + const { getCollectionFieldsOptions } = useCollectionManager_deprecated(); + const { t } = useMapTranslation(); + const fieldSchema = useFieldSchema(); + const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {}; + const field = useField(); + const { dn } = useDesignable(); + const { service } = useMapBlockContext(); + const { name } = useCollection(); + const mapFieldOptions = getCollectionFieldsOptions(name, ['point', 'lineString', 'polygon'], { + association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'], + }); + return { + title: t('Map field'), + value: fieldNames.field, + options: mapFieldOptions, + allowClear: false, + onChange: (v) => { + const fieldNames = field.decoratorProps.fieldNames || {}; + fieldNames['field'] = v; + field.decoratorProps.fieldNames = fieldNames; + fieldSchema['x-decorator-props']['fieldNames'] = fieldNames; + service.refresh(); + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': field.decoratorProps, + }, + }); + dn.refresh(); + }, + }; + }, + }, + { + name: 'markerField', + Component: SchemaSettingsSelectItem, + useComponentProps() { + const { t } = useMapTranslation(); + const fieldSchema = useFieldSchema(); + const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {}; + const { service } = useMapBlockContext(); + const field = useField(); + const { dn } = useDesignable(); + const { getCollectionFieldsOptions } = useCollectionManager_deprecated(); + const { name } = useCollection(); + const markerFieldOptions = getCollectionFieldsOptions(name, 'string'); + return { + title: t('Marker field'), + value: fieldNames.marker, + options: markerFieldOptions, + onChange: (v) => { + const fieldNames = field.decoratorProps.fieldNames || {}; + fieldNames['marker'] = v; + field.decoratorProps.fieldNames = fieldNames; + fieldSchema['x-decorator-props']['fieldNames'] = fieldNames; + service.refresh(); + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': field.decoratorProps, + }, + }); + dn.refresh(); + }, + }; + }, + useVisible() { + const fieldSchema = useFieldSchema(); + const { getCollectionFieldsOptions } = useCollectionManager_deprecated(); + const { name } = useCollection(); + const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {}; + const mapFieldOptions = getCollectionFieldsOptions(name, ['point', 'lineString', 'polygon'], { + association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'], + }); + const isPointField = findNestedOption(fieldNames.field, mapFieldOptions)?.type === 'point'; + return isPointField; + }, + }, + { + name: 'setDefaultSortingRules', + Component: SchemaSettingsDefaultSortingRules, + useComponentProps() { + const { t } = useMapTranslation(); + return { + path: 'x-component-props.lineSort', + title: t('Concatenation order field'), + }; + }, + }, + { + name: 'defaultZoomLevel', + Component: SchemaSettingsModalItem, + useComponentProps() { + const { t } = useMapTranslation(); + const fieldSchema = useFieldSchema(); + const field = useField(); + const { dn } = useDesignable(); + const defaultZoom = fieldSchema?.['x-component-props']?.['zoom'] || 13; + return { + title: t('The default zoom level of the map'), + schema: { + type: 'object', + title: t('Set default zoom level'), + properties: { + zoom: { + title: t('Zoom'), + default: defaultZoom, + 'x-component': 'InputNumber', + 'x-decorator': 'FormItem', + 'x-component-props': { + precision: 0, + }, + }, + }, + } as ISchema, + onSubmit: ({ zoom }) => { + lodash.set(fieldSchema, 'x-component-props.zoom', zoom); + Object.assign(field.componentProps, fieldSchema['x-component-props']); + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + 'x-component-props': field.componentProps, + }, + }); + dn.refresh(); + }, + }; + }, + }, + { + name: 'dataScope', + Component: SchemaSettingsDataScope, + useComponentProps() { + const { name } = useCollection(); + const fieldSchema = useFieldSchema(); + const { form } = useFormBlockContext(); + const field = useField(); + const { service } = useMapBlockContext(); + const { dn } = useDesignable(); + return { + collectionName: name, + defaultFilter: fieldSchema?.['x-decorator-props']?.params?.filter || {}, + form: form, + onSubmit: ({ filter }) => { + const params = field.decoratorProps.params || {}; + params.filter = filter; + field.decoratorProps.params = params; + fieldSchema['x-decorator-props']['params'] = params; + const filters = service.params?.[1]?.filters || {}; + service.run( + { ...service.params?.[0], filter: mergeFilter([...Object.values(filters), filter]), page: 1 }, + { filters }, + ); + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }, + }; + }, + }, + { + name: 'ConnectDataBlocks', + Component: SchemaSettingsConnectDataBlocks, + useComponentProps() { + const { t } = useMapTranslation(); + return { + type: FilterBlockType.TABLE, + emptyDescription: t('No blocks to connect'), + }; + }, + }, + + { + name: 'divider', + type: 'divider', + }, + { + name: 'template', + Component: SchemaSettingsTemplate, + useComponentProps() { + const { name } = useCollection(); + const fieldSchema = useFieldSchema(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + return { + componentName: 'Map', + collectionName: name, + resourceName: defaultResource, + }; + }, + }, + { + name: 'divider2', + type: 'divider', + }, + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }, + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.tsx b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.tsx index 4aecd8012..b7da6116d 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.tsx @@ -1,11 +1,11 @@ -import { useCollection, useCollectionManager, useProps } from '@nocobase/client'; +import { useCollection_deprecated, useCollectionManager_deprecated, useProps } from '@nocobase/client'; import React, { useMemo } from 'react'; import { MapBlockComponent } from '../components'; export const MapBlock = (props) => { const { fieldNames } = useProps(props); - const { getCollectionJoinField } = useCollectionManager(); - const { name } = useCollection(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const { name } = useCollection_deprecated(); const collectionField = useMemo(() => { return getCollectionJoinField([name, fieldNames?.field].flat().join('.')); }, [name, fieldNames?.field]); diff --git a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockDesigner.tsx b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockDesigner.tsx index 28b5f8b47..f1157b94e 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockDesigner.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockDesigner.tsx @@ -1,162 +1,14 @@ -import { ISchema, useField, useFieldSchema } from '@formily/react'; -import { - FilterBlockType, - FixedBlockDesignerItem, - GeneralSchemaDesigner, - SchemaSettingsBlockTitleItem, - SchemaSettingsCascaderItem, - SchemaSettingsConnectDataBlocks, - SchemaSettingsDataScope, - SchemaSettingsDefaultSortingRules, - SchemaSettingsDivider, - SchemaSettingsModalItem, - SchemaSettingsRemove, - SchemaSettingsSelectItem, - SchemaSettingsTemplate, - mergeFilter, - useCollection, - useCollectionManager, - useDesignable, - useFormBlockContext, - useSchemaTemplate, -} from '@nocobase/client'; -import lodash from 'lodash'; +import { GeneralSchemaDesigner, useCollection, useSchemaTemplate } from '@nocobase/client'; import React from 'react'; -import { useMapTranslation } from '../locale'; -import { useMapBlockContext } from './MapBlockProvider'; -import { findNestedOption } from './utils'; - export const MapBlockDesigner = () => { const { name, title } = useCollection(); - const field = useField(); - const fieldSchema = useFieldSchema(); - const { form } = useFormBlockContext(); - const { service } = useMapBlockContext(); - const { t } = useMapTranslation(); - const { dn } = useDesignable(); - const { getCollectionFieldsOptions } = useCollectionManager(); - const collection = useCollection(); - const defaultResource = fieldSchema?.['x-decorator-props']?.resource; - const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {}; - const defaultZoom = fieldSchema?.['x-component-props']?.['zoom'] || 13; - const template = useSchemaTemplate(); - const mapFieldOptions = getCollectionFieldsOptions(collection?.name, ['point', 'lineString', 'polygon'], { - association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'], - }); - const markerFieldOptions = getCollectionFieldsOptions(collection?.name, 'string'); - const isPointField = findNestedOption(fieldNames.field, mapFieldOptions)?.type === 'point'; - return ( - - - - { - const fieldNames = field.decoratorProps.fieldNames || {}; - fieldNames['field'] = v; - field.decoratorProps.fieldNames = fieldNames; - fieldSchema['x-decorator-props']['fieldNames'] = fieldNames; - service.refresh(); - dn.emit('patch', { - schema: { - ['x-uid']: fieldSchema['x-uid'], - 'x-decorator-props': field.decoratorProps, - }, - }); - dn.refresh(); - }} - /> - {isPointField ? ( - { - const fieldNames = field.decoratorProps.fieldNames || {}; - fieldNames['marker'] = v; - field.decoratorProps.fieldNames = fieldNames; - fieldSchema['x-decorator-props']['fieldNames'] = fieldNames; - service.refresh(); - dn.emit('patch', { - schema: { - ['x-uid']: fieldSchema['x-uid'], - 'x-decorator-props': field.decoratorProps, - }, - }); - dn.refresh(); - }} - /> - ) : null} - - { - lodash.set(fieldSchema, 'x-component-props.zoom', zoom); - Object.assign(field.componentProps, fieldSchema['x-component-props']); - dn.emit('patch', { - schema: { - 'x-uid': fieldSchema['x-uid'], - 'x-component-props': field.componentProps, - }, - }); - dn.refresh(); - }} - > - { - const params = field.decoratorProps.params || {}; - params.filter = filter; - field.decoratorProps.params = params; - fieldSchema['x-decorator-props']['params'] = params; - const filters = service.params?.[1]?.filters || {}; - service.run( - { ...service.params?.[0], filter: mergeFilter([...Object.values(filters), filter]), page: 1 }, - { filters }, - ); - dn.emit('patch', { - schema: { - ['x-uid']: fieldSchema['x-uid'], - 'x-decorator-props': fieldSchema['x-decorator-props'], - }, - }); - }} - /> - - - - - - + ); }; diff --git a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockInitializer.tsx index 7d3d5dea8..c019c6ec2 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockInitializer.tsx @@ -6,7 +6,7 @@ import { FormDialog, SchemaComponent, SchemaComponentOptions, - useCollectionManager, + useCollectionManager_deprecated, useGlobalTheme, useSchemaInitializer, useSchemaInitializerItem, @@ -19,7 +19,7 @@ export const MapBlockInitializer = () => { const itemConfig = useSchemaInitializerItem(); const { insert } = useSchemaInitializer(); const options = useContext(SchemaOptionsContext); - const { getCollectionFieldsOptions } = useCollectionManager(); + const { getCollectionFieldsOptions } = useCollectionManager_deprecated(); const { t } = useMapTranslation(); const { theme } = useGlobalTheme(); return ( @@ -29,8 +29,11 @@ export const MapBlockInitializer = () => { onCreateBlockSchema={async ({ item }) => { const mapFieldOptions = getCollectionFieldsOptions(item.name, ['point', 'lineString', 'polygon'], { association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'], + dataSource: item.dataSource, + }); + const markerFieldOptions = getCollectionFieldsOptions(item.name, 'string', { + dataSource: item.dataSource, }); - const markerFieldOptions = getCollectionFieldsOptions(item.name, 'string'); const values = await FormDialog( t('Create map block'), () => { @@ -83,9 +86,11 @@ export const MapBlockInitializer = () => { insert( createMapBlockSchema({ collection: item.name, + dataSource: item.dataSource, fieldNames: { ...values, }, + settings: 'blockSettings:map', }), ); }} diff --git a/packages/plugins/@nocobase/plugin-map/src/client/block/utils.ts b/packages/plugins/@nocobase/plugin-map/src/client/block/utils.ts index e1e65af80..58e4f67d4 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/block/utils.ts +++ b/packages/plugins/@nocobase/plugin-map/src/client/block/utils.ts @@ -2,7 +2,7 @@ import { ISchema } from '@formily/react'; import { uid } from '@formily/shared'; export const createMapBlockSchema = (options) => { - const { collection, resource, fieldNames, ...others } = options; + const { collection, resource, fieldNames, settings, ...others } = options; const schema: ISchema = { type: 'void', 'x-acl-action': `${resource || collection}:list`, @@ -17,7 +17,8 @@ export const createMapBlockSchema = (options) => { }, ...others, }, - 'x-designer': 'MapBlockDesigner', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': settings, 'x-component': 'CardItem', // 保存当前筛选区块所能过滤的数据区块 'x-filter-targets': [], diff --git a/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Block.tsx b/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Block.tsx index 9c1e0bdb5..a4f18f56d 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Block.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Block.tsx @@ -1,13 +1,14 @@ import { CheckOutlined, EnvironmentOutlined, ExpandOutlined } from '@ant-design/icons'; -import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; +import { RecursionField, useFieldSchema } from '@formily/react'; import { ActionContextProvider, RecordProvider, css, - useCollection, - useCollectionManager, + useCollectionManager_deprecated, + useCollection_deprecated, useCompile, useFilterAPI, + useCollectionParentRecordData, useProps, } from '@nocobase/client'; import { useMemoizedFn } from 'ahooks'; @@ -15,14 +16,14 @@ import { Button, Space } from 'antd'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { defaultImage, selectedImage } from '../../constants'; import { useMapTranslation } from '../../locale'; -import { AMapComponent, AMapForwardedRefProps } from './Map'; import { getSource } from '../../utils'; +import { AMapComponent, AMapForwardedRefProps } from './Map'; export const AMapBlock = (props) => { const { collectionField, fieldNames, dataSource, fixedBlock, zoom, setSelectedRecordKeys, lineSort } = useProps(props); - const { name, getPrimaryKey } = useCollection(); - const { getCollectionJoinField } = useCollectionManager(); + const { name, getPrimaryKey } = useCollection_deprecated(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); const primaryKey = getPrimaryKey(); const [isMapInitialization, setIsMapInitialization] = useState(false); const mapRef = useRef(); @@ -310,6 +311,7 @@ export const AMapBlock = (props) => { const MapBlockDrawer = (props) => { const { setVisible, record } = props; + const parentRecordData = useCollectionParentRecordData(); const fieldSchema = useFieldSchema(); const schema = useMemo( () => @@ -325,7 +327,7 @@ const MapBlockDrawer = (props) => { return ( schema && ( - + diff --git a/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Map.tsx b/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Map.tsx index 288bf45fb..0488e654d 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Map.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Map.tsx @@ -2,7 +2,7 @@ import AMapLoader from '@amap/amap-jsapi-loader'; import '@amap/amap-jsapi-types'; import { SyncOutlined } from '@ant-design/icons'; import { useFieldSchema } from '@formily/react'; -import { css, useApp, useCollection } from '@nocobase/client'; +import { css, useApp, useCollection_deprecated } from '@nocobase/client'; import { useMemoizedFn } from 'ahooks'; import { Alert, App, Button, Spin } from 'antd'; import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; @@ -91,7 +91,7 @@ export const AMapComponent = React.forwardRef(); const [needUpdateFlag, forceUpdate] = useState([]); const [errMessage, setErrMessage] = useState(''); - const { getField } = useCollection(); + const { getField } = useCollection_deprecated(); const type = useMemo(() => { if (props.type) return props.type; const collectionField = getField(fieldSchema?.name); diff --git a/packages/plugins/@nocobase/plugin-map/src/client/components/Designer.tsx b/packages/plugins/@nocobase/plugin-map/src/client/components/Designer.tsx index 7022ed1d1..5498f5f1c 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/components/Designer.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/components/Designer.tsx @@ -7,8 +7,8 @@ import { SchemaSettingsRemove, SchemaSettingsSelectItem, isPatternDisabled, - useCollection, - useCollectionManager, + useCollection_deprecated, + useCollectionManager_deprecated, useDesignable, useFormBlockContext, } from '@nocobase/client'; @@ -17,8 +17,8 @@ import React from 'react'; import { useMapTranslation } from '../locale'; const Designer = () => { - const { getCollectionJoinField } = useCollectionManager(); - const { getField } = useCollection(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); + const { getField } = useCollection_deprecated(); const { form } = useFormBlockContext(); const field = useField(); const fieldSchema = useFieldSchema(); diff --git a/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Block.tsx b/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Block.tsx index 0aec2ae36..4e43d6cf4 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Block.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Block.tsx @@ -4,10 +4,11 @@ import { ActionContextProvider, RecordProvider, css, - useCollection, - useCollectionManager, + useCollectionManager_deprecated, + useCollection_deprecated, useCompile, useFilterAPI, + useCollectionParentRecordData, useProps, } from '@nocobase/client'; import { useMemoizedFn } from 'ahooks'; @@ -15,9 +16,9 @@ import { Button, Space } from 'antd'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { defaultImage, selectedImage } from '../../constants'; import { useMapTranslation } from '../../locale'; +import { getSource } from '../../utils'; import { GoogleMapForwardedRefProps, GoogleMapsComponent, OverlayOptions } from './Map'; import { getIcon } from './utils'; -import { getSource } from '../../utils'; const OVERLAY_KEY = 'google-maps-overlay-id'; const OVERLAY_SELECtED = 'google-maps-overlay-selected'; @@ -39,7 +40,7 @@ const pointClass = css` export const GoogleMapsBlock = (props) => { const { collectionField, fieldNames, dataSource, fixedBlock, zoom, setSelectedRecordKeys, lineSort } = useProps(props); - const { getPrimaryKey } = useCollection(); + const { getPrimaryKey } = useCollection_deprecated(); const primaryKey = getPrimaryKey(); const { marker: markerName = 'id' } = fieldNames; const [isMapInitialization, setIsMapInitialization] = useState(false); @@ -55,7 +56,7 @@ export const GoogleMapsBlock = (props) => { const overlaysRef = useRef([]); selectingModeRef.current = selectingMode; - const { getCollectionJoinField } = useCollectionManager(); + const { getCollectionJoinField } = useCollectionManager_deprecated(); const setOverlayOptions = (overlay: google.maps.MVCObject, state?: boolean) => { const selected = typeof state !== 'undefined' ? !state : overlay.get(OVERLAY_SELECtED); @@ -359,6 +360,7 @@ export const GoogleMapsBlock = (props) => { const MapBlockDrawer = (props) => { const { setVisible, record } = props; + const parentRecordData = useCollectionParentRecordData(); const fieldSchema = useFieldSchema(); const schema: Schema = useMemo( () => @@ -374,7 +376,7 @@ const MapBlockDrawer = (props) => { return ( schema && ( - + diff --git a/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Map.tsx b/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Map.tsx index 76150d492..c7ef457c5 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Map.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Map.tsx @@ -1,7 +1,7 @@ import { SyncOutlined } from '@ant-design/icons'; import { useFieldSchema } from '@formily/react'; import { Loader } from '@googlemaps/js-api-loader'; -import { css, useAPIClient, useApp, useCollection } from '@nocobase/client'; +import { css, useAPIClient, useApp, useCollection_deprecated } from '@nocobase/client'; import { useMemoizedFn } from 'ahooks'; import { Alert, App, Button, Spin } from 'antd'; import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; @@ -80,7 +80,7 @@ export const GoogleMapsComponent = React.forwardRef(); const map = useRef(); diff --git a/packages/plugins/@nocobase/plugin-map/src/client/components/ReadPretty.tsx b/packages/plugins/@nocobase/plugin-map/src/client/components/ReadPretty.tsx index ee1aa9ef4..f6aa236ba 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/components/ReadPretty.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/components/ReadPretty.tsx @@ -1,12 +1,12 @@ import { useFieldSchema, useForm } from '@formily/react'; -import { EllipsisWithTooltip, useCollection, useFieldTitle } from '@nocobase/client'; +import { EllipsisWithTooltip, useCollection_deprecated, useFieldTitle } from '@nocobase/client'; import React from 'react'; import { MapComponent } from './MapComponent'; const ReadPretty = (props) => { const { value } = props; const fieldSchema = useFieldSchema(); - const { getField } = useCollection(); + const { getField } = useCollection_deprecated(); const collectionField = getField(fieldSchema.name); const mapType = props.mapType || collectionField?.uiSchema['x-component-props']?.mapType; const form = useForm(); diff --git a/packages/plugins/@nocobase/plugin-map/src/client/constants.ts b/packages/plugins/@nocobase/plugin-map/src/client/constants.ts index 215ffa2d1..62dfdf0a9 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/constants.ts +++ b/packages/plugins/@nocobase/plugin-map/src/client/constants.ts @@ -8,4 +8,4 @@ export const MapTypes = [ export const selectedImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAA/CAMAAAC7OkrPAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAEJQTFRFAAAA8Yti8Yti8Itj8Iti74tj8Itj8YtiKwADKwADKhw5Kh07KwADKwADKwADKwAD4odn1YBlKwADKwADKwADKwAD/5y7LQAAABZ0Uk5TAP/8/f/B/PYOHjY3CCozLP3XCSkvMhA05K4AAAC4SURBVHic7dXLDoIwEIXhwSsoLSrw/q8qbSAiMy3/oivj2czmS5omnVORdapVJJFKBSEL2mrrUurbpdXa5dTH5dXi9tTsGNtX0TFG1OT+7PdY2YdEGd0FuFmUwXWm5UAZbCTKYA3SUrXZQTHLHbUy2OlsMO0ultLsajLlbEWZMEUZ+ig5E6YKM2GqMBOmGKubW1D3ps6g1nnvA5uGa5Os85E9nmF2ebYkzeKhc9wre4V+GMeh317hDfXgCWigIGJbAAAAAElFTkSuQmCC'; export const defaultImage = - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAA/CAYAAACM5Lr9AAAFkklEQVR42s3VaWxUVRjG8ddiEKPRxAQTCVSWLnZaWmjpTgulpYUBExP9Agou4IKAogUKAgItlNJCgUIXINFPfsFoDCjibkQRI0QIICAQWUpZugKtpcv0+DxyYqaTe2/vTLf58Etu7znnPf+0k44opQw5i5qsjIFs2APHoBZatFq+02vZeq94yyKs0dOjsAzOTCtuVtNKWtX0UpeaXqHU07tgt4ZnvuMa93Avz/CsniE2WIQV3iEaCDnQ8F/MTgZ4h2d4ljNgmZ4pFszDpmIRIuFP57YWDMcFu7uHMziLMyEKxJBl2MbbM6DZub2NQ3sUZ3K2vkMMGIdNKbi1ADqcZe1q2i7VKzibd8BCEE8GUQ3PQ4ez3KWcGNCbeAfv0neKm85hWRvqo+Hu1LJ2HuwTvAt3tkAciKbDIDO/7iE4l7X9rpqCA30pa3uLwt3nYRAIuIfV5mcWN6msnapf8G405IGQMGry+prB0DiloqPfwng3G3SLDltXnTt5S6PK3NnRr9jAFhDJyLsZAJcz8SGcjOr+xAa0XGGTpOfeSMrYUKcyKlx+gS1oSkbY9VXpm2+rjHKXX2ALm2TS2qq96duaVHp5u19gC5skbc3V05N23FWT8NIfsIVNkra6siGttE2llbX7h9JWhaZ6mfj+lfaJZW3Kn6DJJRNWXWqbgN+Y39jRqtDUKKkrL15PxfdjammrfyhpVmiqlNQVfx9KwX/cFJT6A7awSVLeu1CeUlinxu9o8QsphbUKTWUyfvn5Wcm511QSXvoDtrBJkpedewzaE/E58wdsYZMopSQ55+z+pKK6fo9KKqpXaDkAIgxLWnomK3H1JZVQ0tyv2ICW6XAvLHHJ6QD4K6GgRsVjQ3/g3WzQLffCKGHJqfHgisOXaNy2f/oU7+TdukGIUW5xJ0vic6+oWGzsS7yTd4NoncPiF594GC7FFlSrcVub+gTv4p36btE6h1Fc9nFn7IpzKmZrY5/gXbwTxI0O8xD77rGPYtZeVtE42Jt4B+8C8WAcNu6dPwZDzVh8PYzdcqdXcDbvgMdBPDHEOG7R0Xkxqy6oKAzpDZzNO0AMmIfFvH1kABwZk1epIotv9yjOxOyj+g4xYB4W/dbvlAAdozGsJ3Gmni0mzMPGLvyN6IMo/NojNt/qEZyFmR+CWLAIW3CYaDDUR+D/TTgGdwdncJaeKRbMw8bM/9XdosjlZ5VjU0O3cAZngXTBPCzqzV/cPQAXHeuqVBgu8IVj/TXFGXqWdME8LHLez55mRSw9qUI31fuEZzFjNogNFmFvHPQUACfC8IUbUlTvlbDcSsWzeobYYB42+vWfjDwbnn3c6zCewdnnQGwyD4t47UcjAXA2JO+qCi6ss4V7eUafFZuswn4wM8ex+LgKwqV2cC/OzAXxgnlY+KvfmxkIVaPyr6tRhbWWuId79RnxgnmYY+53VlaHLj2lRm6stRSSc0ph7xoQL1mEzfnWyhPQNrKgGgE1hrjGPTAExEvmYWGvfNOVPUErz6vhiDDCNez5GMQHVmFfd2Vq6MLD6smCGkNc4x4QH5iHPfXyV125H64NX1eFkOpO+I5reo/4wDws9KUDdhSPxFdNIGLc8R3XQHxkFfalHYnB8w6qYRuqO+E7roH4yDws5MX9dtwHlYF5lQi6qYjPfKfXxEcWYbO/sKtsBP50QxFFI/D/De8qQLrBPCwYizY9M2r+of/D+Mx3ID6zDJv1uaVwGfAIBDqGJsbhZ9eQ/BuK+OwYlhzPNe7hXh9Yhe0zwqBB4IAYouCZn1wIXH5GEZ/d1sjBMzzrBfOwoBf2GWFYBMS4C03JWRA087OaoBmf3uCz5zrP8Kx9lmF7jXiE2RbBs95ghB0MIo8/pS0OeJAzvOBLmKY//BAGURBN+jmMa3qPkLdh/wKOL8SpLbnYFgAAAABJRU5ErkJggg=='; + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAA/CAYAAACM5Lr9AAAFkklEQVR42s3VaWxUVRjG8ddiEKPRxAQTCVSWLnZaWmjpTgulpYUBExP9Agou4IKAogUKAgItlNJCgUIXINFPfsFoDCjibkQRI0QIICAQWUpZugKtpcv0+DxyYqaTe2/vTLf58Etu7znnPf+0k44opQw5i5qsjIFs2APHoBZatFq+02vZeq94yyKs0dOjsAzOTCtuVtNKWtX0UpeaXqHU07tgt4ZnvuMa93Avz/CsniE2WIQV2iEaCDnQ8F/MTgZ4h2d4ljNgmZ4pFszDpmIRIuFP57YWDMcFu7uHMziLMyEKxJBl2MbbM6DZub2NQ3sUZ3K2vkMMGIdNKbi1ADqcZe1q2i7VKzibd8BCEE8GUQ3PQ4ez3KWcGNCbeAfv0neKm85hWRvqo+Hu1LJ2HuwTvAt3tkAciKbDIDO/7iE4l7X9rpqCA30pa3uLwt3nYRAIuIfV5mcWN6msnapf8G405IGQMGry+prB0DiloqPfwng3G3SLDltXnTt5S6PK3NnRr9jAFhDJyLsZAJcz8SGcjOr+xAa0XGGTpOfeSMrYUKcyKlx+gS1oSkbY9VXpm2+rjHKXX2ALm2TS2qq96duaVHp5u19gC5skbc3V05N23FWT8NIfsIVNkra6siGttE2llbX7h9JWhaZ6mfj+lfaJZW3Kn6DJJRNWXWqbgN+Y39jRqtDUKKkrL15PxfdjammrfyhpVmiqlNQVfx9KwX/cFJT6A7awSVLeu1CeUlinxu9o8QsphbUKTWUyfvn5Wcm511QSXvoDtrBJkpedewzaE/E58wdsYZMopSQ55+z+pKK6fo9KKqpXaDkAIgxLWnomK3H1JZVQ0tyv2ICW6XAvLHHJ6QD4K6GgRsVjQ3/g3WzQLffCKGHJqfHgisOXaNy2f/oU7+TdukGIUW5xJ0vic6+oWGzsS7yTd4NoncPiF594GC7FFlSrcVub+gTv4p36btE6h1Fc9nFn7IpzKmZrY5/gXbwTxI0O8xD77rGPYtZeVtE42Jt4B+8C8WAcNu6dPwZDzVh8PYzdcqdXcDbvgMdBPDHEOG7R0Xkxqy6oKAzpDZzNO0AMmIfFvH1kABwZk1epIotv9yjOxOyj+g4xYB4W/dbvlAAdozGsJ3Gmni0mzMPGLvyN6IMo/NojNt/qEZyFmR+CWLAIW3CYaDDUR+D/TTgGdwdncJaeKRbMw8bM/9XdosjlZ5VjU0O3cAZngXTBPCzqzV/cPQAXHeuqVBgu8IVj/TXFGXqWdME8LHLez55mRSw9qUI31fuEZzFjNogNFmFvHPQUACfC8IUbUlTvlbDcSsWzeobYYB42+vWfjDwbnn3c6zCewdnnQGwyD4t47UcjAXA2JO+qCi6ss4V7eUafFZuswn4wM8ex+LgKwqV2cC/OzAXxgnlY+KvfmxkIVaPyr6tRhbWWuId79RnxgnmYY+53VlaHLj2lRm6stRSSc0ph7xoQL1mEzfnWyhPQNrKgGgE1hrjGPTAExEvmYWGvfNOVPUErz6vhiDDCNez5GMQHVmFfd2Vq6MLD6smCGkNc4x4QH5iHPfXyV125H64NX1eFkOpO+I5reo/4wDws9KUDdhSPxFdNIGLc8R3XQHxkFfalHYnB8w6qYRuqO+E7roH4yDws5MX9dtwHlYF5lQi6qYjPfKfXxEcWYbO/sKtsBP50QxFFI/D/De8qQLrBPCwYizY9M2r+of/D+Mx3ID6zDJv1uaVwGfAIBDqGJsbhZ9eQ/BuK+OwYlhzPNe7hXh9Yhe0zwqBB4IAYouCZn1wIXH5GEZ/d1sjBMzzrBfOwoBf2GWFYBMS4C03JWRA087OaoBmf3uCz5zrP8Kx9lmF7jXiE2RbBs95ghB0MIo8/pS0OeJAzvOBLmKY//BAGURBN+jmMa3qPkLdh/wKOL8SpLbnYFgAAAABJRU5ErkJggg=='; diff --git a/packages/plugins/@nocobase/plugin-map/src/client/fields/circle.ts b/packages/plugins/@nocobase/plugin-map/src/client/fields/circle.ts index aa98c9fea..32acefa5a 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/fields/circle.ts +++ b/packages/plugins/@nocobase/plugin-map/src/client/fields/circle.ts @@ -1,17 +1,16 @@ -import { IField } from '@nocobase/client'; import { generateNTemplate } from '../locale'; -import { commonSchema } from './schema'; +import { CommonSchema } from './schema'; -export const circle: IField = { - name: 'circle', - type: 'object', - group: 'map', - order: 3, - title: generateNTemplate('Circle'), - availableTypes: ['circle'], - description: generateNTemplate('Circle'), - sortable: true, - default: { +export class CircleFieldInterface extends CommonSchema { + name = 'circle'; + type = 'object'; + group = 'map'; + order = 3; + title = generateNTemplate('Circle'); + availableTypes = ['circle']; + description = generateNTemplate('Circle'); + sortable = true; + default = { type: 'circle', uiSchema: { type: 'void', @@ -19,6 +18,5 @@ export const circle: IField = { 'x-component-designer': 'Map.Designer', 'x-component-props': {}, }, - }, - ...commonSchema, -}; + }; +} diff --git a/packages/plugins/@nocobase/plugin-map/src/client/fields/index.ts b/packages/plugins/@nocobase/plugin-map/src/client/fields/index.ts index 13a589554..ca9abe26c 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/fields/index.ts +++ b/packages/plugins/@nocobase/plugin-map/src/client/fields/index.ts @@ -1,11 +1,6 @@ -import { circle } from './circle'; -import { lineString } from './lineString'; -import { point } from './point'; -import { polygon } from './polygon'; +import { CircleFieldInterface } from './circle'; +import { LineStringFieldInterface } from './lineString'; +import { PointFieldInterface } from './point'; +import { PolygonFieldInterface } from './polygon'; -export const fields = [point, polygon, lineString, circle]; - -export const interfaces = fields.reduce((ins, field) => { - ins[field.name] = field; - return ins; -}, {}); +export const fields = [PointFieldInterface, PolygonFieldInterface, LineStringFieldInterface, CircleFieldInterface]; diff --git a/packages/plugins/@nocobase/plugin-map/src/client/fields/lineString.ts b/packages/plugins/@nocobase/plugin-map/src/client/fields/lineString.ts index 08e561b9a..55bc4f6e9 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/fields/lineString.ts +++ b/packages/plugins/@nocobase/plugin-map/src/client/fields/lineString.ts @@ -1,17 +1,16 @@ -import { IField } from '@nocobase/client'; import { generateNTemplate } from '../locale'; -import { commonSchema } from './schema'; +import { CommonSchema } from './schema'; -export const lineString: IField = { - name: 'lineString', - type: 'object', - group: 'map', - order: 2, - title: generateNTemplate('Line'), - description: generateNTemplate('Line'), - availableTypes: ['lineString'], - sortable: true, - default: { +export class LineStringFieldInterface extends CommonSchema { + name = 'lineString'; + type = 'object'; + group = 'map'; + order = 2; + title = generateNTemplate('Line'); + description = generateNTemplate('Line'); + availableTypes = ['lineString']; + sortable = true; + default = { type: 'lineString', uiSchema: { type: 'void', @@ -19,6 +18,5 @@ export const lineString: IField = { 'x-component-designer': 'Map.Designer', 'x-component-props': {}, }, - }, - ...commonSchema, -}; + }; +} diff --git a/packages/plugins/@nocobase/plugin-map/src/client/fields/point.ts b/packages/plugins/@nocobase/plugin-map/src/client/fields/point.ts index e4923482f..c709e31d3 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/fields/point.ts +++ b/packages/plugins/@nocobase/plugin-map/src/client/fields/point.ts @@ -1,17 +1,16 @@ -import { IField } from '@nocobase/client'; import { generateNTemplate } from '../locale'; -import { commonSchema } from './schema'; +import { CommonSchema } from './schema'; -export const point: IField = { - name: 'point', - type: 'object', - group: 'map', - order: 1, - title: generateNTemplate('Point'), - description: generateNTemplate('Point'), - availableTypes: ['point'], - sortable: true, - default: { +export class PointFieldInterface extends CommonSchema { + name = 'point'; + type = 'object'; + group = 'map'; + order = 1; + title = generateNTemplate('Point'); + description = generateNTemplate('Point'); + availableTypes = ['point']; + sortable = true; + default = { type: 'point', uiSchema: { type: 'void', @@ -19,6 +18,5 @@ export const point: IField = { 'x-component-designer': 'Map.Designer', 'x-component-props': {}, }, - }, - ...commonSchema, -}; + }; +} diff --git a/packages/plugins/@nocobase/plugin-map/src/client/fields/polygon.ts b/packages/plugins/@nocobase/plugin-map/src/client/fields/polygon.ts index 58e7ed939..2bf2dbe7d 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/fields/polygon.ts +++ b/packages/plugins/@nocobase/plugin-map/src/client/fields/polygon.ts @@ -1,17 +1,16 @@ -import { IField } from '@nocobase/client'; import { generateNTemplate } from '../locale'; -import { commonSchema } from './schema'; +import { CommonSchema } from './schema'; -export const polygon: IField = { - name: 'polygon', - type: 'object', - group: 'map', - order: 4, - title: generateNTemplate('Polygon'), - description: generateNTemplate('Polygon'), - availableTypes: ['polygon'], - sortable: true, - default: { +export class PolygonFieldInterface extends CommonSchema { + name = 'polygon'; + type = 'object'; + group = 'map'; + order = 4; + title = generateNTemplate('Polygon'); + description = generateNTemplate('Polygon'); + availableTypes = ['polygon']; + sortable = true; + default = { type: 'polygon', uiSchema: { type: 'void', @@ -19,6 +18,5 @@ export const polygon: IField = { 'x-component-designer': 'Map.Designer', 'x-component-props': {}, }, - }, - ...commonSchema, -}; + }; +} diff --git a/packages/plugins/@nocobase/plugin-map/src/client/fields/schema.ts b/packages/plugins/@nocobase/plugin-map/src/client/fields/schema.ts index 9513533d3..d739a6f93 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/fields/schema.ts +++ b/packages/plugins/@nocobase/plugin-map/src/client/fields/schema.ts @@ -1,5 +1,5 @@ import { ISchema } from '@formily/react'; -import { interfacesProperties } from '@nocobase/client'; +import { CollectionFieldInterface, interfacesProperties } from '@nocobase/client'; import { MapTypes } from '../constants'; import { generateNTemplate } from '../locale'; @@ -26,8 +26,8 @@ if (Array.isArray(interfacesProperties.type.enum)) { ); } -export const commonSchema = { - properties: { +export class CommonSchema extends CollectionFieldInterface { + properties = { ...defaultProps, 'uiSchema.x-component-props.mapType': { title: generateNTemplate('Map type'), @@ -43,12 +43,12 @@ export const commonSchema = { default: 'amap', enum: MapTypes, }, - }, + }; schemaInitialize(schema: ISchema, { block }) { if (block === 'Form') { Object.assign(schema, { 'x-designer': 'Map.Designer', }); } - }, -}; + } +} diff --git a/packages/plugins/@nocobase/plugin-map/src/client/index.tsx b/packages/plugins/@nocobase/plugin-map/src/client/index.tsx index b8057da56..d400d901d 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/index.tsx @@ -1,26 +1,18 @@ -import { CollectionManagerContext, CurrentAppInfoProvider, Plugin, SchemaComponentOptions } from '@nocobase/client'; -import React, { useContext } from 'react'; +import { CurrentAppInfoProvider, Plugin, SchemaComponentOptions } from '@nocobase/client'; +import React from 'react'; import { MapBlockOptions } from './block'; import { mapActionInitializers } from './block/MapActionInitializers'; import { Configuration, Map } from './components'; -import { interfaces } from './fields'; -import { MapInitializer } from './initialize'; +import { fields } from './fields'; import { generateNTemplate } from './locale'; import { NAMESPACE } from './locale'; - +import { mapBlockSettings } from './block/MapBlock.Settings'; const MapProvider = React.memo((props) => { - const ctx = useContext(CollectionManagerContext); return ( - - - - - {props.children} - - - - + + {props.children} + ); }); @@ -30,7 +22,15 @@ export class MapPlugin extends Plugin { async load() { this.app.use(MapProvider); + this.app.dataSourceManager.addFieldInterfaces(fields); + this.app.dataSourceManager.addFieldInterfaceGroups({ + map: { + label: generateNTemplate('Map-based geometry'), + order: 51, + }, + }); this.app.schemaInitializerManager.add(mapActionInitializers); + this.schemaSettingsManager.add(mapBlockSettings); const blockInitializers = this.app.schemaInitializerManager.get('BlockInitializers'); blockInitializers?.add('dataBlocks.map', { diff --git a/packages/plugins/@nocobase/plugin-map/src/client/initialize.tsx b/packages/plugins/@nocobase/plugin-map/src/client/initialize.tsx deleted file mode 100644 index 623a3b297..000000000 --- a/packages/plugins/@nocobase/plugin-map/src/client/initialize.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { registerField, registerGroup, useCurrentAppInfo } from '@nocobase/client'; -import React, { useEffect } from 'react'; -import { fields } from './fields'; -import { generateNTemplate } from './locale'; -import './locale'; - -export const useRegisterInterface = () => { - const { data } = useCurrentAppInfo() || {}; - useEffect(() => { - const dialect = data?.database.dialect; - if (!dialect) return; - - registerGroup(fields[0].group, { - label: generateNTemplate('Map-based geometry'), - order: 51, - }); - - fields.forEach((field) => { - if (Array.isArray(field.dialects)) { - if (!field.dialects.includes(dialect)) { - return; - } - } - registerField(field.group, field.name as string, field); - }); - }, [data]); -}; - -export const MapInitializer: React.FC = (props) => { - useRegisterInterface(); - return {props.children}; -}; diff --git a/packages/plugins/@nocobase/plugin-math-formula-field/src/client/MathFormulaFieldProvider.tsx b/packages/plugins/@nocobase/plugin-math-formula-field/src/client/MathFormulaFieldProvider.tsx index 0f80a370b..b1c319970 100644 --- a/packages/plugins/@nocobase/plugin-math-formula-field/src/client/MathFormulaFieldProvider.tsx +++ b/packages/plugins/@nocobase/plugin-math-formula-field/src/client/MathFormulaFieldProvider.tsx @@ -1,13 +1,9 @@ -import { CollectionManagerContext, registerField, SchemaComponentOptions } from '@nocobase/client'; -import React, { useContext } from 'react'; +import { SchemaComponentOptions } from '@nocobase/client'; +import React from 'react'; import { useTranslation } from 'react-i18next'; -import { mathFormula } from './math-formula'; import { MathFormula } from './MathFormula'; -registerField(mathFormula.group, 'mathFormula', mathFormula); - export const MathFormulaFieldProvider = React.memo((props) => { - const ctx = useContext(CollectionManagerContext); const { t } = useTranslation(); return ( { }} components={{ MathFormula }} > - - {props.children} - + {props.children} ); }); diff --git a/packages/plugins/@nocobase/plugin-math-formula-field/src/client/index.tsx b/packages/plugins/@nocobase/plugin-math-formula-field/src/client/index.tsx index 95a50d80e..80a9437c4 100644 --- a/packages/plugins/@nocobase/plugin-math-formula-field/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-math-formula-field/src/client/index.tsx @@ -1,9 +1,11 @@ import { Plugin } from '@nocobase/client'; import { MathFormulaFieldProvider } from './MathFormulaFieldProvider'; +import { MathFormulaFieldInterface } from './math-formula'; export class MathFormulaFieldPlugin extends Plugin { async load() { this.app.use(MathFormulaFieldProvider); + this.app.dataSourceManager.addFieldInterfaces([MathFormulaFieldInterface]); } } diff --git a/packages/plugins/@nocobase/plugin-math-formula-field/src/client/math-formula.tsx b/packages/plugins/@nocobase/plugin-math-formula-field/src/client/math-formula.tsx index 9089e5717..ec1926ffc 100644 --- a/packages/plugins/@nocobase/plugin-math-formula-field/src/client/math-formula.tsx +++ b/packages/plugins/@nocobase/plugin-math-formula-field/src/client/math-formula.tsx @@ -1,15 +1,15 @@ -import { IField, interfacesProperties } from '@nocobase/client'; +import { CollectionFieldInterface, interfacesProperties } from '@nocobase/client'; const { defaultProps, operators } = interfacesProperties; -export const mathFormula: IField = { - name: 'mathFormula', - type: 'object', - group: 'advanced', - order: 1, - title: '{{t("Math formula")}}', - description: '{{t("Compute a value based on the other fields using mathjs")}}', - sortable: true, - default: { +export class MathFormulaFieldInterface extends CollectionFieldInterface { + name = 'mathFormula'; + type = 'object'; + group = 'advanced'; + order = 1; + title = '{{t("Math formula")}}'; + description = '{{t("Compute a value based on the other fields using mathjs")}}'; + sortable = true; + default = { type: 'mathFormula', // name, uiSchema: { @@ -22,8 +22,8 @@ export const mathFormula: IField = { step: '1', }, }, - }, - properties: { + }; + properties = { ...defaultProps, dataType: { type: 'string', @@ -75,8 +75,8 @@ export const mathFormula: IField = { useCurrentFields: '{{ useCurrentFields }}', }, }, - }, - filterable: { + }; + filterable = { operators: operators.number, - }, -}; + }; +} diff --git a/packages/plugins/@nocobase/plugin-mobile-client/src/client/index.tsx b/packages/plugins/@nocobase/plugin-mobile-client/src/client/index.tsx index a9997f1d6..b900b50e1 100644 --- a/packages/plugins/@nocobase/plugin-mobile-client/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-mobile-client/src/client/index.tsx @@ -36,7 +36,7 @@ export class MobileClientPlugin extends Plugin { } setMobileRouter() { - const router = createRouterManager({ type: 'hash' }); + const router = createRouterManager({ type: 'hash' }, this.app); router.add('root', { path: '/', element: , diff --git a/packages/plugins/@nocobase/plugin-mock-collections/src/server/field-interfaces/index.ts b/packages/plugins/@nocobase/plugin-mock-collections/src/server/field-interfaces/index.ts index 1983b2e8c..f53d61cd5 100644 --- a/packages/plugins/@nocobase/plugin-mock-collections/src/server/field-interfaces/index.ts +++ b/packages/plugins/@nocobase/plugin-mock-collections/src/server/field-interfaces/index.ts @@ -36,3 +36,4 @@ export * from './time'; export * from './updatedAt'; export * from './updatedBy'; export * from './url'; +export * from './sort'; diff --git a/packages/plugins/@nocobase/plugin-mock-collections/src/server/field-interfaces/input.ts b/packages/plugins/@nocobase/plugin-mock-collections/src/server/field-interfaces/input.ts index ae4f44e1b..e9583ffec 100644 --- a/packages/plugins/@nocobase/plugin-mock-collections/src/server/field-interfaces/input.ts +++ b/packages/plugins/@nocobase/plugin-mock-collections/src/server/field-interfaces/input.ts @@ -8,5 +8,12 @@ export const input = { 'x-component': 'Input', }, }), - mock: () => faker.word.words({ count: { min: 5, max: 10 } }), + mock: (option) => { + if (option.collectionName === 'roles' && option.name === 'name') { + // roles.name can only include A-Z, a-z, 0-9, _-*$ + return faker.string.alpha(10); + } + + return faker.word.words({ count: { min: 5, max: 10 } }); + }, }; diff --git a/packages/plugins/@nocobase/plugin-mock-collections/src/server/field-interfaces/sort.ts b/packages/plugins/@nocobase/plugin-mock-collections/src/server/field-interfaces/sort.ts new file mode 100644 index 000000000..52ed32c62 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-mock-collections/src/server/field-interfaces/sort.ts @@ -0,0 +1,20 @@ +export const sort = { + options: () => ({ + type: 'sort', + // name, + uiSchema: { + type: 'number', + // title, + 'x-component': 'InputNumber', + 'x-component-props': { + stringMode: true, + step: '1', + }, + 'x-validator': 'integer', + }, + }), + mock: () => { + // 返回一个 undefined,由后端自动生成 + return; + }, +}; diff --git a/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/settings/schemas/applications.ts b/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/settings/schemas/applications.ts index 1541cbc66..ef79bcdd2 100644 --- a/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/settings/schemas/applications.ts +++ b/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/settings/schemas/applications.ts @@ -216,7 +216,7 @@ export const schema: ISchema = { }, }, }, - 'x-component': 'CollectionProvider', + 'x-component': 'CollectionProvider_deprecated', 'x-component-props': { collection, }, diff --git a/packages/plugins/@nocobase/plugin-multi-app-share-collection/src/client/TableTransfer.tsx b/packages/plugins/@nocobase/plugin-multi-app-share-collection/src/client/TableTransfer.tsx index 7d0506769..f1b10280a 100644 --- a/packages/plugins/@nocobase/plugin-multi-app-share-collection/src/client/TableTransfer.tsx +++ b/packages/plugins/@nocobase/plugin-multi-app-share-collection/src/client/TableTransfer.tsx @@ -1,5 +1,5 @@ import { connect } from '@formily/react'; -import { css, useCollectionManager, useRecord, useRequest, useToken } from '@nocobase/client'; +import { css, useCollectionManager_deprecated, useRecord, useRequest, useToken } from '@nocobase/client'; import { CollectionsGraph, lodash } from '@nocobase/utils/client'; import { App, Col, Input, Row, Select, Spin, Table, Tag } from 'antd'; import React, { useCallback, useMemo, useState } from 'react'; @@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'; const excludeCollections = ['users', 'roles', 'applications']; const useCollectionsGraph = ({ removed = [] }) => { - const { collections } = useCollectionManager(); + const { collections } = useCollectionManager_deprecated(); const findAddable = useCallback( (name) => { diff --git a/packages/plugins/@nocobase/plugin-saml/src/client/schemas/saml.ts b/packages/plugins/@nocobase/plugin-saml/src/client/schemas/saml.ts index b676e94ca..a9594bf48 100644 --- a/packages/plugins/@nocobase/plugin-saml/src/client/schemas/saml.ts +++ b/packages/plugins/@nocobase/plugin-saml/src/client/schemas/saml.ts @@ -146,7 +146,7 @@ export const samlSchema: ISchema = { }, }, }, - 'x-component': 'CollectionProvider', + 'x-component': 'CollectionProvider_deprecated', 'x-component-props': { collection, }, diff --git a/packages/plugins/@nocobase/plugin-sequence-field/src/client/SequenceFieldProvider.tsx b/packages/plugins/@nocobase/plugin-sequence-field/src/client/SequenceFieldProvider.tsx index c0bb32bb6..434e23575 100644 --- a/packages/plugins/@nocobase/plugin-sequence-field/src/client/SequenceFieldProvider.tsx +++ b/packages/plugins/@nocobase/plugin-sequence-field/src/client/SequenceFieldProvider.tsx @@ -1,23 +1,17 @@ -import React, { FC, useContext } from 'react'; +import React, { FC } from 'react'; -import { registerField, CollectionManagerContext, SchemaComponentOptions } from '@nocobase/client'; +import { SchemaComponentOptions } from '@nocobase/client'; -import { RuleConfigForm, sequence } from './sequence'; - -registerField(sequence.group, 'sequence', sequence); +import { RuleConfigForm } from './sequence'; export const SequenceFieldProvider: FC = (props) => { - const ctx = useContext(CollectionManagerContext); - return ( - - {props.children} - + {props.children} ); }; diff --git a/packages/plugins/@nocobase/plugin-sequence-field/src/client/index.tsx b/packages/plugins/@nocobase/plugin-sequence-field/src/client/index.tsx index 43a06910f..511c4a872 100644 --- a/packages/plugins/@nocobase/plugin-sequence-field/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-sequence-field/src/client/index.tsx @@ -1,9 +1,11 @@ import { Plugin } from '@nocobase/client'; import { SequenceFieldProvider } from './SequenceFieldProvider'; +import { SequenceFieldInterface } from './sequence'; export class SequenceFieldPlugin extends Plugin { async load() { this.app.use(SequenceFieldProvider); + this.app.dataSourceManager.addFieldInterfaces([SequenceFieldInterface]); } } diff --git a/packages/plugins/@nocobase/plugin-sequence-field/src/client/sequence.tsx b/packages/plugins/@nocobase/plugin-sequence-field/src/client/sequence.tsx index b46142a48..20474c03e 100644 --- a/packages/plugins/@nocobase/plugin-sequence-field/src/client/sequence.tsx +++ b/packages/plugins/@nocobase/plugin-sequence-field/src/client/sequence.tsx @@ -2,8 +2,8 @@ import { ArrayTable, FormButtonGroup, FormDrawer, FormLayout, Submit } from '@fo import { onFieldValueChange } from '@formily/core'; import { ISchema, SchemaOptionsContext, useForm, useFormEffects } from '@formily/react'; import { + CollectionFieldInterface, Cron, - IField, SchemaComponent, SchemaComponentOptions, css, @@ -256,28 +256,28 @@ export function RuleConfigForm() { ) : null; } -export const sequence: IField = { - name: 'sequence', - type: 'object', - group: 'advanced', - order: 3, - title: `{{t("Sequence", { ns: "${NAMESPACE}" })}}`, - description: `{{t("Automatically generate codes based on configured rules, supporting combinations of dates, numbers, and text.", { ns: "${NAMESPACE}" })}}`, - sortable: true, - default: { +export class SequenceFieldInterface extends CollectionFieldInterface { + name = 'sequence'; + type = 'object'; + group = 'advanced'; + order = 3; + title = `{{t("Sequence", { ns: "${NAMESPACE}" })}}`; + description = `{{t("Automatically generate codes based on configured rules, supporting combinations of dates, numbers, and text.", { ns: "${NAMESPACE}" })}}`; + sortable = true; + default = { type: 'sequence', uiSchema: { type: 'string', 'x-component': 'Input', 'x-component-props': {}, }, - }, - availableTypes: ['string'], - hasDefaultValue: false, - filterable: { + }; + availableTypes = ['string']; + hasDefaultValue = false; + filterable = { operators: interfacesProperties.operators.string, - }, - titleUsable: true, + }; + titleUsable = true; schemaInitialize(schema: ISchema, { block, field }) { if (block === 'Form') { Object.assign(schema['x-component-props'], { @@ -285,8 +285,8 @@ export const sequence: IField = { }); } return schema; - }, - properties: { + } + properties = { ...interfacesProperties.defaultProps, unique: interfacesProperties.unique, patterns: { @@ -402,5 +402,5 @@ export const sequence: IField = { }, }, }, - }, -}; + }; +} diff --git a/packages/plugins/@nocobase/plugin-sms-auth/src/server/__tests__/signin.test.ts b/packages/plugins/@nocobase/plugin-sms-auth/src/server/__tests__/signin.test.ts index 2094f2317..7e95c91ec 100644 --- a/packages/plugins/@nocobase/plugin-sms-auth/src/server/__tests__/signin.test.ts +++ b/packages/plugins/@nocobase/plugin-sms-auth/src/server/__tests__/signin.test.ts @@ -1,6 +1,6 @@ import { Database, Model, ModelStatic } from '@nocobase/database'; import VerificationPlugin from '@nocobase/plugin-verification'; -import { MockServer, createMockServer } from '@nocobase/test'; +import { createMockServer, MockServer } from '@nocobase/test'; import { authType } from '../../constants'; class Provider { @@ -21,7 +21,7 @@ describe('signin', () => { beforeAll(async () => { app = await createMockServer({ - plugins: ['users', 'auth', 'verification', 'acl', 'sms-auth'], + plugins: ['users', 'auth', 'verification', 'acl', 'sms-auth', 'data-source-manager'], }); db = app.db; agent = app.agent(); diff --git a/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializersDetailItem.tsx b/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializersDetailItem.tsx index 1f79e3831..244a2ac38 100644 --- a/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializersDetailItem.tsx +++ b/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializersDetailItem.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { FormOutlined } from '@ant-design/icons'; import { useBlockAssociationContext, - useCollection, + useCollection_deprecated, useSchemaTemplateManager, useRecordCollectionDataSourceItems, useBlockRequestContext, @@ -74,7 +74,7 @@ export const SnapshotBlockInitializersDetailItem = () => { const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); // eslint-disable-next-line react-hooks/rules-of-hooks - const collection = targetCollection || useCollection(); + const collection = targetCollection || useCollection_deprecated(); const association = useBlockAssociationContext(); const { block } = useBlockRequestContext(); const actionInitializers = diff --git a/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotBlock/SnapshotBlockProvider.tsx b/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotBlock/SnapshotBlockProvider.tsx index a5364dccd..eb4e4431b 100644 --- a/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotBlock/SnapshotBlockProvider.tsx +++ b/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotBlock/SnapshotBlockProvider.tsx @@ -1,21 +1,22 @@ -import React, { useContext, useMemo, useRef } from 'react'; import { createForm } from '@formily/core'; import { useField } from '@formily/react'; import { BlockAssociationContext, - BlockRequestContext, + BlockRequestContext_deprecated, BlockResourceContext, + CollectionManagerProvider, FormBlockContext, MaybeCollectionProvider, RecordProvider, useBlockRequestContext, useBlockResource, - useCollectionManager, + useCollectionManager_deprecated, useDesignable, useRecord, useResource, } from '@nocobase/client'; import { Spin } from 'antd'; +import React, { useMemo, useRef } from 'react'; const InternalFormBlockProvider = (props) => { const { action, readPretty } = props; @@ -55,7 +56,7 @@ const InternalFormBlockProvider = (props) => { ); }; -const BlockRequestProvider = (props) => { +const BlockRequestProvider_deprecated = (props) => { const field = useField(); const resource = useBlockResource(); const service = { @@ -64,33 +65,35 @@ const BlockRequestProvider = (props) => { data: useRecord(), }, }; - const __parent = useContext(BlockRequestContext); + const __parent = useBlockRequestContext(); return ( - + {props.children} - + ); }; const BlockProvider = (props) => { - const { collection, association } = props; + const { collection, association, dataSource } = props; const resource = useResource(props); return ( - - - - {props.children} - - - + + + + + {props.children} + + + + ); }; export const SnapshotBlockProvider = (props) => { const record = useRecord(); const { __tableName } = record; - const { getInheritCollections } = useCollectionManager(); + const { getInheritCollections } = useCollectionManager_deprecated(props.dataSource); const inheritCollections = getInheritCollections(__tableName); const { designable } = useDesignable(); const flag = diff --git a/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotFieldProvider.tsx b/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotFieldProvider.tsx index 188ebf87d..f130498b5 100644 --- a/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotFieldProvider.tsx +++ b/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotFieldProvider.tsx @@ -1,40 +1,24 @@ -import { - CollectionHistoryProvider, - CollectionManagerProvider, - registerField, - SchemaComponentOptions, -} from '@nocobase/client'; -import React, { useEffect } from 'react'; +import { CollectionHistoryProvider, SchemaComponentOptions } from '@nocobase/client'; +import React from 'react'; import { SnapshotOwnerCollectionFieldsSelect } from './components/SnapshotOwnerCollectionFieldsSelect'; -import { snapshot } from './interface'; import { SnapshotBlockInitializersDetailItem } from './SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializersDetailItem'; import { SnapshotBlockProvider } from './SnapshotBlock/SnapshotBlockProvider'; import { SnapshotRecordPicker } from './SnapshotRecordPicker'; export const SnapshotFieldProvider = React.memo((props) => { - useEffect(() => { - registerField(snapshot.group, snapshot.name as string, snapshot); - }, []); - return ( - - - - {props.children} - - - + + + {props.children} + + ); }); SnapshotFieldProvider.displayName = 'SnapshotFieldProvider'; diff --git a/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotHistoryCollectionProvider.tsx b/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotHistoryCollectionProvider.tsx index 8b6e733fe..465d53ad1 100644 --- a/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotHistoryCollectionProvider.tsx +++ b/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotHistoryCollectionProvider.tsx @@ -1,9 +1,14 @@ -import { CollectionManagerContext, useHistoryCollectionsByNames } from '@nocobase/client'; -import React, { useContext } from 'react'; +import { + CollectionManagerProvider_deprecated, + ExtendCollectionsProvider, + useCollectionManager_deprecated, + useHistoryCollectionsByNames, +} from '@nocobase/client'; +import React from 'react'; export const SnapshotHistoryCollectionProvider: React.FC<{ collectionName: string }> = (props) => { const { collectionName } = props; - const { collections: allCollections, ...rest } = useContext(CollectionManagerContext); + const { collections: allCollections } = useCollectionManager_deprecated(); // 目标表 const snapshotTargetCollection = useHistoryCollectionsByNames([collectionName])?.[0]; @@ -30,13 +35,8 @@ export const SnapshotHistoryCollectionProvider: React.FC<{ collectionName: strin const overridedCollections = [...filterdAllCollection, ...finallyHistoryCollecionts]; return ( - - {props.children} - + + {props.children} + ); }; diff --git a/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotRecordPicker.tsx b/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotRecordPicker.tsx index b4627d030..36d683e95 100644 --- a/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotRecordPicker.tsx +++ b/packages/plugins/@nocobase/plugin-snapshot-field/src/client/SnapshotRecordPicker.tsx @@ -1,11 +1,11 @@ import { connect, mapReadPretty, useFieldSchema } from '@formily/react'; -import { ReadPrettyRecordPicker, useCollection } from '@nocobase/client'; +import { ReadPrettyRecordPicker, useCollection_deprecated } from '@nocobase/client'; import React from 'react'; import { SnapshotHistoryCollectionProvider } from './SnapshotHistoryCollectionProvider'; const ReadPrettyRecordPickerWrapper = (props) => { const fieldSchema = useFieldSchema(); - const { getField } = useCollection(); + const { getField } = useCollection_deprecated(); const collectionField = getField(fieldSchema.name); return ( diff --git a/packages/plugins/@nocobase/plugin-snapshot-field/src/client/components/SnapshotOwnerCollectionFieldsSelect.tsx b/packages/plugins/@nocobase/plugin-snapshot-field/src/client/components/SnapshotOwnerCollectionFieldsSelect.tsx index e3ad6f569..dd841427f 100644 --- a/packages/plugins/@nocobase/plugin-snapshot-field/src/client/components/SnapshotOwnerCollectionFieldsSelect.tsx +++ b/packages/plugins/@nocobase/plugin-snapshot-field/src/client/components/SnapshotOwnerCollectionFieldsSelect.tsx @@ -1,4 +1,4 @@ -import { useCollectionManager, useCompile } from '@nocobase/client'; +import { useCollectionManager_deprecated, useCompile } from '@nocobase/client'; import { Select, SelectProps } from 'antd'; import React from 'react'; import { useTopRecord } from '../interface'; @@ -7,7 +7,7 @@ export type SnapshotOwnerCollectionFieldsSelectProps = Omit { const record = useTopRecord(); - const { getCollection } = useCollectionManager(); + const { getCollection } = useCollectionManager_deprecated(); const collection = getCollection(record.name); const compile = useCompile(); diff --git a/packages/plugins/@nocobase/plugin-snapshot-field/src/client/index.tsx b/packages/plugins/@nocobase/plugin-snapshot-field/src/client/index.tsx index cc538bfca..d4d416912 100644 --- a/packages/plugins/@nocobase/plugin-snapshot-field/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-snapshot-field/src/client/index.tsx @@ -1,11 +1,13 @@ import { Plugin } from '@nocobase/client'; import { SnapshotFieldProvider } from './SnapshotFieldProvider'; import { snapshotBlockInitializers } from './SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializers'; +import { SnapshotFieldInterface } from './interface'; export class SnapshotFieldPlugin extends Plugin { async load() { this.app.use(SnapshotFieldProvider); this.app.schemaInitializerManager.add(snapshotBlockInitializers); + this.app.dataSourceManager.addFieldInterfaces([SnapshotFieldInterface]); } } diff --git a/packages/plugins/@nocobase/plugin-snapshot-field/src/client/interface.ts b/packages/plugins/@nocobase/plugin-snapshot-field/src/client/interface.ts index 83cc045ec..731800c23 100644 --- a/packages/plugins/@nocobase/plugin-snapshot-field/src/client/interface.ts +++ b/packages/plugins/@nocobase/plugin-snapshot-field/src/client/interface.ts @@ -1,6 +1,11 @@ import type { Field } from '@formily/core'; import { ISchema, useForm } from '@formily/react'; -import { IField, interfacesProperties, useCollectionManager, useRecord } from '@nocobase/client'; +import { + CollectionFieldInterface, + interfacesProperties, + useCollectionManager_deprecated, + useRecord, +} from '@nocobase/client'; import lodash from 'lodash'; import { NAMESPACE } from './locale'; @@ -19,7 +24,7 @@ export const useTopRecord = () => { }; function useRecordCollection() { - const { getCollectionField } = useCollectionManager(); + const { getCollectionField } = useCollectionManager_deprecated(); const record = useTopRecord(); const formValues = useForm().values; return getCollectionField(`${record.name}.${formValues.targetField}`)?.target; @@ -34,7 +39,7 @@ const onTargetFieldChange = (field: Field) => { }; function MakeFieldsPathOptions(fields, appends = []) { - const { getCollection } = useCollectionManager(); + const { getCollection } = useCollectionManager_deprecated(); const options = []; fields.forEach((field) => { if (['belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type)) { @@ -95,13 +100,13 @@ const recordPickerViewer = { }, }; -export const snapshot: IField = { - name: 'snapshot', - type: 'object', - group: 'advanced', - title: `{{t('Snapshot', {ns: '${NAMESPACE}'})}}`, - description: `{{t('When adding a new record, create a snapshot for its relational record and save in the new record. The snapshot will not be updated when the relational record is updated.', {ns: '${NAMESPACE}'})}}`, - default: { +export class SnapshotFieldInterface extends CollectionFieldInterface { + name = 'snapshot'; + type = 'object'; + group = 'advanced'; + title = `{{t('Snapshot', {ns: '${NAMESPACE}'})}}`; + description = `{{t('When adding a new record, create a snapshot for its relational record and save in the current record. The snapshot is not updated when the record is subsequently updated.', {ns: '${NAMESPACE}'})}}`; + default = { type: 'snapshot', // name, uiSchema: { @@ -115,16 +120,17 @@ export const snapshot: IField = { }, }, }, - }, + }; schemaInitialize(schema: ISchema, { field, readPretty, action, block }) { schema['properties'] = { viewer: lodash.cloneDeep(recordPickerViewer), }; - }, - initialize: (values: any) => {}, + } + initialize(values: any) {} usePathOptions(field) { const { appends = [], targetCollection } = field; - const { getCollection } = useCollectionManager(); + // eslint-disable-next-line + const { getCollection } = useCollectionManager_deprecated(); const { fields } = getCollection(targetCollection); const result = MakeFieldsPathOptions(fields, appends); @@ -136,8 +142,8 @@ export const snapshot: IField = { children: result, }, ]; - }, - properties: { + } + properties = { ...defaultProps, [TARGET_FIELD]: { type: 'string', @@ -170,7 +176,7 @@ export const snapshot: IField = { 'x-component': 'AppendsTreeSelect', 'x-component-props': { multiple: true, - useCollection: useRecordCollection, + useCollection_deprecated: useRecordCollection, }, 'x-reactions': [ { @@ -182,5 +188,5 @@ export const snapshot: IField = { }, ], }, - }, -}; + }; +} diff --git a/packages/plugins/@nocobase/plugin-snapshot-field/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-snapshot-field/src/server/plugin.ts index fb713baa1..de6ce6e10 100644 --- a/packages/plugins/@nocobase/plugin-snapshot-field/src/server/plugin.ts +++ b/packages/plugins/@nocobase/plugin-snapshot-field/src/server/plugin.ts @@ -31,10 +31,10 @@ export class SnapshotFieldPlugin extends Plugin { transaction, }); - await fieldsHistoryRepository.createMany({ - records: collectionDoc.fields ?? [], - transaction, - }); + // await fieldsHistoryRepository.createMany({ + // records: collectionDoc.fields ?? [], + // transaction, + // }); }; this.app.db.on('collections.afterCreateWithAssociations', collectionHandler); diff --git a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/fixtures/data.ts b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/fixtures/data.ts index fce3a8fa5..db685149f 100644 --- a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/fixtures/data.ts +++ b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/fixtures/data.ts @@ -307,7 +307,7 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': 'CollectionProvider_deprecated', 'x-decorator-props': { name: 'attachments' }, 'x-component': 'Action.Drawer', 'x-component-props': { className: 'nb-action-popup' }, @@ -561,7 +561,7 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': 'CollectionProvider_deprecated', 'x-decorator-props': { name: 't_94rsj6kbzvn' }, 'x-component': 'Action.Drawer', 'x-component-props': { className: 'nb-action-popup' }, @@ -722,7 +722,7 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': 'CollectionProvider_deprecated', 'x-decorator-props': { name: 'users' }, 'x-component': 'Action.Drawer', 'x-component-props': { className: 'nb-action-popup' }, @@ -950,7 +950,7 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': 'CollectionProvider_deprecated', 'x-decorator-props': { name: 't_j6omof6tza8' }, 'x-component': 'Action.Drawer', 'x-component-props': { className: 'nb-action-popup' }, @@ -1222,7 +1222,7 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': 'CollectionProvider_deprecated', 'x-decorator-props': { name: 'users' }, 'x-component': 'Action.Drawer', 'x-component-props': { @@ -1413,7 +1413,7 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': 'CollectionProvider_deprecated', 'x-decorator-props': { name: 'attachments' }, 'x-component': 'Action.Drawer', 'x-component-props': { @@ -1854,7 +1854,7 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': 'CollectionProvider_deprecated', 'x-decorator-props': { name: 'users' }, 'x-component': 'Action.Drawer', 'x-component-props': { className: 'nb-action-popup' }, @@ -2018,7 +2018,7 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': 'CollectionProvider_deprecated', 'x-decorator-props': { name: 'attachments' }, 'x-component': 'Action.Drawer', 'x-component-props': { className: 'nb-action-popup' }, @@ -2478,7 +2478,8 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': + 'CollectionProvider_deprecated', 'x-decorator-props': { name: 'attachments', }, @@ -2698,7 +2699,8 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': + 'CollectionProvider_deprecated', 'x-decorator-props': { name: 't_94rsj6kbzvn', }, @@ -2847,7 +2849,8 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': + 'CollectionProvider_deprecated', 'x-decorator-props': { name: 'users' }, 'x-component': 'Action.Drawer', 'x-component-props': { @@ -3306,7 +3309,7 @@ export default { _isJSONSchemaObject: true, version: '2.0', 'x-decorator': - 'CollectionProvider', + 'CollectionProvider_deprecated', 'x-decorator-props': { name: 'users', }, @@ -3708,7 +3711,7 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': 'CollectionProvider_deprecated', 'x-decorator-props': { name: 'attachments' }, 'x-component': 'Action.Drawer', 'x-component-props': { className: 'nb-action-popup' }, @@ -3962,7 +3965,7 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': 'CollectionProvider_deprecated', 'x-decorator-props': { name: 't_94rsj6kbzvn' }, 'x-component': 'Action.Drawer', 'x-component-props': { className: 'nb-action-popup' }, @@ -4124,7 +4127,7 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': 'CollectionProvider_deprecated', 'x-decorator-props': { name: 'users' }, 'x-component': 'Action.Drawer', 'x-component-props': { className: 'nb-action-popup' }, @@ -4352,7 +4355,7 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': 'CollectionProvider_deprecated', 'x-decorator-props': { name: 't_j6omof6tza8' }, 'x-component': 'Action.Drawer', 'x-component-props': { className: 'nb-action-popup' }, @@ -4630,7 +4633,8 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': + 'CollectionProvider_deprecated', 'x-decorator-props': { name: 'users' }, 'x-component': 'Action.Drawer', 'x-component-props': { @@ -4827,7 +4831,8 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': + 'CollectionProvider_deprecated', 'x-decorator-props': { name: 'attachments', }, @@ -5165,7 +5170,7 @@ export default { _isJSONSchemaObject: true, version: '2.0', 'x-decorator': - 'CollectionProvider', + 'CollectionProvider_deprecated', 'x-decorator-props': { name: 'users', }, @@ -5376,7 +5381,7 @@ export default { _isJSONSchemaObject: true, version: '2.0', 'x-decorator': - 'CollectionProvider', + 'CollectionProvider_deprecated', 'x-decorator-props': { name: 'attachments', }, @@ -5631,7 +5636,7 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': 'CollectionProvider_deprecated', 'x-decorator-props': { name: 'users' }, 'x-component': 'Action.Drawer', 'x-component-props': { className: 'nb-action-popup' }, @@ -6017,7 +6022,7 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': 'CollectionProvider_deprecated', 'x-decorator-props': { name: 't_94rsj6kbzvn' }, 'x-component': 'Action.Drawer', 'x-component-props': { className: 'nb-action-popup' }, @@ -6202,7 +6207,7 @@ export default { drawer1: { _isJSONSchemaObject: true, version: '2.0', - 'x-decorator': 'CollectionProvider', + 'x-decorator': 'CollectionProvider_deprecated', 'x-decorator-props': { name: 'users' }, 'x-component': 'Action.Drawer', 'x-component-props': { className: 'nb-action-popup' }, diff --git a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/server-hook-impl.test.ts b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/server-hook-impl.test.ts index 543ee05cf..4bcfd988f 100644 --- a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/server-hook-impl.test.ts +++ b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/__tests__/server-hook-impl.test.ts @@ -1,6 +1,6 @@ import { BelongsToManyRepository, Database } from '@nocobase/database'; import UiSchemaStoragePlugin, { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage'; -import { MockServer, createMockServer } from '@nocobase/test'; +import { createMockServer, MockServer } from '@nocobase/test'; describe('server hooks', () => { let app: MockServer; @@ -15,7 +15,7 @@ describe('server hooks', () => { beforeEach(async () => { app = await createMockServer({ registerActions: true, - plugins: ['ui-schema-storage', 'collection-manager', 'error-handler', 'users', 'acl'], + plugins: ['ui-schema-storage', 'collection-manager', 'error-handler', 'users', 'acl', 'data-source-manager'], }); await app.runCommand('install', '-f'); db = app.db; diff --git a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/collections/uiSchemaTemplates.ts b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/collections/uiSchemaTemplates.ts index 45fef2fec..be0500091 100644 --- a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/collections/uiSchemaTemplates.ts +++ b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/collections/uiSchemaTemplates.ts @@ -27,19 +27,19 @@ export default defineCollection({ type: 'string', name: 'resourceName', }, + { + type: 'string', + name: 'collectionName', + }, + { + type: 'string', + name: 'dataSourceKey', + }, { type: 'belongsTo', name: 'uiSchema', target: 'uiSchemas', foreignKey: 'uid', }, - { - type: 'belongsTo', - name: 'collection', - target: 'collections', - foreignKey: 'collectionName', - targetKey: 'name', - onDelete: 'CASCADE', - }, ], }); diff --git a/packages/plugins/@nocobase/plugin-users/package.json b/packages/plugins/@nocobase/plugin-users/package.json index 0b61eecf4..c13d5c2f5 100644 --- a/packages/plugins/@nocobase/plugin-users/package.json +++ b/packages/plugins/@nocobase/plugin-users/package.json @@ -2,7 +2,7 @@ "name": "@nocobase/plugin-users", "displayName": "Users", "displayName.zh-CN": "用户", - "description": "Provides a basic user model, as well as createdBy and updatedBy fields..", + "description": "Provides basic user model, as well as created by and updated by fields.", "description.zh-CN": "提供了基础的用户模型,以及创建人和最后更新人字段。", "version": "0.19.0-alpha.9", "license": "AGPL-3.0", diff --git a/packages/plugins/@nocobase/plugin-users/src/client/PasswordField.tsx b/packages/plugins/@nocobase/plugin-users/src/client/PasswordField.tsx new file mode 100644 index 000000000..ca16119ce --- /dev/null +++ b/packages/plugins/@nocobase/plugin-users/src/client/PasswordField.tsx @@ -0,0 +1,47 @@ +import React, { useEffect } from 'react'; +import { Input, Row, Col, Button } from 'antd'; +import { Password, useActionContext } from '@nocobase/client'; +import { useField } from '@formily/react'; +import { Field } from '@formily/core'; +import { useUsersTranslation } from './locale'; +import { generatePassword } from './utils'; + +export const PasswordField: React.FC = () => { + const { t } = useUsersTranslation(); + const field = useField(); + const [visible, setVisible] = React.useState(false); + const ctx = useActionContext(); + + useEffect(() => { + if (ctx.visible) { + return; + } + field.reset(); + }, [field, ctx.visible]); + + return ( + + + field.setValue(e.target.value)} + /> + + + + + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-users/src/client/RoleUsersManager.tsx b/packages/plugins/@nocobase/plugin-users/src/client/RoleUsersManager.tsx new file mode 100644 index 000000000..c3aeb17df --- /dev/null +++ b/packages/plugins/@nocobase/plugin-users/src/client/RoleUsersManager.tsx @@ -0,0 +1,127 @@ +import React, { useContext, useRef } from 'react'; +import { App } from 'antd'; +import { + CollectionProvider_deprecated, + ResourceActionContext, + ResourceActionProvider, + SchemaComponent, + useAPIClient, + useActionContext, + useRecord, + useRequest, + useResourceActionContext, +} from '@nocobase/client'; +import { RolesManagerContext } from '@nocobase/plugin-acl/client'; +import { useUsersTranslation } from './locale'; +import { roleUsersSchema, userCollection } from './schemas/users'; +import { useFilterActionProps } from './hooks'; + +const useRemoveUser = () => { + const api = useAPIClient(); + const { role } = useContext(RolesManagerContext); + const record = useRecord(); + const { refresh } = useResourceActionContext(); + return { + async run() { + await api.resource('roles.users', role?.name).remove({ + values: [record['id']], + }); + refresh(); + }, + }; +}; + +const useBulkRemoveUsers = () => { + const { t } = useUsersTranslation(); + const { message } = App.useApp(); + const api = useAPIClient(); + const { state, setState, refresh } = useResourceActionContext(); + const { role } = useContext(RolesManagerContext); + + return { + async run() { + const selected = state?.selectedRowKeys; + if (!selected?.length) { + message.warning(t('Please select users')); + return; + } + await api.resource('roles.users', role?.name).remove({ + values: selected, + }); + setState?.({ selectedRowKeys: [] }); + refresh(); + }, + }; +}; + +const RoleUsersProvider: React.FC = (props) => { + const { role } = useContext(RolesManagerContext); + return ( + + {props.children} + + ); +}; + +export const RoleUsersManager: React.FC = () => { + const { t } = useUsersTranslation(); + const { role } = useContext(RolesManagerContext); + const service = useRequest( + { + resource: 'roles.users', + resourceOf: role?.name, + action: 'list', + }, + { + ready: !!role, + refreshDeps: [role], + }, + ); + const selectedRoleUsers = useRef([]); + const handleSelectRoleUsers = (_: number[], rows: any[]) => { + selectedRoleUsers.current = rows; + }; + const useAddRoleUsers = () => { + const api = useAPIClient(); + const { setVisible } = useActionContext(); + const { refresh } = useResourceActionContext(); + return { + async run() { + await api.resource('roles.users', role?.name).add({ + values: selectedRoleUsers.current.map((user) => user.id), + }); + selectedRoleUsers.current = []; + setVisible(false); + refresh(); + }, + }; + }; + + return ( + + + + + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-users/src/client/UserRolesField.tsx b/packages/plugins/@nocobase/plugin-users/src/client/UserRolesField.tsx new file mode 100644 index 000000000..a9e278865 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-users/src/client/UserRolesField.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { useField, Schema } from '@formily/react'; +import { Field } from '@formily/core'; +import { useUsersTranslation } from './locale'; +import { Tag } from 'antd'; + +export const UserRolesField: React.FC = () => { + const { t } = useUsersTranslation(); + const field = useField(); + return (field.value || []).map((role: { name: string; title: string }) => ( + {Schema.compile(role.title, { t })} + )); +}; diff --git a/packages/plugins/@nocobase/plugin-users/src/client/UsersManagement.tsx b/packages/plugins/@nocobase/plugin-users/src/client/UsersManagement.tsx new file mode 100644 index 000000000..b1876aa88 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-users/src/client/UsersManagement.tsx @@ -0,0 +1,24 @@ +import { SchemaComponent, SchemaComponentContext, useSchemaComponentContext } from '@nocobase/client'; +import React from 'react'; +import { usersSchema } from './schemas/users'; +import { Card } from 'antd'; +import { UserRolesField } from './UserRolesField'; +import { useUsersTranslation } from './locale'; +import { useFilterActionProps } from './hooks'; +import { PasswordField } from './PasswordField'; + +export const UsersManagement: React.FC = () => { + const { t } = useUsersTranslation(); + const scCtx = useSchemaComponentContext(); + return ( + + + + + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-users/src/client/hooks.ts b/packages/plugins/@nocobase/plugin-users/src/client/hooks.ts new file mode 100644 index 000000000..766fbec70 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-users/src/client/hooks.ts @@ -0,0 +1,18 @@ +import { + CollectionContext, + useFilterFieldOptions, + useFilterFieldProps, + useResourceActionContext, +} from '@nocobase/client'; +import { useContext } from 'react'; + +export const useFilterActionProps = () => { + const collection = useContext(CollectionContext); + const options = useFilterFieldOptions(collection.fields); + const service = useResourceActionContext(); + return useFilterFieldProps({ + options, + params: service.state?.params?.[0] || service.params, + service, + }); +}; diff --git a/packages/plugins/@nocobase/plugin-users/src/client/index.ts b/packages/plugins/@nocobase/plugin-users/src/client/index.ts index 2d3dc4265..9929c2247 100644 --- a/packages/plugins/@nocobase/plugin-users/src/client/index.ts +++ b/packages/plugins/@nocobase/plugin-users/src/client/index.ts @@ -1,7 +1,27 @@ -import { Plugin } from '@nocobase/client'; +import { Plugin, tval } from '@nocobase/client'; +import { UsersManagement } from './UsersManagement'; +import ACLPlugin from '@nocobase/plugin-acl/client'; +import { RoleUsersManager } from './RoleUsersManager'; -export class PluginUsersClient extends Plugin { - async load() {} +class PluginUsersClient extends Plugin { + async load() { + this.app.pluginSettingsManager.add('users-permissions', { + title: tval('Users & Permissions', { ns: 'users' }), + icon: 'TeamOutlined', + }); + this.app.pluginSettingsManager.add('users-permissions.users', { + title: tval('Users'), + icon: 'UserOutlined', + Component: UsersManagement, + aclSnippet: 'pm.users', + }); + + const acl = this.app.pm.get(ACLPlugin); + acl.rolesManager.add('users', { + title: tval('Users'), + Component: RoleUsersManager, + }); + } } export default PluginUsersClient; diff --git a/packages/plugins/@nocobase/plugin-users/src/client/locale.ts b/packages/plugins/@nocobase/plugin-users/src/client/locale.ts new file mode 100644 index 000000000..b4cdc0a0c --- /dev/null +++ b/packages/plugins/@nocobase/plugin-users/src/client/locale.ts @@ -0,0 +1,5 @@ +import { useTranslation } from 'react-i18next'; + +export function useUsersTranslation() { + return useTranslation(['users', 'client'], { nsMode: 'fallback' }); +} diff --git a/packages/plugins/@nocobase/plugin-users/src/client/schemas/users.ts b/packages/plugins/@nocobase/plugin-users/src/client/schemas/users.ts new file mode 100644 index 000000000..420778dce --- /dev/null +++ b/packages/plugins/@nocobase/plugin-users/src/client/schemas/users.ts @@ -0,0 +1,727 @@ +import { ISchema } from '@formily/react'; +import { useActionContext, useCollectionRecord, useRecord, useRequest } from '@nocobase/client'; +import { useEffect } from 'react'; + +export const userCollection = { + name: 'users', + fields: [ + { + name: 'id', + type: 'bigInt', + autoIncrement: true, + primaryKey: true, + allowNull: false, + uiSchema: { type: 'number', title: '{{t("ID")}}', 'x-component': 'InputNumber', 'x-read-pretty': true }, + interface: 'id', + }, + { + interface: 'input', + type: 'string', + name: 'nickname', + uiSchema: { + type: 'string', + title: '{{t("Nickname")}}', + 'x-component': 'Input', + }, + }, + { + interface: 'input', + type: 'string', + name: 'username', + unique: true, + uiSchema: { + type: 'string', + title: '{{t("Username")}}', + 'x-component': 'Input', + 'x-validator': { username: true }, + required: true, + }, + }, + { + interface: 'email', + type: 'string', + name: 'email', + unique: true, + uiSchema: { + type: 'string', + title: '{{t("Email")}}', + 'x-component': 'Input', + 'x-validator': 'email', + required: true, + }, + }, + { + interface: 'phone', + type: 'string', + name: 'phone', + unique: true, + uiSchema: { + type: 'string', + title: '{{t("Phone")}}', + 'x-component': 'Input', + 'x-validator': 'phone', + required: true, + }, + }, + { + interface: 'password', + type: 'password', + name: 'password', + hidden: true, + uiSchema: { + type: 'string', + title: '{{t("Password")}}', + 'x-component': 'Password', + }, + }, + { + interface: 'm2m', + type: 'belongsToMany', + name: 'roles', + target: 'roles', + foreignKey: 'userId', + otherKey: 'roleName', + onDelete: 'CASCADE', + sourceKey: 'id', + targetKey: 'name', + through: 'rolesUsers', + uiSchema: { + type: 'array', + title: '{{t("Roles")}}', + 'x-component': 'AssociationField', + 'x-component-props': { + multiple: true, + fieldNames: { + label: 'title', + value: 'name', + }, + }, + }, + }, + // { + // name: 'departments', + // type: 'belongsToMany', + // interface: 'm2m', + // target: 'departments', + // foreignKey: 'userId', + // otherKey: 'departmentId', + // onDelete: 'CASCADE', + // sourceKey: 'id', + // targetKey: 'id', + // through: 'departmentsUsers', + // uiSchema: { + // type: 'array', + // title: '{{t("Departments")}}', + // 'x-component': 'DepartmentField', + // }, + // }, + ], +}; + +export const usersSchema: ISchema = { + type: 'object', + properties: { + block1: { + type: 'void', + 'x-decorator': 'ResourceActionProvider', + 'x-decorator-props': { + collection: userCollection, + resourceName: 'users', + request: { + resource: 'users', + action: 'list', + params: { + pageSize: 50, + appends: [], + }, + }, + }, + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 16, + }, + }, + properties: { + filter: { + type: 'void', + title: '{{ t("Filter") }}', + 'x-action': 'filter', + 'x-component': 'Filter.Action', + 'x-component-props': { + icon: 'FilterOutlined', + useProps: '{{ useFilterActionProps }}', + }, + 'x-align': 'left', + }, + delete: { + type: 'void', + title: '{{ t("Delete") }}', + 'x-component': 'Action', + 'x-component-props': { + useAction: '{{ cm.useBulkDestroyAction }}', + confirm: { + title: "{{t('Delete users')}}", + content: "{{t('Are you sure you want to delete it?')}}", + }, + icon: 'DeleteOutlined', + }, + }, + create: { + type: 'void', + title: '{{t("Add new")}}', + 'x-component': 'Action', + 'x-component-props': { + type: 'primary', + icon: 'PlusOutlined', + }, + properties: { + drawer: { + type: 'void', + 'x-component': 'Action.Drawer', + 'x-decorator': 'Form', + title: '{{t("Add user")}}', + properties: { + nickname: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + }, + username: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + }, + email: { + title: '{{t("Email")}}', + 'x-component': 'Input', + 'x-validator': 'email', + 'x-decorator': 'FormItem', + required: false, + }, + phone: { + title: '{{t("Phone")}}', + 'x-component': 'Input', + 'x-validator': 'phone', + 'x-decorator': 'FormItem', + required: false, + }, + password: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + required: true, + }, + roles: { + 'x-component': 'CollectionField', + 'x-collection-field': 'users.roles', + 'x-decorator': 'FormItem', + }, + footer: { + type: 'void', + 'x-component': 'Action.Drawer.Footer', + properties: { + cancel: { + title: '{{t("Cancel")}}', + 'x-component': 'Action', + 'x-component-props': { + useAction: '{{ cm.useCancelAction }}', + }, + }, + submit: { + title: '{{t("Submit")}}', + 'x-component': 'Action', + 'x-component-props': { + type: 'primary', + useAction: '{{ cm.useCreateAction }}', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + table: { + type: 'void', + 'x-uid': 'input', + 'x-component': 'Table.Void', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + useDataSource: '{{ cm.useDataSourceFromRAC }}', + }, + properties: { + column1: { + type: 'void', + 'x-decorator': 'Table.Column.Decorator', + 'x-component': 'Table.Column', + properties: { + nickname: { + type: 'number', + 'x-component': 'CollectionField', + 'x-read-pretty': true, + }, + }, + }, + column2: { + type: 'void', + 'x-decorator': 'Table.Column.Decorator', + 'x-component': 'Table.Column', + properties: { + username: { + type: 'string', + 'x-component': 'CollectionField', + 'x-read-pretty': true, + }, + }, + }, + column3: { + type: 'void', + 'x-decorator': 'Table.Column.Decorator', + 'x-component': 'Table.Column', + properties: { + email: { + type: 'string', + 'x-component': 'CollectionField', + 'x-read-pretty': true, + }, + }, + }, + column4: { + type: 'void', + 'x-decorator': 'Table.Column.Decorator', + 'x-component': 'Table.Column', + title: '{{t("Roles")}}', + properties: { + roles: { + type: 'array', + 'x-component': 'UserRolesField', + }, + }, + }, + column5: { + type: 'void', + title: '{{t("Actions")}}', + 'x-component': 'Table.Column', + properties: { + actions: { + type: 'void', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + properties: { + update: { + type: 'void', + title: '{{t("Edit profile")}}', + 'x-decorator': 'ACLActionProvider', + 'x-acl-action': 'users:update', + 'x-component': 'Action.Link', + 'x-component-props': { + type: 'primary', + }, + properties: { + drawer: { + type: 'void', + 'x-component': 'Action.Drawer', + 'x-decorator': 'Form', + 'x-decorator-props': { + useValues: (options) => { + const record = useRecord(); + const result = useRequest(() => Promise.resolve({ data: record }), { + ...options, + manual: true, + }); + const ctx = useActionContext(); + useEffect(() => { + if (ctx.visible) { + result.run(); + } + }, [ctx.visible]); + return result; + }, + }, + title: '{{t("Edit profile")}}', + properties: { + nickname: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + }, + username: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + }, + email: { + title: '{{t("Email")}}', + 'x-component': 'Input', + 'x-validator': 'email', + 'x-decorator': 'FormItem', + required: false, + }, + phone: { + title: '{{t("Phone")}}', + 'x-component': 'Input', + 'x-validator': 'phone', + 'x-decorator': 'FormItem', + required: false, + }, + roles: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'users.roles', + }, + footer: { + type: 'void', + 'x-component': 'Action.Drawer.Footer', + properties: { + cancel: { + title: '{{t("Cancel")}}', + 'x-component': 'Action', + 'x-component-props': { + useAction: '{{ cm.useCancelAction }}', + }, + }, + submit: { + title: '{{t("Submit")}}', + 'x-component': 'Action', + 'x-component-props': { + type: 'primary', + useAction: '{{ cm.useUpdateAction }}', + }, + }, + }, + }, + }, + }, + }, + }, + changePassword: { + type: 'void', + title: '{{t("Change password")}}', + 'x-decorator': 'ACLActionProvider', + 'x-acl-action': 'users:update', + 'x-component': 'Action.Link', + 'x-component-props': { + type: 'primary', + }, + properties: { + drawer: { + type: 'void', + 'x-component': 'Action.Drawer', + 'x-decorator': 'Form', + title: '{{t("Change password")}}', + properties: { + password: { + 'x-component': 'CollectionField', + 'x-component-props': { + component: 'PasswordField', + }, + 'x-decorator': 'FormItem', + required: true, + }, + footer: { + type: 'void', + 'x-component': 'Action.Drawer.Footer', + properties: { + cancel: { + title: '{{t("Cancel")}}', + 'x-component': 'Action', + 'x-component-props': { + useAction: '{{ cm.useCancelAction }}', + }, + }, + submit: { + title: '{{t("Submit")}}', + 'x-component': 'Action', + 'x-component-props': { + type: 'primary', + useAction: '{{ cm.useUpdateAction }}', + }, + }, + }, + }, + }, + }, + }, + }, + delete: { + type: 'void', + title: '{{ t("Delete") }}', + 'x-acl-action': 'users:destroy', + 'x-action': 'destroy', + 'x-decorator': 'ACLActionProvider', + 'x-component': 'Action.Link', + 'x-component-props': { + confirm: { + title: "{{t('Delete')}}", + content: "{{t('Are you sure you want to delete it?')}}", + }, + useAction: '{{cm.useDestroyAction}}', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; + +export const roleUsersSchema: ISchema = { + type: 'void', + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 16, + }, + }, + properties: { + filter: { + type: 'void', + title: '{{ t("Filter") }}', + 'x-action': 'filter', + 'x-component': 'Filter.Action', + 'x-component-props': { + icon: 'FilterOutlined', + useProps: '{{ useFilterActionProps }}', + }, + 'x-align': 'left', + }, + actions: { + type: 'void', + 'x-component': 'Space', + properties: { + remove: { + type: 'void', + title: '{{t("Remove")}}', + 'x-component': 'Action', + 'x-component-props': { + icon: 'MinusOutlined', + confirm: { + title: "{{t('Remove')}}", + content: "{{t('Are you sure you want to remove these users?')}}", + }, + style: { + marginRight: 8, + }, + useAction: '{{ useBulkRemoveUsers }}', + }, + }, + create: { + type: 'void', + title: '{{t("Add users")}}', + 'x-component': 'Action', + 'x-component-props': { + type: 'primary', + icon: 'PlusOutlined', + }, + properties: { + drawer: { + type: 'void', + 'x-component': 'Action.Drawer', + 'x-decorator': 'FormV2', + title: '{{t("Add users")}}', + properties: { + resource: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'RoleUsersProvider', + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 16, + }, + }, + properties: { + filter: { + type: 'void', + title: '{{ t("Filter") }}', + default: { + $and: [{ username: { $includes: '' } }, { nickname: { $includes: '' } }], + }, + 'x-action': 'filter', + 'x-component': 'Filter.Action', + 'x-component-props': { + icon: 'FilterOutlined', + useProps: '{{ useFilterActionProps }}', + }, + 'x-align': 'left', + }, + }, + }, + table: { + type: 'void', + 'x-component': 'Table.Void', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + onChange: '{{ handleSelectRoleUsers }}', + }, + useDataSource: '{{ cm.useDataSourceFromRAC }}', + }, + properties: { + username: { + type: 'void', + 'x-decorator': 'Table.Column.Decorator', + 'x-component': 'Table.Column', + properties: { + username: { + type: 'string', + 'x-component': 'CollectionField', + 'x-read-pretty': true, + }, + }, + }, + nickname: { + type: 'void', + 'x-decorator': 'Table.Column.Decorator', + 'x-component': 'Table.Column', + properties: { + nickname: { + type: 'string', + 'x-component': 'CollectionField', + 'x-read-pretty': true, + }, + }, + }, + phone: { + type: 'void', + 'x-decorator': 'Table.Column.Decorator', + 'x-component': 'Table.Column', + properties: { + phone: { + type: 'string', + 'x-component': 'CollectionField', + 'x-read-pretty': true, + }, + }, + }, + email: { + type: 'void', + 'x-decorator': 'Table.Column.Decorator', + 'x-component': 'Table.Column', + properties: { + email: { + type: 'string', + 'x-component': 'CollectionField', + 'x-read-pretty': true, + }, + }, + }, + }, + }, + }, + }, + footer: { + type: 'void', + 'x-component': 'Action.Drawer.Footer', + properties: { + cancel: { + title: '{{t("Cancel")}}', + 'x-component': 'Action', + 'x-component-props': { + useAction: '{{ cm.useCancelAction }}', + }, + }, + submit: { + title: '{{t("Submit")}}', + 'x-component': 'Action', + 'x-component-props': { + type: 'primary', + useAction: '{{ useAddRoleUsers }}', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + table: { + type: 'void', + 'x-component': 'Table.Void', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + useDataSource: '{{ cm.useDataSourceFromRAC }}', + }, + properties: { + username: { + type: 'void', + title: '{{t("Username")}}', + 'x-decorator': 'Table.Column.Decorator', + 'x-component': 'Table.Column', + properties: { + username: { + type: 'string', + 'x-component': 'CollectionField', + 'x-read-pretty': true, + }, + }, + }, + nickname: { + type: 'void', + title: '{{t("Nickname")}}', + 'x-decorator': 'Table.Column.Decorator', + 'x-component': 'Table.Column', + properties: { + nickname: { + type: 'string', + 'x-component': 'CollectionField', + 'x-read-pretty': true, + }, + }, + }, + actions: { + type: 'void', + title: '{{t("Actions")}}', + 'x-component': 'Table.Column', + properties: { + actions: { + type: 'void', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + properties: { + remove: { + type: 'void', + title: '{{ t("Remove") }}', + 'x-component': 'Action.Link', + 'x-component-props': { + confirm: { + title: "{{t('Remove user')}}", + content: "{{t('Are you sure you want to remove it?')}}", + }, + useAction: '{{ useRemoveUser }}', + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/packages/plugins/@nocobase/plugin-users/src/client/utils.ts b/packages/plugins/@nocobase/plugin-users/src/client/utils.ts new file mode 100644 index 000000000..1d2e844c5 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-users/src/client/utils.ts @@ -0,0 +1,36 @@ +export const generatePassword = (length = 10) => { + const uppercaseLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const lowercaseLetters = 'abcdefghijklmnopqrstuvwxyz'; + const numbers = '0123456789'; + const specialCharacters = '!#$%^&*-_+='; + + // Combine all character sets + const allCharacters = uppercaseLetters + lowercaseLetters + numbers + specialCharacters; + + const passwordArray = new Uint32Array(length); + crypto.getRandomValues(passwordArray); + + // Ensure at least one character from each character set + let password = ''; + password += uppercaseLetters.charAt(passwordArray[0] % uppercaseLetters.length); + password += lowercaseLetters.charAt(passwordArray[1] % lowercaseLetters.length); + password += numbers.charAt(passwordArray[2] % numbers.length); + password += specialCharacters.charAt(passwordArray[3] % specialCharacters.length); + + // Fill the rest of the password with random characters + for (let i = 4; i < length; i++) { + const randomIndex = passwordArray[i] % allCharacters.length; + password += allCharacters.charAt(randomIndex); + } + + // Shuffle the password characters to ensure randomness + password = + password[0] + + password + .slice(1) + .split('') + .sort(() => Math.random() - 0.5) + .join(''); + + return password; +}; diff --git a/packages/plugins/@nocobase/plugin-users/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-users/src/locale/en-US.json new file mode 100644 index 000000000..e81f51386 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-users/src/locale/en-US.json @@ -0,0 +1,7 @@ +{ + "Users & Permissions": "Users & Permissions", + "Add users": "Add users", + "Remove user": "Remove user", + "Are you sure you want to remove it?": "Are you sure you want to remove it?", + "Random password": "Random password" +} diff --git a/packages/plugins/@nocobase/plugin-users/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-users/src/locale/zh-CN.json new file mode 100644 index 000000000..e6c567817 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-users/src/locale/zh-CN.json @@ -0,0 +1,7 @@ +{ + "Users & Permissions": "用户和权限", + "Add users": "添加用户", + "Remove user": "移除用户", + "Are you sure you want to remove it?": "你确定要移除吗?", + "Random password": "随机密码" +} diff --git a/packages/plugins/@nocobase/plugin-users/src/server/__tests__/fields.test.ts b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/fields.test.ts index 6a82e0e80..69255bb19 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/__tests__/fields.test.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/fields.test.ts @@ -1,20 +1,76 @@ -import Database from '@nocobase/database'; +import Database, { Collection as DBCollection } from '@nocobase/database'; import { createMockServer, MockServer } from '@nocobase/test'; + describe('createdBy/updatedBy', () => { let api: MockServer; let db: Database; + let Collection: DBCollection; + let Field: DBCollection; beforeEach(async () => { api = await createMockServer({ - plugins: ['acl', 'users'], + plugins: ['acl', 'users', 'collection-manager', 'error-handler', 'data-source-manager'], }); db = api.db; + + Collection = db.getCollection('collections'); + Field = db.getCollection('fields'); }); afterEach(async () => { await api.destroy(); }); + it('should add createdBy field', async () => { + await Collection.repository.create({ + values: { + name: 'a', + autoGenId: true, + timestamps: false, + title: 'A', + }, + context: {}, + }); + + const A = db.getCollection('a'); + + await Field.repository.create({ + values: { + name: 'xxxxxx', + type: 'belongsTo', + collectionName: 'a', + target: 'users', + foreignKey: 'createdById', + uiSchema: { + type: 'object', + title: '{{t("Created by")}}', + 'x-component': 'AssociationField', + 'x-component-props': { fieldNames: { value: 'id', label: 'nickname' } }, + 'x-read-pretty': true, + }, + interface: 'createdBy', + }, + context: {}, + }); + + const currentUser = await db.getCollection('users').model.create(); + + await A.repository.create({ + context: { + state: { + currentUser, + }, + }, + }); + + const p2 = await A.repository.findOne({ + appends: ['xxxxxx'], + }); + + const data = p2.toJSON(); + expect(data.xxxxxx.id).toBe(currentUser.id); + }); + describe('collection definition', () => { it('case 1', async () => { const Post = db.collection({ diff --git a/packages/plugins/@nocobase/plugin-users/src/server/actions/users.ts b/packages/plugins/@nocobase/plugin-users/src/server/actions/users.ts index b1503abf1..fa1aadc7b 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/actions/users.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/actions/users.ts @@ -1,4 +1,4 @@ -import { Context, Next } from '@nocobase/actions'; +import { Context, DEFAULT_PAGE, DEFAULT_PER_PAGE, Next } from '@nocobase/actions'; export async function updateProfile(ctx: Context, next: Next) { const { values } = ctx.action.params; @@ -14,3 +14,37 @@ export async function updateProfile(ctx: Context, next: Next) { ctx.body = result; await next(); } + +export const listExcludeRole = async (ctx: Context, next: Next) => { + const { roleName, page = DEFAULT_PAGE, pageSize = DEFAULT_PER_PAGE } = ctx.action.params; + const repo = ctx.db.getRepository('users'); + const users = await repo.find({ + fields: ['id'], + filter: { + 'roles.name': roleName, + }, + }); + const userIds = users.map((user: { id: number }) => user.id); + const [rows, count] = await repo.findAndCount({ + context: ctx, + offset: (page - 1) * pageSize, + limit: pageSize, + // Ensure memberIds is not empty to resolve strange issue: + // notIn: [] will be translated to "WHERE id IS NULL" + filter: userIds.length + ? { + id: { + $notIn: userIds, + }, + } + : {}, + }); + ctx.body = { + count, + rows, + page: Number(page), + pageSize: Number(pageSize), + totalPage: Math.ceil(count / pageSize), + }; + await next(); +}; diff --git a/packages/plugins/@nocobase/plugin-users/src/server/server.ts b/packages/plugins/@nocobase/plugin-users/src/server/server.ts index cd0781c57..359caef6e 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/server.ts @@ -4,6 +4,7 @@ import { parse } from '@nocobase/utils'; import { resolve } from 'path'; import * as actions from './actions/users'; +import { Cache } from '@nocobase/cache'; export default class PluginUsersServer extends Plugin { async beforeLoad() { @@ -26,6 +27,29 @@ export default class PluginUsersServer extends Plugin { }, }); + this.db.on('field.afterAdd', ({ collection, field }) => { + if (field.options.interface === 'createdBy') { + collection.setField('createdById', { + type: 'context', + dataType: 'bigInt', + dataIndex: 'state.currentUser.id', + createOnly: true, + visible: true, + index: true, + }); + } + + if (field.options.interface === 'updatedBy') { + collection.setField('updatedById', { + type: 'context', + dataType: 'bigInt', + dataIndex: 'state.currentUser.id', + visible: true, + index: true, + }); + } + }); + this.db.on('afterDefineCollection', (collection: Collection) => { const { createdBy, updatedBy } = collection.options; if (createdBy === true) { @@ -82,8 +106,12 @@ export default class PluginUsersServer extends Plugin { }); const loggedInActions = ['updateProfile']; - loggedInActions.forEach((action) => this.app.acl.allow('users', action, 'loggedIn')); + + this.app.acl.registerSnippet({ + name: `pm.${this.name}.*`, + actions: ['users:listExcludeRole', 'users:list'], + }); } async load() { @@ -96,6 +124,23 @@ export default class PluginUsersServer extends Plugin { plugin: this, }, }); + + this.app.resourcer.use(async (ctx, next) => { + await next(); + const { associatedName, resourceName, actionName, values } = ctx.action.params; + const cache = ctx.app.cache as Cache; + if ( + associatedName === 'roles' && + resourceName === 'users' && + ['add', 'remove', 'set'].includes(actionName) && + values?.length + ) { + // Delete cache when the members of a department changed + for (const userId of values) { + await cache.del(`roles:${userId}`); + } + } + }); } getInstallingData(options: any = {}) { diff --git a/packages/plugins/@nocobase/plugin-verification/src/client/schemas/providers.ts b/packages/plugins/@nocobase/plugin-verification/src/client/schemas/providers.ts index 6fe8953b5..d294cb8e4 100644 --- a/packages/plugins/@nocobase/plugin-verification/src/client/schemas/providers.ts +++ b/packages/plugins/@nocobase/plugin-verification/src/client/schemas/providers.ts @@ -72,7 +72,7 @@ export default { }, }, }, - 'x-component': 'CollectionProvider', + 'x-component': 'CollectionProvider_deprecated', 'x-component-props': { collection, }, diff --git a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/client/AggregateInstruction.tsx b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/client/AggregateInstruction.tsx index 11bf256a3..9ee696428 100644 --- a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/client/AggregateInstruction.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/client/AggregateInstruction.tsx @@ -8,7 +8,7 @@ import { css, useCollectionDataSource, useCollectionFilterOptions, - useCollectionManager, + useCollectionManager_deprecated, useCompile, } from '@nocobase/client'; @@ -52,7 +52,7 @@ function useAssociatedFields() { function AssociatedConfig({ value, onChange, ...props }): JSX.Element { const { setValuesIn } = useForm(); - const { getCollection } = useCollectionManager(); + const { getCollection } = useCollectionManager_deprecated(); const baseOptions = useAssociatedFields(); const [options, setOptions] = useState(baseOptions); diff --git a/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/package.json b/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/package.json index bf6e094be..fa9060116 100644 --- a/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/package.json +++ b/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/package.json @@ -32,6 +32,7 @@ }, "gitHead": "979a9c59a98c61a2287dd847580746a9b597cbde", "keywords": [ - "Workflow" + "Workflow", + "Collections" ] } diff --git a/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/DynamicCalculation.tsx b/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/DynamicCalculation.tsx index c177c278a..2e267c46f 100644 --- a/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/DynamicCalculation.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/DynamicCalculation.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { SchemaInitializerItemType, Variable, useCollectionManager } from '@nocobase/client'; +import { SchemaInitializerItemType, Variable, useCollectionManager_deprecated } from '@nocobase/client'; import { BaseTypeSets, Instruction, @@ -25,7 +25,7 @@ function useDynamicExpressionCollectionFieldMatcher(field): boolean { } function DynamicExpression({ value, onChange }) { - const { getCollectionFields, getCollection } = useCollectionManager(); + const { getCollectionFields, getCollection } = useCollectionManager_deprecated(); const scope = useWorkflowVariableOptions({ types: [useDynamicExpressionCollectionFieldMatcher.bind({ getCollectionFields, getCollection })], }); diff --git a/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/DynamicExpression.tsx b/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/DynamicExpression.tsx index 18fb22ecf..217bf661f 100644 --- a/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/DynamicExpression.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/DynamicExpression.tsx @@ -4,7 +4,7 @@ import { Tag } from 'antd'; import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useCollectionManager, useCompile, useRecord, Variable } from '@nocobase/client'; +import { useCollectionManager_deprecated, useCompile, useRecord, Variable } from '@nocobase/client'; import { getCollectionFieldOptions } from '@nocobase/plugin-workflow/client'; import { NAMESPACE } from '../locale'; @@ -19,7 +19,7 @@ const InternalExpression = observer( const collectionPath = [...basePath, 'sourceCollection'].join('.'); const [collection, setCollection] = useState(form.getValuesIn(collectionPath)); const compile = useCompile(); - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(); useFormEffects(() => { onFormInitialValuesChange((form) => { @@ -42,7 +42,7 @@ function Result(props) { const { t } = useTranslation(); const values = useRecord(); const compile = useCompile(); - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(); const options = useMemo( () => getCollectionFieldOptions({ collection: values.sourceCollection, compile, getCollectionFields }), [values.sourceCollection, values.sourceCollection], diff --git a/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/Provider.tsx b/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/Provider.tsx deleted file mode 100644 index 0777e42f6..000000000 --- a/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/Provider.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; - -import { CollectionManagerContext, useCollectionManager } from '@nocobase/client'; - -import expression from './expression'; - -export function Provider(props) { - const cmCtx = useCollectionManager(); - return ( - - {props.children} - - ); -} diff --git a/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/expression.tsx b/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/expression.tsx index b14b6201c..25ebf8fcc 100644 --- a/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/expression.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/expression.tsx @@ -1,24 +1,24 @@ -import { IField, interfacesProperties } from '@nocobase/client'; +import { CollectionFieldInterface, interfacesProperties } from '@nocobase/client'; import { NAMESPACE } from '../locale'; const { defaultProps } = interfacesProperties; -export default { - name: 'expression', - type: 'string', - group: 'advanced', - order: 1, - title: `{{t("Expression", { ns: "${NAMESPACE}" })}}`, - description: `{{t("Used to store expressions for use in workflows so that different expressions can be called for different data.", { ns: "${NAMESPACE}" })}}`, - sortable: true, - default: { +export class ExpressionFieldInterface extends CollectionFieldInterface { + name = 'expression'; + type = 'string'; + group = 'advanced'; + order = 1; + title = `{{t("Expression", { ns: "${NAMESPACE}" })}}`; + description = `{{t("Used to store expressions for use in workflows so that different expressions can be called for different data.", { ns: "${NAMESPACE}" })}}`; + sortable = true; + default = { type: 'text', uiSchema: { 'x-component': 'DynamicExpression', }, - }, - properties: { + }; + properties = { ...defaultProps, - }, -} as IField; + }; +} diff --git a/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/index.ts b/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/index.ts index 40d8104b6..3441c008a 100644 --- a/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/index.ts +++ b/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/src/client/index.ts @@ -1,9 +1,9 @@ import { Plugin } from '@nocobase/client'; import WorkflowPlugin from '@nocobase/plugin-workflow/client'; -import { Provider } from './Provider'; import DynamicCalculation from './DynamicCalculation'; import { DynamicExpression } from './DynamicExpression'; +import { ExpressionFieldInterface } from './expression'; export default class extends Plugin { async afterAdd() { @@ -14,7 +14,7 @@ export default class extends Plugin { // You can get and modify the app instance here async load() { - this.app.addProvider(Provider); + this.app.dataSourceManager.addFieldInterfaces([ExpressionFieldInterface]); this.app.addComponents({ DynamicExpression, }); diff --git a/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/client/FormTrigger.tsx b/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/client/FormTrigger.tsx index 7390d9d3b..333dd045e 100644 --- a/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/client/FormTrigger.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/client/FormTrigger.tsx @@ -1,6 +1,11 @@ import { useForm } from '@formily/react'; -import { SchemaInitializerItemType, useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client'; +import { + SchemaInitializerItemType, + useCollectionDataSource, + useCollectionManager_deprecated, + useCompile, +} from '@nocobase/client'; import { Trigger, CollectionBlockInitializer, getCollectionFieldOptions } from '@nocobase/plugin-workflow/client'; import { NAMESPACE, useLang } from '../locale'; @@ -39,7 +44,7 @@ export default class extends Trigger { 'x-component-props': { title: 'Preload associations', multiple: true, - useCollection() { + useCollection_deprecated() { const { values } = useForm(); return values?.collection; }, @@ -66,7 +71,7 @@ export default class extends Trigger { // eslint-disable-next-line react-hooks/rules-of-hooks const compile = useCompile(); // eslint-disable-next-line react-hooks/rules-of-hooks - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(); // eslint-disable-next-line react-hooks/rules-of-hooks const langTriggerData = useLang('Trigger data'); // eslint-disable-next-line react-hooks/rules-of-hooks diff --git a/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/client/__e2e__/configuration.test.ts b/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/client/__e2e__/configuration.test.ts index 33721f737..bc8dc5c62 100644 --- a/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/client/__e2e__/configuration.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/client/__e2e__/configuration.test.ts @@ -1,7 +1,5 @@ import { faker } from '@faker-js/faker'; import { - CreateWorkFlow, - EditWorkFlow, FormEventTriggerNode, WorkflowListRecords, apiCreateRecordTriggerFormEvent, @@ -87,7 +85,9 @@ test.describe('Configuration page to configure the Trigger node', () => { await page.mouse.move(300, 0); await page.getByLabel(`action-Action-Submit-submit-${triggerNodeCollectionName}-form`).hover(); await page - .getByRole('button', { name: `designer-schema-settings-Action-Action.Designer-${triggerNodeCollectionName}` }) + .getByRole('button', { + name: `designer-schema-settings-Action-actionSettings:createSubmit-${triggerNodeCollectionName}`, + }) .hover(); await page.getByRole('menuitem', { name: 'Bind workflows' }).click(); await page.getByRole('button', { name: 'plus Add workflow' }).click(); @@ -181,7 +181,9 @@ test.describe('Configuration page to configure the Trigger node', () => { await page.mouse.move(300, 0); await page.getByRole('button', { name: 'Submit to workflow' }).hover(); await page - .getByRole('button', { name: `designer-schema-settings-Action-Action.Designer-${triggerNodeCollectionName}` }) + .getByRole('button', { + name: `designer-schema-settings-Action-actionSettings:submitToWorkflow-${triggerNodeCollectionName}`, + }) .hover(); await page.getByRole('menuitem', { name: 'Bind workflows' }).click(); await page.getByRole('button', { name: 'plus Add workflow' }).click(); diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodo.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodo.tsx index b91c53601..5f28daecc 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodo.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodo.tsx @@ -1,37 +1,35 @@ -import React, { createContext, useContext, useEffect, useState } from 'react'; import { observer, useField, useFieldSchema, useForm } from '@formily/react'; import { Space, Spin, Tag } from 'antd'; import dayjs from 'dayjs'; +import React, { createContext, useContext, useEffect, useState } from 'react'; -import { css, usePlugin } from '@nocobase/client'; +import { css, useCompile, usePlugin } from '@nocobase/client'; import { - CollectionManagerProvider, SchemaComponent, SchemaComponentContext, TableBlockProvider, useAPIClient, useActionContext, - useCollectionManager, - useCompile, useCurrentUserContext, useFormBlockContext, useRecord, useTableBlockContext, + ExtendCollectionsProvider, } from '@nocobase/client'; import WorkflowPlugin, { - useAvailableUpstreams, FlowContext, - useFlowContext, JobStatusOptions, JobStatusOptionsMap, linkNodes, + useAvailableUpstreams, + useFlowContext, } from '@nocobase/plugin-workflow/client'; +import { NAMESPACE, useLang } from '../locale'; import { DetailsBlockProvider } from './instruction/DetailsBlockProvider'; import { FormBlockProvider } from './instruction/FormBlockProvider'; import { ManualFormType, manualFormTypes } from './instruction/SchemaConfig'; -import { NAMESPACE, useLang } from '../locale'; const nodeCollection = { title: `{{t("Task", { ns: "${NAMESPACE}" })}}`, @@ -258,7 +256,9 @@ export const WorkflowTodo: React.FC & { Drawer: React.FC; Decorator: React.FC } title: '{{ t("Refresh") }}', 'x-action': 'refresh', 'x-component': 'Action', - 'x-designer': 'Action.Designer', + // 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:refresh', 'x-component-props': { icon: 'ReloadOutlined', useProps: '{{ useRefreshActionProps }}', @@ -617,7 +617,6 @@ function Drawer() { } function Decorator({ params = {}, children }) { - const { collections, ...cm } = useCollectionManager(); const blockProps = { collection: 'users_jobs', resource: 'users_jobs', @@ -635,14 +634,11 @@ function Decorator({ params = {}, children }) { }; return ( - + {children} - + ); } diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodoBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodoBlockInitializer.tsx index be28c92b0..18f3bcc95 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodoBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodoBlockInitializer.tsx @@ -1,5 +1,5 @@ -import React, { FC } from 'react'; import { TableOutlined } from '@ant-design/icons'; +import React, { FC } from 'react'; import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '@nocobase/client'; @@ -16,7 +16,9 @@ export const WorkflowTodoBlockInitializer: FC = () => { 'x-decorator': 'WorkflowTodo.Decorator', 'x-decorator-props': {}, 'x-component': 'CardItem', - 'x-designer': 'TableBlockDesigner', + // 'x-designer': 'TableBlockDesigner', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', properties: { todos: { type: 'void', diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/DetailsBlockProvider.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/DetailsBlockProvider.tsx index 6b586730d..434c2fb89 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/DetailsBlockProvider.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/DetailsBlockProvider.tsx @@ -1,16 +1,17 @@ import { createForm } from '@formily/core'; import { useField } from '@formily/react'; import { - BlockRequestContext, - CollectionProvider, + BlockRequestContext_deprecated, + CollectionProvider_deprecated, FormBlockContext, RecordProvider, useAPIClient, useAssociationNames, + useBlockRequestContext, } from '@nocobase/client'; import { useFlowContext } from '@nocobase/plugin-workflow/client'; import { parse } from '@nocobase/utils/client'; -import React, { useContext, useMemo, useRef } from 'react'; +import React, { useMemo, useRef } from 'react'; function useFlowContextData(dataSource) { const { execution, nodes } = useFlowContext(); @@ -61,12 +62,12 @@ export function DetailsBlockProvider(props) { }; const api = useAPIClient(); const resource = api.resource(props.collection); - const __parent = useContext(BlockRequestContext); + const __parent = useBlockRequestContext(); return ( - - - + + + {props.children} - + - + ); } diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/FormBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/FormBlockInitializer.tsx index 5bb5ee3b8..656f3bb01 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/FormBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/FormBlockInitializer.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { - CollectionProvider, + CollectionProvider_deprecated, SchemaInitializerItem, SchemaInitializerItemType, createFormBlockSchema, @@ -69,8 +69,11 @@ function InternalFormBlockInitializer({ schema, ...others }) { export function FormBlockInitializer() { const itemConfig = useSchemaInitializerItem(); return ( - + - + ); } diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/FormBlockProvider.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/FormBlockProvider.tsx index f7bc931e3..fca9b7f0a 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/FormBlockProvider.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/FormBlockProvider.tsx @@ -1,25 +1,31 @@ import { createForm } from '@formily/core'; import { RecursionField, useField, useFieldSchema } from '@formily/react'; import { - BlockRequestContext, - CollectionProvider, + BlockRequestContext_deprecated, + CollectionManagerProvider, + CollectionProvider_deprecated, + DEFAULT_DATA_SOURCE_KEY, FormActiveFieldsProvider, FormBlockContext, FormV2, RecordProvider, useAPIClient, useAssociationNames, + useBlockRequestContext, + useDataSourceHeaders, useDesignable, useRecord, } from '@nocobase/client'; -import React, { useContext, useMemo, useRef } from 'react'; +import React, { useMemo, useRef } from 'react'; export function FormBlockProvider(props) { const userJob = useRecord(); const fieldSchema = useFieldSchema(); const field = useField(); const formBlockRef = useRef(null); - const { getAssociationAppends } = useAssociationNames(); + const dataSource = props.dataSource || DEFAULT_DATA_SOURCE_KEY; + + const { getAssociationAppends } = useAssociationNames(dataSource); const { appends, updateAssociationValues } = getAssociationAppends(); const [formKey] = Object.keys(fieldSchema.toJSON().properties ?? {}); const values = userJob?.result?.[formKey]; @@ -50,8 +56,10 @@ export function FormBlockProvider(props) { }; }, [values]); const api = useAPIClient(); - const resource = api.resource(props.collection); - const __parent = useContext(BlockRequestContext); + const headers = useDataSourceHeaders(dataSource); + + const resource = api.resource(props.collection, undefined, headers); + const __parent = useBlockRequestContext(); const formBlockValue = useMemo(() => { return { @@ -65,21 +73,25 @@ export function FormBlockProvider(props) { }, [field, form, params, service, updateAssociationValues]); return !userJob.status || values ? ( - - - - - - - -
- -
-
-
-
-
-
-
+ + + + + + + + +
+ +
+
+
+
+
+
+
+
) : null; } diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/SchemaConfig.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/SchemaConfig.tsx index cfc1681a9..3b08356f4 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/SchemaConfig.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/SchemaConfig.tsx @@ -24,8 +24,8 @@ import { VariableScopeProvider, css, gridRowColWrap, - useCollectionManager, useCompile, + useDataSourceManager, useFormActiveFields, useFormBlockContext, usePlugin, @@ -68,7 +68,7 @@ export type FormType = { export type ManualFormType = { title: string; config: { - useInitializer: ({ collections }?: { collections: any[] }) => SchemaInitializerItemType; + useInitializer: ({ allCollections }?: { allCollections: any[] }) => SchemaInitializerItemType; initializers?: { [key: string]: React.FC; }; @@ -131,7 +131,7 @@ export const addBlockButton: SchemaInitializer = new SchemaInitializer({ type: 'itemGroup', name: 'dataBlocks', title: '{{t("Data blocks")}}', - checkChildrenLength: true, + hideIfNoChildren: true, useChildren() { const workflowPlugin = usePlugin(WorkflowPlugin); const current = useNodeContext(); @@ -164,10 +164,11 @@ export const addBlockButton: SchemaInitializer = new SchemaInitializer({ name: 'form', title: '{{t("Form")}}', useChildren() { - const { collections } = useCollectionManager(); + const dm = useDataSourceManager(); + const allCollections = dm.getAllCollections(); return Array.from(manualFormTypes.getValues()).map((item: ManualFormType) => { const { useInitializer: getInitializer } = item.config; - return getInitializer({ collections }); + return getInitializer({ allCollections }); }); }, }, @@ -412,7 +413,6 @@ export function SchemaConfig({ value, onChange }) { const form = useForm(); const { workflow } = useFlowContext(); - const nodeInitializers = {}; const nodeComponents = {}; nodes.forEach((item) => { const instruction = workflowPlugin.instructions.get(item.type); diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/create.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/create.tsx index 1ae095fd8..86284b5ef 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/create.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/create.tsx @@ -7,7 +7,8 @@ import { SchemaSettingsDivider, SchemaSettingsLinkageRules, SchemaSettingsRemove, - useCollection, + useCollection_deprecated, + useGetSchemaInitializerMenuItems, useMenuSearch, } from '@nocobase/client'; @@ -18,7 +19,7 @@ import { ManualFormType } from '../SchemaConfig'; import { findSchema } from '../utils'; function CreateFormDesigner() { - const { name, title } = useCollection(); + const { name, title } = useCollection_deprecated(); return ( @@ -39,26 +40,33 @@ function CreateFormDesigner() { export default { title: `{{t("Create record form", { ns: "${NAMESPACE}" })}}`, config: { - useInitializer({ collections }) { + useInitializer({ allCollections }) { const childItems = useMemo( () => - collections.map((item) => ({ - name: _.camelCase(`createRecordForm-child-${item.name}`), - type: 'item', - title: item.title, - label: item.label, - schema: { - collection: item.name, - title: `{{t("Create record", { ns: "${NAMESPACE}" })}}`, - formType: 'create', - 'x-designer': 'CreateFormDesigner', - }, - Component: FormBlockInitializer, + allCollections.map(({ key, displayName, collections }) => ({ + key: key, + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((item) => ({ + name: _.camelCase(`createRecordForm-child-${item.name}`), + type: 'item', + title: item.title || item.tableName, + schema: { + collection: item.name, + dataSource: key, + title: `{{t("Create record", { ns: "${NAMESPACE}" })}}`, + formType: 'create', + 'x-designer': 'CreateFormDesigner', + }, + Component: FormBlockInitializer, + })), })), - [collections], + [allCollections], ); - const [isOpenSubMenu, setIsOpenSubMenu] = useState(false); - const searchedChildren = useMenuSearch(childItems, isOpenSubMenu, true); + const [openMenuKeys, setOpenMenuKeys] = useState([]); + const searchedChildren = useMenuSearch(childItems, openMenuKeys); + return { name: 'createRecordForm', key: 'createRecordForm', @@ -66,7 +74,7 @@ export default { title: `{{t("Create record form", { ns: "${NAMESPACE}" })}}`, componentProps: { onOpenChange(keys) { - setIsOpenSubMenu(keys.length > 0); + setOpenMenuKeys(keys); }, }, children: searchedChildren, diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/custom.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/custom.tsx index bde085a07..8ef4198e1 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/custom.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/custom.tsx @@ -7,8 +7,7 @@ import lodash from 'lodash'; import { ActionContextProvider, - CollectionContext, - CollectionProvider, + CollectionProvider_deprecated, FormBlockContext, RecordProvider, SchemaComponent, @@ -17,7 +16,8 @@ import { SchemaInitializerItemType, SchemaInitializerItems, gridRowColWrap, - useCollectionManager, + useCollectionManager_deprecated, + useCollection_deprecated, useRecord, useSchemaInitializer, useSchemaInitializerItem, @@ -46,13 +46,13 @@ function CustomFormBlockProvider(props) { ); return !userJob.status || values ? ( - - + - + ) : null; } @@ -183,7 +183,7 @@ function getOptions(interfaces) { } function useCommonInterfaceInitializers(): SchemaInitializerItemType[] { - const { interfaces } = useCollectionManager(); + const { interfaces } = useCollectionManager_deprecated(); const options = getOptions(interfaces); return options.map((group) => ({ @@ -206,7 +206,7 @@ const CustomItemsComponent = (props) => { const [interfaceOptions, setInterface] = useState(null); const [insert, setCallback] = useState(); const items = useCommonInterfaceInitializers(); - const collection = useContext(CollectionContext); + const collection = useCollection_deprecated(); const { setCollectionFields } = useContext(FormBlockContext); return ( @@ -297,7 +297,9 @@ const CustomItemsComponent = (props) => { field: newField, }, 'x-collection-field': `${collection.name}.${options.name}`, - 'x-designer': 'FormItem.Designer', + // 'x-designer': 'FormItem.Designer', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', }); setCallback(null); setInterface(null); @@ -332,7 +334,7 @@ function CustomFormFieldInitializer() { const itemConfig = useSchemaInitializerItem(); const { insert, setVisible } = useSchemaInitializer(); const { onAddField, setCallback } = useContext(AddCustomFormFieldButtonContext); - const { getInterface } = useCollectionManager(); + const { getInterface } = useCollectionManager_deprecated(); const interfaceOptions = getInterface(itemConfig.fieldInterface); diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/update.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/update.tsx index 479d22cf2..0db045c25 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/update.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/update.tsx @@ -10,7 +10,7 @@ import { SchemaSettingsDivider, SchemaSettingsLinkageRules, SchemaSettingsRemove, - useCollection, + useCollection_deprecated, useCollectionFilterOptions, useDesignable, useMenuSearch, @@ -23,7 +23,7 @@ import { ManualFormType } from '../SchemaConfig'; import { findSchema } from '../utils'; function UpdateFormDesigner() { - const { name, title } = useCollection(); + const { name, title } = useCollection_deprecated(); const fieldSchema = useFieldSchema(); const { t } = useTranslation(); const { dn } = useDesignable(); @@ -76,26 +76,32 @@ function UpdateFormDesigner() { export default { title: `{{t("Update record form", { ns: "${NAMESPACE}" })}}`, config: { - useInitializer({ collections }) { + useInitializer({ allCollections }) { const childItems = useMemo( () => - collections.map((item) => ({ - name: _.camelCase(`updateRecordForm-child-${item.name}`), - type: 'item', - title: item.title, - label: item.label, - schema: { - collection: item.name, - title: `{{t("Update record", { ns: "${NAMESPACE}" })}}`, - formType: 'update', - 'x-designer': 'UpdateFormDesigner', - }, - Component: FormBlockInitializer, + allCollections.map(({ key, displayName, collections }) => ({ + key: key, + name: key, + label: displayName, + type: 'subMenu', + children: collections.map((item) => ({ + name: _.camelCase(`updateRecordForm-child-${item.name}`), + type: 'item', + title: item.title || item.tableName, + schema: { + collection: item.name, + dataSource: key, + title: `{{t("Update record", { ns: "${NAMESPACE}" })}}`, + formType: 'update', + 'x-designer': 'UpdateFormDesigner', + }, + Component: FormBlockInitializer, + })), })), - [collections], + [allCollections], ); - const [isOpenSubMenu, setIsOpenSubMenu] = useState(false); - const searchedChildren = useMenuSearch(childItems, isOpenSubMenu, true); + const [openMenuKeys, setOpenMenuKeys] = useState([]); + const searchedChildren = useMenuSearch(childItems, openMenuKeys); return { name: 'updateRecordForm', key: 'updateRecordForm', @@ -103,7 +109,7 @@ export default { title: `{{t("Update record form", { ns: "${NAMESPACE}" })}}`, componentProps: { onOpenChange(keys) { - setIsOpenSubMenu(keys.length > 0); + setOpenMenuKeys(keys); }, }, children: searchedChildren, diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/index.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/index.tsx index de9031315..886f1d3f4 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/index.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/index.tsx @@ -1,4 +1,4 @@ -import { SchemaInitializerItemType, useCollectionManager, useCompile, usePlugin } from '@nocobase/client'; +import { SchemaInitializerItemType, useCollectionManager_deprecated, useCompile, usePlugin } from '@nocobase/client'; import WorkflowPlugin, { defaultFieldNames, @@ -80,7 +80,7 @@ export default class extends Instruction { // eslint-disable-next-line react-hooks/rules-of-hooks const compile = useCompile(); // eslint-disable-next-line react-hooks/rules-of-hooks - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(); const formKeys = Object.keys(config.forms ?? {}); if (!formKeys.length) { return null; @@ -120,7 +120,7 @@ export default class extends Instruction { } useInitializers(node): SchemaInitializerItemType | null { // eslint-disable-next-line react-hooks/rules-of-hooks - const { getCollection } = useCollectionManager(); + const { getCollection } = useCollectionManager_deprecated(); const formKeys = Object.keys(node.config.forms ?? {}); if (!formKeys.length || node.config.mode) { return null; diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/components/CollectionBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/components/CollectionBlockInitializer.tsx index 3cc8d7ec1..048b2a942 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/components/CollectionBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/components/CollectionBlockInitializer.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { - CollectionProvider, + CollectionProvider_deprecated, SchemaInitializerItem, SchemaInitializerItemType, - useCollectionManager, + useCollectionManager_deprecated, useRecordCollectionDataSourceItems, useSchemaInitializer, useSchemaInitializerItem, @@ -16,7 +16,7 @@ import { traverseSchema } from '../utils'; function InnerCollectionBlockInitializer({ collection, dataSource, ...props }) { const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); - const { getCollection } = useCollectionManager(); + const { getCollection } = useCollectionManager_deprecated(); const items = useRecordCollectionDataSourceItems('FormItem') as SchemaInitializerItemType[]; const resolvedCollection = getCollection(collection); @@ -69,8 +69,8 @@ function InnerCollectionBlockInitializer({ collection, dataSource, ...props }) { export function CollectionBlockInitializer() { const itemConfig = useSchemaInitializerItem(); return ( - + - + ); } diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/components/CollectionFieldset.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/components/CollectionFieldset.tsx index bbcce8976..e98aaf13c 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/components/CollectionFieldset.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/components/CollectionFieldset.tsx @@ -2,11 +2,11 @@ import { CloseCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { observer, useField, useForm } from '@formily/react'; import { CollectionField, - CollectionProvider, + CollectionProvider_deprecated, SchemaComponent, Variable, css, - useCollectionManager, + useCollectionManager_deprecated, useCompile, useToken, } from '@nocobase/client'; @@ -17,7 +17,7 @@ import { lang } from '../locale'; import { useWorkflowVariableOptions } from '../variable'; function AssociationInput(props) { - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(); const { path } = useField(); const fieldName = path.segments[path.segments.length - 1] as string; const { values: config } = useForm(); @@ -39,7 +39,7 @@ const CollectionFieldSet = observer( const { t } = useTranslation(); const compile = useCompile(); const form = useForm(); - const { getCollection, getCollectionFields } = useCollectionManager(); + const { getCollection, getCollectionFields } = useCollectionManager_deprecated(); const scope = useWorkflowVariableOptions(); const { values: config } = form; const collectionName = config?.collection; @@ -79,7 +79,7 @@ const CollectionFieldSet = observer( `} > {fields.length ? ( - + {fields .filter((field) => value && field.name in value) .map((field) => { @@ -141,7 +141,7 @@ const CollectionFieldSet = observer( ) : null} - + ) : (

{lang('Please select collection first')}

)} diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/components/FieldsSelect.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/components/FieldsSelect.tsx index 9191c86ab..358766af8 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/components/FieldsSelect.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/components/FieldsSelect.tsx @@ -2,7 +2,7 @@ import { observer, useForm } from '@formily/react'; import { Select } from 'antd'; import React from 'react'; -import { useCollectionManager, useCompile } from '@nocobase/client'; +import { useCollectionManager_deprecated, useCompile } from '@nocobase/client'; function defaultFilter() { return true; @@ -12,7 +12,7 @@ export const FieldsSelect = observer( (props: any) => { const { filter = defaultFilter, ...others } = props; const compile = useCompile(); - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(); const { values } = useForm(); const fields = getCollectionFields(values?.collection); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/components/FormTriggerWorkflowActionInitializerV2.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/components/FormTriggerWorkflowActionInitializerV2.tsx new file mode 100644 index 000000000..f88b0a22b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/components/FormTriggerWorkflowActionInitializerV2.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { BlockInitializer, useSchemaInitializerItem } from '@nocobase/client'; + +export const FormTriggerWorkflowActionInitializerV2 = () => { + const schema = { + title: '{{t("Submit to workflow", { ns: "workflow" })}}', + 'x-component': 'Action', + 'x-component-props': { + useProps: '{{ useTriggerWorkflowsActionProps }}', + }, + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:submitToWorkflow', + 'x-action-settings': { + assignedValues: {}, + skipValidator: false, + onSuccess: { + manualClose: true, + redirecting: false, + successMessage: '{{t("Submitted successfully")}}', + }, + triggerWorkflows: [], + }, + 'x-action': 'customize:triggerWorkflows', + }; + + const itemConfig = useSchemaInitializerItem(); + return ; +}; diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/components/index.ts b/packages/plugins/@nocobase/plugin-workflow/src/client/components/index.ts index bdcf67aa7..325a72fd0 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/components/index.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/components/index.ts @@ -4,3 +4,4 @@ export * from './FilterDynamicComponent'; export * from './RadioWithTooltip'; export * from './CheckboxGroupWithTooltip'; export * from './ValueBlock'; +export * from './FormTriggerWorkflowActionInitializerV2'; diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx index de36d9ebd..619f04cde 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx @@ -31,8 +31,10 @@ import CreateInstruction from './nodes/create'; import UpdateInstruction from './nodes/update'; import DestroyInstruction from './nodes/destroy'; import { useTriggerWorkflowsActionProps } from './hooks/useTriggerWorkflowActionProps'; +import { FormTriggerWorkflowActionInitializerV2 } from './components/FormTriggerWorkflowActionInitializerV2'; import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './constant'; import { NAMESPACE } from './locale'; +import { customizeSubmitToWorkflowActionSettings } from './settings/customizeSubmitToWorkflowActionSettings'; export default class PluginWorkflowClient extends Plugin { triggers = new Registry(); @@ -82,6 +84,8 @@ export default class PluginWorkflowClient extends Plugin { aclSnippet: 'pm.workflow.workflows', }); + this.app.schemaSettingsManager.add(customizeSubmitToWorkflowActionSettings); + this.registerTrigger('collection', CollectionTrigger); this.registerTrigger('schedule', ScheduleTrigger); @@ -105,6 +109,7 @@ export default class PluginWorkflowClient extends Plugin { this.app.addComponents({ WorkflowPage, ExecutionPage, + FormTriggerWorkflowActionInitializerV2, }); } diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/create.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/create.tsx index 58189bb0a..8736587a4 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/create.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/create.tsx @@ -1,4 +1,9 @@ -import { SchemaInitializerItemType, useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client'; +import { + SchemaInitializerItemType, + useCollectionDataSource, + useCollectionManager_deprecated, + useCompile, +} from '@nocobase/client'; import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer'; import CollectionFieldset from '../components/CollectionFieldset'; @@ -42,7 +47,7 @@ export default class extends Instruction { // eslint-disable-next-line react-hooks/rules-of-hooks const compile = useCompile(); // eslint-disable-next-line react-hooks/rules-of-hooks - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(); // const depth = config?.params?.appends?.length // ? config?.params?.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1) // : 0; diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/query.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/query.tsx index d02f6036c..9228a6cea 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/query.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/query.tsx @@ -4,7 +4,7 @@ import { SchemaComponentContext, SchemaInitializerItemType, useCollectionDataSource, - useCollectionManager, + useCollectionManager_deprecated, useCompile, } from '@nocobase/client'; @@ -61,7 +61,7 @@ export default class extends Instruction { useCollectionDataSource, useSortableFields() { const compile = useCompile(); - const { getCollectionFields, getInterface } = useCollectionManager(); + const { getCollectionFields, getInterface } = useCollectionManager_deprecated(); const { values } = useForm(); const fields = getCollectionFields(values.collection); return fields @@ -93,7 +93,7 @@ export default class extends Instruction { // eslint-disable-next-line react-hooks/rules-of-hooks const compile = useCompile(); // eslint-disable-next-line react-hooks/rules-of-hooks - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(); // const depth = config?.params?.appends?.length // ? config?.params?.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1) // : 0; diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/update.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/update.tsx index df5c0c290..8a0f1b606 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/update.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/update.tsx @@ -1,7 +1,7 @@ import { useField, useForm } from '@formily/react'; import React from 'react'; -import { useCollectionDataSource, useCollectionManager } from '@nocobase/client'; +import { useCollectionDataSource, useCollectionManager_deprecated } from '@nocobase/client'; import CollectionFieldset from '../components/CollectionFieldset'; import { FilterDynamicComponent } from '../components/FilterDynamicComponent'; @@ -13,7 +13,7 @@ import { isValidFilter } from '../utils'; import { Instruction } from '.'; function IndividualHooksRadioWithTooltip({ onChange, ...props }) { - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(); const form = useForm(); const { collection } = form.values; const fields = getCollectionFields(collection); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/schemas/collection.ts b/packages/plugins/@nocobase/plugin-workflow/src/client/schemas/collection.ts index 0a0e92d4a..7edf15680 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/schemas/collection.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/schemas/collection.ts @@ -174,7 +174,7 @@ export const appends = { 'x-component-props': { title: 'Preload associations', multiple: true, - useCollection() { + useCollection_deprecated() { const { values } = useForm(); return values?.collection; }, diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/schemas/executions.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/schemas/executions.tsx index 6b15e0a47..6fdcc8fda 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/schemas/executions.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/schemas/executions.tsx @@ -9,7 +9,7 @@ import { message } from 'antd'; import { getWorkflowDetailPath } from '../constant'; export const executionCollection = { - name: 'executions', + name: 'execution-executions', fields: [ { interface: 'id', diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/schemas/workflows.ts b/packages/plugins/@nocobase/plugin-workflow/src/client/schemas/workflows.ts index 896d14462..c9708fd81 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/schemas/workflows.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/schemas/workflows.ts @@ -156,7 +156,7 @@ export const workflowSchema: ISchema = { }, }, }, - 'x-component': 'CollectionProvider', + 'x-component': 'CollectionProvider_deprecated', 'x-component-props': { collection, }, diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/settings/customizeSubmitToWorkflowActionSettings.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/settings/customizeSubmitToWorkflowActionSettings.tsx new file mode 100644 index 000000000..2576ea9cc --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/settings/customizeSubmitToWorkflowActionSettings.tsx @@ -0,0 +1,60 @@ +import { useFieldSchema } from '@formily/react'; +import { isValid } from '@formily/shared'; +import { + AfterSuccess, + AssignedFieldValues, + ButtonEditor, + RemoveButton, + SchemaSettings, + SecondConFirm, + SkipValidation, + WorkflowConfig, + useSchemaToolbar, +} from '@nocobase/client'; + +export const customizeSubmitToWorkflowActionSettings = new SchemaSettings({ + name: 'actionSettings:submitToWorkflow', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + const { buttonEditorProps } = useSchemaToolbar(); + return buttonEditorProps; + }, + }, + { + name: 'secondConfirmation', + Component: SecondConFirm, + }, + { + name: 'assignFieldValues', + Component: AssignedFieldValues, + }, + { + name: 'skipRequiredValidation', + Component: SkipValidation, + }, + { + name: 'afterSuccessfulSubmission', + Component: AfterSuccess, + useVisible() { + const fieldSchema = useFieldSchema(); + return isValid(fieldSchema?.['x-action-settings']?.onSuccess); + }, + }, + { + name: 'bindWorkflow', + Component: WorkflowConfig, + }, + { + name: 'delete', + sort: 100, + Component: RemoveButton as any, + useComponentProps() { + const { removeButtonProps } = useSchemaToolbar(); + return removeButtonProps; + }, + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/collection.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/collection.tsx index 490c828d6..a558899db 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/collection.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/collection.tsx @@ -1,4 +1,9 @@ -import { SchemaInitializerItemType, useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client'; +import { + SchemaInitializerItemType, + useCollectionDataSource, + useCollectionManager_deprecated, + useCompile, +} from '@nocobase/client'; import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer'; import { FieldsSelect } from '../components/FieldsSelect'; import { NAMESPACE, lang } from '../locale'; @@ -150,7 +155,7 @@ export default class extends Trigger { // eslint-disable-next-line react-hooks/rules-of-hooks const compile = useCompile(); // eslint-disable-next-line react-hooks/rules-of-hooks - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(); const rootFields = [ { collectionName: config.collection, diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/schedule/index.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/schedule/index.tsx index 9c1547d0b..092485596 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/schedule/index.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/schedule/index.tsx @@ -1,4 +1,9 @@ -import { SchemaInitializerItemType, useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client'; +import { + SchemaInitializerItemType, + useCollectionDataSource, + useCollectionManager_deprecated, + useCompile, +} from '@nocobase/client'; import { CollectionBlockInitializer } from '../../components/CollectionBlockInitializer'; import { NAMESPACE, lang } from '../../locale'; @@ -28,7 +33,7 @@ export default class extends Trigger { // eslint-disable-next-line react-hooks/rules-of-hooks const compile = useCompile(); // eslint-disable-next-line react-hooks/rules-of-hooks - const { getCollectionFields } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager_deprecated(); const options: any[] = []; if (!opts?.types || opts.types.includes('date')) { options.push({ key: 'date', value: 'date', label: lang('Trigger time') }); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20230612021134-manual-collection-block.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20230612021134-manual-collection-block.ts index 6b29e8cb9..dfee79936 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20230612021134-manual-collection-block.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/migrations/20230612021134-manual-collection-block.ts @@ -27,7 +27,7 @@ function migrateSchema(schema = {}): object { return ( item['x-component'] === 'CardItem' && item['x-designer'] === 'SimpleDesigner' && - item['x-decorator'] === 'CollectionProvider' + item['x-decorator'] === 'CollectionProvider_deprecated' ); }); diff --git a/packages/presets/nocobase/package.json b/packages/presets/nocobase/package.json index 96a57e54f..5c3f38f53 100644 --- a/packages/presets/nocobase/package.json +++ b/packages/presets/nocobase/package.json @@ -23,6 +23,7 @@ "@nocobase/plugin-collection-manager": "0.19.0-alpha.9", "@nocobase/plugin-custom-request": "0.19.0-alpha.9", "@nocobase/plugin-data-visualization": "0.19.0-alpha.9", + "@nocobase/plugin-data-source-manager": "0.19.0-alpha.9", "@nocobase/plugin-error-handler": "0.19.0-alpha.9", "@nocobase/plugin-export": "0.19.0-alpha.9", "@nocobase/plugin-file-manager": "0.19.0-alpha.9", diff --git a/packages/presets/nocobase/src/server/index.ts b/packages/presets/nocobase/src/server/index.ts index 41a7d5566..234009d46 100644 --- a/packages/presets/nocobase/src/server/index.ts +++ b/packages/presets/nocobase/src/server/index.ts @@ -3,6 +3,7 @@ import _ from 'lodash'; export class PresetNocoBase extends Plugin { builtInPlugins = [ + 'data-source-manager', 'error-handler', 'collection-manager', 'ui-schema-storage', @@ -169,10 +170,11 @@ export class PresetNocoBase extends Plugin { async install() { await this.createIfNotExists(); - this.log.info('install built-in plugins'); + this.log.info('start install built-in plugins'); await this.pm.repository.init(); await this.pm.load(); await this.pm.install(); + this.log.info('finish install built-in plugins'); } async upgrade() { diff --git a/yarn.lock b/yarn.lock index 01a558f2c..5110e39b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,15 @@ resolved "https://registry.npmmirror.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== +"@ahooksjs/use-url-state@3.5.1": + version "3.5.1" + resolved "https://registry.yarnpkg.com/@ahooksjs/use-url-state/-/use-url-state-3.5.1.tgz#c3ad04e98cbcbc8f9eba476bcbd3237e9809aa5b" + integrity sha512-XTrOLZKOAXahDD1Evg+aSN6qNzoh/FuvRKbUtB/0RhYvz57tyXRPbED0KXK4h2C3ZyHUKBJcVCSDcd6EsTyMyQ== + dependencies: + ahooks "^3.4.1" + query-string "^6.9.0" + tslib "^2.4.1" + "@alicloud/credentials@^2": version "2.3.0" resolved "https://registry.npmmirror.com/@alicloud/credentials/-/credentials-2.3.0.tgz#941233a07ba74cd2fdaa3f6a5d2a3cca5a10c184" @@ -4578,12 +4587,12 @@ picocolors "^1.0.0" tslib "^2.6.0" -"@playwright/test@^1.40.1": - version "1.40.1" - resolved "https://registry.npmmirror.com/@playwright/test/-/test-1.40.1.tgz#9e66322d97b1d74b9f8718bacab15080f24cde65" - integrity sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw== +"@playwright/test@^1.42.1": + version "1.42.1" + resolved "https://registry.npmmirror.com/@playwright/test/-/test-1.42.1.tgz#9eff7417bcaa770e9e9a00439e078284b301f31c" + integrity sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ== dependencies: - playwright "1.40.1" + playwright "1.42.1" "@pm2/agent@~2.0.0": version "2.0.3" @@ -7444,7 +7453,7 @@ ahooks-v3-count@^1.0.0: resolved "https://registry.npmmirror.com/ahooks-v3-count/-/ahooks-v3-count-1.0.0.tgz#ddeb392e009ad6e748905b3cbf63a9fd8262ca80" integrity sha512-V7uUvAwnimu6eh/PED4mCDjE7tokeZQLKlxg9lCTMPhN+NjsSbtdacByVlR1oluXQzD3MOw55wylDmQo4+S9ZQ== -ahooks@3.x, ahooks@^3.7.2, ahooks@^3.7.6: +ahooks@3.x, ahooks@^3.4.1, ahooks@^3.7.2, ahooks@^3.7.6: version "3.7.8" resolved "https://registry.npmmirror.com/ahooks/-/ahooks-3.7.8.tgz#3fa3c491cd153e884a32b0c4192fc72cf84c4332" integrity sha512-e/NMlQWoCjaUtncNFIZk3FG1ImSkV/JhScQSkTqnftakRwdfZWSw6zzoWSG9OMYqPNs2MguDYBUFFC6THelWXA== @@ -20020,17 +20029,17 @@ platform@^1.3.1: resolved "https://registry.npmmirror.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== -playwright-core@1.40.1: - version "1.40.1" - resolved "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.40.1.tgz#442d15e86866a87d90d07af528e0afabe4c75c05" - integrity sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ== +playwright-core@1.42.1: + version "1.42.1" + resolved "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.42.1.tgz#13c150b93c940a3280ab1d3fbc945bc855c9459e" + integrity sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA== -playwright@1.40.1: - version "1.40.1" - resolved "https://registry.npmmirror.com/playwright/-/playwright-1.40.1.tgz#a11bf8dca15be5a194851dbbf3df235b9f53d7ae" - integrity sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw== +playwright@1.42.1: + version "1.42.1" + resolved "https://registry.npmmirror.com/playwright/-/playwright-1.42.1.tgz#79c828b51fe3830211137550542426111dc8239f" + integrity sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg== dependencies: - playwright-core "1.40.1" + playwright-core "1.42.1" optionalDependencies: fsevents "2.3.2" @@ -20809,7 +20818,7 @@ qs@~6.5.2: resolved "https://registry.npmmirror.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== -query-string@^6.13.6, query-string@^6.13.8: +query-string@^6.13.6, query-string@^6.13.8, query-string@^6.9.0: version "6.14.1" resolved "https://registry.npmmirror.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==