feat: support for multiple data sources (#3418)

* refactor: collectionName display with tablePrefix

* fix: bug

* fix: schema toolbar no ddata source (T-3182)

* fix: unit test bug

* fix: useAssociationNames support data source

* chore(RecordProvider_deprecated): add collectionName

* fix: deprecated

* refactor: default value

* refactor: default value

* fix: fastRefresh=false

* style: fix action link style (T-3228)

* fix: should not diaplay Save mode for some Action (T-3217)

* chore: remove group title (T-3194)

* fix: extend collections bug

* chore: transaction

* fix: filter block only current data source (T-3226)

* fix: fix filter block in drawer (T-3224)

* fix: avoid error when editing field (T-3232)

* fix: primary key name in postgres

* chore: test

* chore: test

* refactor: forgin key support select and input

* fix: doc bug

* fix: change duplllicte divier name

* feat: throughScope

* fix: bug

* refactor: local improve

* fix: fix parent record of Add child in tree table (T-3235)

* fix: block template filter by dataSource(T-3234)

* chore: change table primary key

* refactor: index for primarykey & unique

* chore: test

* fix: should not display filter blocks option if no association field (T-3242)

* fix: dataSourceKey

* refactor: sourcekey & forginkey & targetkey limit type

* fix: bug

* chore: test

* fix: upload action

* fix: unit test

* fix: useSourceIdFromParentRecord

* fix: permissions

* fix: oho association field should has default fieldnames

* fix: useSourceIdFromParentRecord

* fix: tableSelectorProvider collection undefined

* fix: bug

* chore: validate association keys

* fix: apply mixin bug

* fix: getPrimaryKey

* fix: bug T-3253

* fix:  collection unit test

* chore: validate association keys

* fix: create collection

* fix: getCollection in TableBlockProvider

* refactor: association key in data source manager

* fix: improve doc

* fix(relationshipBlocks): fix sourceId (T-3257,T-3264)

* fix: plugin acl test

* chore: correct field options

* fix: dataScope resource

* fix: improve doc

* fix: appVersion = '<0.20.0-alpha.1'

* refactor: fieldNames

* refactor: primarykey & unique & autoIncrement shuld not support edit in third dataSource

* fix: bug

* fix: gantt block params tree

* fix: style

* fix: wording & icon

* fix: bug

* fix: roles cache

* refactor: calender & express & file collection support preset fields

* fix: decode uri

* refactor: migrate files [wip] (#3584)

* refactor: migrate blockSettings:table

* refactor: migrate fieldSettings:TableColumn

* refactor: migrate TableBlockInitializer

* fix: fix import path

* refactor: migrate TableActionInitailizers

* refactor: migrate TableColumnInitializers

* refactor: migrate TableActionColumnInitializers

* refactor: migrate TableColumnSchemaToolbar

* refactor: migrate TableSelectorInitializer

* refactor: migrate blockSettings:tableSelector

* refactor(tableSelector): migrate e2e

* refactor(form): migrate e2e

* refactor: migrate FormBlockInitializer

* refactor: migrate CreateFormBlockInitializer

* refactor: migrate RecordFormBlockInitializer

* refactor: migrate blockSettings:createForm

* refactor: rename file name

* refactor: migrate blockSettings:editForm

* refactor: migrate FormActionInitailizers

* refactor: move to a new file

* refactor: migrate formItemInitializers

* refactor: migrate FormItemSchemaToolbar

* refactor: migrate fieldSettings:FormItem

* chore: fix build

* fix: fix weird path error

* fix: rename formActionInitializers

* fix: create collection field

* refactor: throughCollection

* fix: datasources get permission

* fix: throughCollection

* fix: throughCollection

* fix: register initializer components

* refactor: targetkey & source key must be unique

* refactor: targetkey & source key must be unique index

* fix(customRequest): avoid error when clicking button

* chore: error message when add multiple primary keys

* fix: target key in hasMany

* fix: default value should not support edit in outside dataSource

* fix: test

* fix: update associations (#3586)

* fix: source key

* fix: addAccessor

* fix: updateAssociations

* fix: bugs

* fix: remove test.only

* refactor: migrate RecordReadPrettyFormBlockInitializer

* refactor: migrate singleDataDetailsBlockSettings

* fix(users): filter bug

* refactor: migrate readPrettyFormActionInitializers

* refactor: migrate readPrettyFormItemInitializers

* refactor: migrate DetailsBlockInitializer

* refactor: migrate multiDataDetailsBlockSettings

* feat: validate association key pairs

* chore: default title

* refactor: migrate detailsActionInitializers

* refactor: migrate e2e

* refactor: migrate ListBlockInitializer

* refactor: migrate listBlockSettings

* refactor: migrate listActionInitializers

* refactor: migrate listItemActionInitializers

* fix: create collection

* fix: remove fieldsHistoryRepository.createMany

* test(e2e): fix error message for roles.name

* fix: sync indexes in postgres

* chore: test

* test: acl test

* test(e2e): fix sort error

* refactor: remove useless code

* test: kanban e2e

* fix: load user

* fix: test

* test: fix unit tests

* fix: db.sync

* test: updateRole

* fix: test

* fix: settings and initializer performance improve

* fix: update role resources

* fix: add block

* fix: fix T-3308

* test: fix e2e

* test(e2e): skip fix block

* chore: skip test in sqlite

* fix: change initializer menu key

* test(collectionManager): fix e2e

* refactor: sort field availableTypes

* fix: client core performance optimization

* refactor(GridCard): migrate e2e

* refactor: migrate GridCard

* fix: bug

* refactor: migrate utils

* refactor: migrate filter-form

* fix: change Record to CollectionRecord

* chore: acl migration

* chore: acl migration

* chore: migration of acl

* refactor: migrate Collapse

* chore: error message

* fix: update associations

* chore: update collection search to be case-insensitive

* refactor: migrate Markdown

* fix(WorkflowTodos): x-toolbar typo

* feat: admin change password

* feat: check foreign key && target key value in update associations

* chore: dataSource permission

* refactor: dataSource permission

* fix: acl support data source permission

* fix: fix T-3307

* chore: test

* refactor: locale improve

* chore: locale

* chore: sqlite test config

* chore: create user with roles test

* chore: test

* test: fix mock data to avoid duplication

* chore: test

* fix: load table with tablePrefix

* chore: move action in datasource

* chore: number field to sort field type

* test: optimize dropdown

* chore: upgrade @playwright/test to v1.42.1

* fix: fix invalid path for Windows

* test: fix e2e

* chore: kanban Sort field

* fix: kanban

* fix: kanban

* refactor: create sort in kanban

* refactor: create sort field in kanban

* refactor: locale improve

* refactor: locale improve

* fix: sync with null default value

* refactor: collectionFieldInterfaceSelect

* fix: move action

* fix: update associations

* fix: test case

* chore: test

* test: optimize e2e

* feat: remvoe Duplicate for single details block (T-3195)

* fix(fieldNames): should use primaryKey as default value (T-3322, T-3319)

* fix: use filterTargetKey as fieldNNames.value

* test: fix e2e

* test: fix e2e

* test(kanban): fix e2e

* test(blockTemplate): should clear template at end of test

* refactor: migrate fields

* refactor: migrate actions

* refactor: migrate menu

* refactor: migrate page

* refactor(SchemaSettings): unify naming style

* fix: scopeKeyOptions undefined

* refactor(SchemaInitializers): unify naming stle

* fix(bi): chart filter fields

* chore: acl snippets

* refactor: replace CreateFormBlockInitializers to blockInitializers:createForm

* refactor: replace to blockInitializers:customizeCreateForm

* refactor: replace block intializers name

* refactor: replace action initializers name

* refactor: replace field initializers name

* style: fix hover style for column action (T-3297)

* refactor: revert some codes

* chore: update comment

* fix: revert record deprected

* fix: remove pro-plugins

* fix: bug

* chore: replace iframeBlockSchemaSettings to blockSettings:iframe

* Revert "refactor: revert some codes"

This reverts commit 991021ceaeecc5d27113a51e501a4abd439edcd2.

* Revert "refactor: replace field initializers name"

This reverts commit b47b808d06305741b56302e3dad1dd256658fad4.

* Revert "refactor: replace action initializers name"

This reverts commit eab1b6e3d986d1c3dc80d75fa6230fa948e3a33e.

* Revert "refactor: replace block intializers name"

This reverts commit 50ab9da177f344d037184a17746cb1d0e037a826.

* Revert "refactor: replace to blockInitializers:customizeCreateForm"

This reverts commit 77b9f59bb14d944fd8c42006e899861196589748.

* Revert "refactor: replace CreateFormBlockInitializers to blockInitializers:createForm"

This reverts commit e9a38b0b4d9fabc571b7d9cdc8929914f5e2a367.

* Revert "refactor(SchemaInitializers): unify naming stle"

This reverts commit 542390899fa84d212a8dbbe7f77e0f19befa6ae8.

* Revert "refactor(SchemaSettings): unify naming style"

This reverts commit 8566735922c4a157efccdb3830deaedeb08c6f6a.

* Revert "chore: replace iframeBlockSchemaSettings to blockSettings:iframe"

This reverts commit 884f6df92fdc860a50500025f132904e9528002f.

* refactor: create sorting field in kanban

* refactor: create sorting field in kanban

* fix: style

* fix: bug

* fix(SideMenu): fix the problem of invalid add menu (T-3331)

* fix: translation

* feat: client en-US docs

---------

Co-authored-by: xilesun <2013xile@gmail.com>
Co-authored-by: dream2023 <1098626505@qq.com>
Co-authored-by: Zeke Zhang <958414905@qq.com>
Co-authored-by: chenos <chenlinxh@gmail.com>
Co-authored-by: Chareice <chareice@live.com>
This commit is contained in:
katherinehhh 2024-03-03 23:06:24 +08:00 committed by GitHub
parent 41a8344be9
commit af6113c8ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1211 changed files with 59358 additions and 10253 deletions

101
.github/workflows/build-pro-image.yml vendored Normal file
View File

@ -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\"
}"

View File

@ -8,7 +8,6 @@ on:
push:
branches:
- main
- develop
paths:
- 'package.json'
- 'packages/core/acl/**'

View File

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

23
.vscode/launch.json vendored
View File

@ -26,25 +26,12 @@
"request": "launch",
"name": "Debug Tests",
"runtimeExecutable": "yarn",
"runtimeArgs": ["run", "--inspect-brk", "test", "${file}"],
"skipFiles": ["<node_internals>/**"],
"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_internals>/**", "**/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": ["<node_internals>/**"],
"runtimeArgs": ["e2e", "test", "${file}"],
"skipFiles": ["<node_internals>/**", "**/node_modules/**", "**/dist/**", "**/lib/**", "**/es/**"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"windows": {
"runtimeArgs": ["test:e2e", "${fileBasename}"]
"runtimeArgs": ["e2e", "test", "${fileBasename}"]
}
},
{

View File

@ -8,6 +8,7 @@ NocoBase is in early stage of development and is subject to frequent changes, pl
## Recent major updates
- [v0.20Support 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/)

View File

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

View File

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

View File

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

View File

@ -186,6 +186,7 @@ export class ACLRole {
const resource = this.resources.get(resourceName);
let action = null;
if (resource) {
action = resource.getAction(actionName);
}

View File

@ -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[];

View File

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

View File

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

View File

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

View File

@ -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'));
}

View File

@ -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<string, string> = {
env: {
...process.env,
...envs,
NODE_ENV: 'production',
sourcemap: process.argv.includes('--sourcemap') ? 'sourcemap' : undefined,
NODE_ENV: process.env.NODE_ENV || 'production',
},
});
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["**/*"]
}

View File

@ -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': [
// {

View File

@ -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 {
- schemaSettingsSchema 设置工具,具体参考:[SchemaSettingsManager](/core/ui-schema/schema-initializer-manager)
- schemaInitializersSchema 添加工具,具体参考:[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()

View File

@ -34,6 +34,10 @@ class Plugin<T = any> {
return this.app.schemaSettingsManager;
}
get dataSourceManager() {
return this.app.dataSourceManager;
}
async afterAdd() {}
async beforeLoad() {}

View File

@ -0,0 +1,259 @@
# CollectionRecordProvider
用于提供 [CollectionRecord](./collection-record) 实例。
## 组件
- 类型
```tsx | pure
interface CollectionRecordProviderProps<DataType = {}, ParentDataType = {}> {
isNew?: boolean;
record?: CollectionRecord<DataType, ParentDataType> | DataType;
parentRecord?: CollectionRecord<ParentDataType> | 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' } });
<CollectionRecordProvider record={record} />
// 最终向子组件传递的数据为: 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 <pre>{JSON.stringify(record, null, 2)}</pre>;
}
export default () => <CollectionRecordProvider record={record}><Demo /></CollectionRecordProvider>
```
- record 参数为普通对象
```tsx | pure
<CollectionRecordProvider record={{ id: 1, name: 'foo' }} />
// 最终向子组件传递的数据为: const record = new CollectionRecord({ data: { id: 1, name: 'foo' } });
```
```tsx
import { useCollectionRecord, CollectionRecordProvider } from '@nocobase/client';
const Demo = () => {
const record = useCollectionRecord();
return <pre>{JSON.stringify(record, null, 2)}</pre>;
}
export default () => <CollectionRecordProvider record={{ id: 1, name: 'foo' }}><Demo /></CollectionRecordProvider>
```
- record 参数为 CollectionRecord 实例且带有父记录
```tsx | pure
const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } });
const record = new CollectionRecord({ data: { id: 1, name: 'foo' }, parentRecord });
<CollectionRecordProvider record={record} />
// 最终向子组件传递的数据为: 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 <pre>{JSON.stringify(record, null, 2)}</pre>;
}
export default () => <CollectionRecordProvider record={record}><Demo /></CollectionRecordProvider>
```
- record 参数为 CollectionRecord 实例,父记录通过 `parentRecord` 参数传递
```tsx | pure
const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } });
const record = new CollectionRecord({ data: { id: 1, name: 'foo' } });
<CollectionRecordProvider record={record} parentRecord={parentRecord} />
// 首先设置父记录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 <pre>{JSON.stringify(record, null, 2)}</pre>;
}
export default () => <CollectionRecordProvider record={record} parentRecord={parentRecord}><Demo /></CollectionRecordProvider>
```
- record 参数为普通对象,父记录也是普通对象
```tsx | pure
<CollectionRecordProvider record={{ id: 1, name: 'foo' }} parentRecord={{ id: 1, role: 'admin' }} />
// 首先实例化父记录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 <pre>{JSON.stringify(record, null, 2)}</pre>;
}
export default () => <CollectionRecordProvider record={{ id: 1, name: 'foo' }} parentRecord={{ id: 1, role: 'admin' }} ><Demo /></CollectionRecordProvider>
```
## 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 <div>
<div>record: <pre>{JSON.stringify(record, null, 2)}</pre></div>
<div style={{ marginTop: 10 }}>record.data: <pre>{JSON.stringify(record.data, null, 2)}</pre></div>
<div style={{ marginTop: 10 }}>record.parentRecord: <pre>{JSON.stringify(record.parentRecord, null, 2)}</pre></div>
<div style={{ marginTop: 10 }}>record.parentRecord.data: <pre>{JSON.stringify(record.parentRecord.data, null, 2)}</pre></div>
</div>;
}
export default () => <CollectionRecordProvider record={{ id: 1, name: 'foo' }} parentRecord={{ id: 1, role: 'admin' }} ><Demo /></CollectionRecordProvider>
```
### 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 <div>
<div>data === record.data: { JSON.stringify(data === record.data) }</div>
<div style={{ marginTop: 10 }}>data: <pre>{JSON.stringify(data, null, 2)}</pre></div>
<div style={{ marginTop: 10 }}>record.data: <pre>{JSON.stringify(record.data, null, 2)}</pre></div>
</div>;
}
export default () => <CollectionRecordProvider record={{ id: 1, name: 'foo' }} ><Demo /></CollectionRecordProvider>
```
### 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 <div>
<div>parentRecord === record.parentRecord: { JSON.stringify(parentRecord === record.parentRecord) }</div>
<div style={{ marginTop: 10 }}>parentRecord: <pre>{JSON.stringify(parentRecord, null, 2)}</pre></div>
<div style={{ marginTop: 10 }}>record.parentRecord: <pre>{JSON.stringify(record.parentRecord, null, 2)}</pre></div>
</div>;
}
export default () => <CollectionRecordProvider record={{ id: 1, name: 'foo' }} parentRecord={{ id: 1, role: 'admin' }} ><Demo /></CollectionRecordProvider>
```
### 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 <div>
<div style={{ marginTop: 10 }}>parentData: <pre>{JSON.stringify(parentData, null, 2)}</pre></div>
<div style={{ marginTop: 10 }}>parentRecord.data: <pre>{JSON.stringify(parentRecord.data, null, 2)}</pre></div>
<div style={{ marginTop: 10 }}>record.parentRecord.data: <pre>{JSON.stringify(record.parentRecord.data, null, 2)}</pre></div>
</div>;
}
export default () => <CollectionRecordProvider record={{ id: 1, name: 'foo' }} parentRecord={{ id: 1, role: 'admin' }} ><Demo /></CollectionRecordProvider>
```

View File

@ -0,0 +1,194 @@
# CollectionRecord
数据表中的一条记录。
## 类型
```tsx | pure
interface CollectionRecordOptions<DataType = {}, ParentDataType = {}> {
isNew?: boolean;
data?: DataType;
parentRecord?: CollectionRecord<ParentDataType>;
/**
* 当前记录所属的 collection name
*/
collectionName?: string;
}
class CollectionRecord<DataType = {}, ParentDataType = {}> {
public isNew?: boolean;
public data?: DataType;
public parentRecord?: CollectionRecord<ParentDataType>;
public collectionName?: string;
constructor(options: CollectionRecordOptions<DataType, ParentDataType>) {}
setData(data: DataType) {
this.data = data;
}
setParentRecord(parentRecord: CollectionRecord<ParentDataType>) {
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);
```

View File

@ -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 <DataBlockContext.Provider>
<CollectionDataSourceProvider>
<CollectionProvider> / <AssociationProvider>
<BlockResourceProvider>
<BlockRequestProvider>
{action !== 'list' && <CollectionRecordProvider record={blocRequest.data}>
{props.children}
</Record>}
</BlockRequestProvider>
</BlockResourceProvider> / </AssociationProvider>
</CollectionProvider>
</CollectionDataSourceProvider>
</DataBlockContext.Provider>
}
```
上述组件封装到 `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',
}
```
### 完整示例
<code src="./demos/data-block-provider/complete-demo.tsx"></code>
## 属性
### 静态属性和动态属性
- 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<string, any>;
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 <DataBlockContext.Provider value={props}>
<CollectionDataSourceProvider>
<CollectionProvider name={props.collection}> / <CollectionProvider name={props.association}>
<BlockResourceProvider {...props}>
<BlockRequestProvider resource={resource}>
{action !== 'list' && <CollectionRecordProvider record={blocRequest.data}>
{props.children}
</Record>}
</BlockRequestProvider>
</BlockResourceProvider>
</CollectionProvider>
</CollectionDataSourceProvider>
</DataBlock.Provider>
}
```
### 属性组合和场景
这些属性根据不同的场景,共有 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<T extends {} = {}> {
props: AllDataBlockProps & T;
dn: Designable;
}
const useDataBlock: <T extends {}>() => Result<T>
```
- 详解
`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
<code src="./demos/data-block-provider/collection-table-list.tsx"></code>
#### Form get & update
<code src="./demos/data-block-provider/collection-form-get-and-update.tsx"></code>
#### Form create
<code src="./demos/data-block-provider/collection-form-create.tsx"></code>
#### Form record & update
<code src="./demos/data-block-provider/collection-form-record-and-update.tsx"></code>
### association
association 与 collection 类似,只是需要提供 `sourceId`,我们以 `Table list` 为例。
#### Table list & sourceId
<code src="./demos/data-block-provider/association-table-list-and-source-id.tsx"></code>
#### Table list & parentRecord
如果不提供 `sourceId`,则需要提供 `parentRecord`,我们以 `Table list` 为例。
<code src="./demos/data-block-provider/association-table-list-and-parent-record.tsx"></code>

View File

@ -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 (
<Table
dataSource={data?.data || []}
loading={loading}
pagination={{
total: data?.meta.total,
pageSize: data?.meta.pageSize,
page: data?.meta.page,
}}
/>
)
}
```
## 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` 对象,需要自己通过 `<CollectionRecordProvider />` 设置上下文。
```tsx | pure
const MyTable = () => {
const { data } = useDataBlockRequest();
return (
<Table
dataSource={data?.data || []}
columns={[
{
title: 'ID',
dataIndex: 'id',
},
{
title: 'Action',
render: (v, record) => {
return (
<CollectionRecordProvider record={record}>
<MyAction />
</CollectionRecordProvider>
)
},
},
]}
pagination={{
total: data?.meta.total,
pageSize: data?.meta.pageSize,
page: data?.meta.page,
}}
/>
)
}
const MyAction = () => {
const record = useCollectionRecord();
return (
<Button onClick={() => {
console.log(record.data);
}}>Dialog</Button>
)
}
```

View File

@ -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();
}
```
可运行示例

View File

@ -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<any> {
const { data, loading } = useDataBlockRequest<any[]>();
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 <SchemaComponent schema={schema}></SchemaComponent>;
};
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;

View File

@ -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<any> {
const { data, loading } = useDataBlockRequest<any[]>();
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 (
<>
<Select
defaultValue={state.userId}
options={[
{ key: 1, value: '1', label: 'Tom' },
{ key: 2, value: '2', label: 'Jack' },
]}
onChange={(v) => {
setState({ userId: v });
}}
></Select>
<SchemaComponent schema={schema}></SchemaComponent>
</>
);
};
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;

View File

@ -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<DemoFormFieldType>;
const DemoForm: FC<DemoFormProps> = withDynamicSchemaProps((props) => {
return (
<Form labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} style={{ maxWidth: 600 }} autoComplete="off" {...props}>
<Form.Item<DemoFormFieldType>
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input />
</Form.Item>
<Form.Item<DemoFormFieldType>
label="Age"
name="age"
rules={[{ required: true, message: 'Please input your age!' }]}
>
<InputNumber />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
});
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 <SchemaComponent schema={schema}></SchemaComponent>;
};
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;

View File

@ -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<DemoFormFieldType>;
const DemoForm: FC<DemoFormProps> = withDynamicSchemaProps((props) => {
return (
<Form labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} style={{ maxWidth: 600 }} autoComplete="off" {...props}>
<Form.Item<DemoFormFieldType>
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input />
</Form.Item>
<Form.Item<DemoFormFieldType>
label="Age"
name="age"
rules={[{ required: true, message: 'Please input your age!' }]}
>
<InputNumber />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
});
function useDemoFormProps(): DemoFormProps {
const data = useCollectionRecordData<DemoFormFieldType>();
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 (
<>
<Select
defaultValue={state.id}
options={[
{ key: 1, value: '1', label: 'Bamboo' },
{ key: 2, value: '2', label: 'Mary' },
]}
onChange={(v) => {
setState({ id: v });
}}
></Select>
<SchemaComponent schema={schema}></SchemaComponent>
</>
);
};
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;

View File

@ -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<DemoFormFieldType>;
const DemoForm: FC<DemoFormProps> = withDynamicSchemaProps((props) => {
return (
<Form labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} style={{ maxWidth: 600 }} autoComplete="off" {...props}>
<Form.Item<DemoFormFieldType>
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input />
</Form.Item>
<Form.Item<DemoFormFieldType>
label="Age"
name="age"
rules={[{ required: true, message: 'Please input your age!' }]}
>
<InputNumber />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
});
function useDemoFormProps(): DemoFormProps {
const data = useCollectionRecordData<DemoFormFieldType>();
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 (
<>
<CollectionRecordProvider record={recordData}>
<SchemaComponent schema={schema}></SchemaComponent>
</CollectionRecordProvider>
</>
);
};
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;

View File

@ -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<any> {
const { data, loading } = useDataBlockRequest<any[]>();
return {
loading,
dataSource: data?.data || [],
columns: [
{
title: 'UserName',
dataIndex: 'username',
},
{
title: 'NickName',
dataIndex: 'nickname',
},
{
title: 'Email',
dataIndex: 'email',
},
],
};
}
const Demo = () => {
return <SchemaComponent schema={schema}></SchemaComponent>;
};
const Root = createApp(Demo, {
components: { MyTable },
scopes: { useTableProps },
});
export default Root;

View File

@ -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<any> {
const { tableProps } = useDataBlockProps();
const { data, loading } = useDataBlockRequest<any[]>();
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 <SchemaInitializerItem title={'Table'} items={collectionMenuItems} onClick={handleClick} />;
};
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 (
<div>
{props.children}
<AddBlockButton />
</div>
);
},
{ displayName: 'Page' },
);
const Root = () => {
return <SchemaComponent schema={schema}></SchemaComponent>;
};
const App = createApp(Root, {
components: { MyTable, Page },
scopes: { useTableProps },
schemaSettings: [MyTableSettings],
schemaInitializers: [myInitializer],
});
export default App;

View File

@ -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<any> {
// 获取
const { props } = useDataBlock();
const { bordered } = props;
const { data, loading } = useDataBlockRequest<any[]>();
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 <SchemaComponent schema={schema}></SchemaComponent>;
};
const Root = createApp(Demo, {
components: { MyTable },
scopes: { useTableProps },
schemaSettings: [MyTableSettings],
});
export default Root;

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -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,
},
],
},
];

View File

@ -0,0 +1 @@
import { Application } from '@nocobase/client';

View File

@ -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 <Table dataSource={dataSource} columns={columns} />;
};
export default MyTable;

View File

@ -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<any>();
return <div>{field.title}</div>;
});
function useTableProps(): TableProps<any> {
const { tableProps } = useDataBlockProps();
const { data, loading } = useDataBlockRequest<any[]>();
const dataSource = useMemo(() => data?.data || [], [data]);
const collection = useCollection();
const field = useField<any>();
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: <RecursionField name={collectionField.name} schema={tableFieldSchema} onlyRenderSelf />,
dataIndex: collectionField.name,
render(value, record, index) {
return (
<RecursionField basePath={field.address.concat(index)} onlyRenderProperties schema={tableFieldSchema} />
);
},
};
});
}, [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 (
<div>
{props.children}
<AddBlockButton />
</div>
);
},
{ 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 (
<>
<div>
{render()}
<Button type="primary" onClick={showDrawer}>
Add New
</Button>
</div>
<Drawer title={`${title} | Add New`} onClose={onClose} open={open}>
<p>Some contents...</p>
</Drawer>
</>
);
};
const createActionSettings = new SchemaSettings({
name: 'createActionSettings',
items: [
{
type: 'remove',
name: 'remove',
},
],
});
const RefreshAction = () => {
const { refresh } = useDataBlockRequest();
return <Button onClick={refresh}>Refresh</Button>;
};
const ActionBar = ({ children }) => {
const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']);
return (
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
marginBottom: 'var(--nb-spacing)',
}}
>
<Space>
{children}
{render()}
</Space>
</div>
);
};
const CreateActionInitializer = () => {
const { insert } = useSchemaInitializer();
const handleClick = () => {
insert({
type: 'void',
'x-component': 'CreateAction',
'x-settings': 'createActionSettings',
});
};
return <SchemaInitializerItem title={'Add New'} onClick={handleClick}></SchemaInitializerItem>;
};
const RefreshActionInitializer = () => {
const { insert } = useSchemaInitializer();
const handleClick = () => {
insert({
type: 'void',
'x-component': 'RefreshAction',
});
};
return <SchemaInitializerItem title={'Refresh'} onClick={handleClick}></SchemaInitializerItem>;
};
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 <SchemaInitializerItem title={'Table'} items={menuItems} onClick={handleClick} />;
};
const MyToolbar = (props) => {
const collection = useCollection();
const compile = useCompile();
return <SchemaToolbar title={`${compile(collection.title)}`} {...props} />;
};
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 <SchemaComponent schema={rootSchema}></SchemaComponent>;
};
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();

View File

@ -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<any>();
return <div>{field.title}</div>;
});
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: <RecursionField name={tableField.name} schema={tableField} onlyRenderSelf />,
dataIndex: name,
key: name,
width: 200,
render(value, record, index) {
return <RecursionField basePath={filed.address.concat(index)} onlyRenderProperties schema={tableField} />;
},
};
});
const tableColumns = useMemo(() => {
return [
...columns,
{
title: render(),
dataIndex: 'TABLE_COLUMN_INITIALIZER',
key: 'TABLE_COLUMN_INITIALIZER',
width: 200,
render: designable ? () => <div style={{ minWidth: 300 }} /> : null,
},
];
}, [columns, render, designable]);
return tableColumns;
}
function useTableProps(): TableProps<any> {
const { tableProps } = useDataBlockProps();
const { data, loading } = useDataBlockRequest<any[]>();
const dataSource = useMemo(() => data?.data || [], [data]);
const field = useField<any>();
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 (
<div>
{props.children}
<AddBlockButton />
</div>
);
},
{ 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 (
<>
<div>
{render()}
<Button type="primary" onClick={showDrawer}>
Add New
</Button>
</div>
<Drawer title={`${title} | Add New`} onClose={onClose} open={open}>
<p>Some contents...</p>
</Drawer>
</>
);
};
const createActionSettings = new SchemaSettings({
name: 'createActionSettings',
items: [
{
type: 'remove',
name: 'remove',
},
],
});
const RefreshAction = () => {
const { refresh } = useDataBlockRequest();
return <Button onClick={refresh}>Refresh</Button>;
};
const ActionBar = ({ children }) => {
const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']);
return (
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
marginBottom: 'var(--nb-spacing)',
}}
>
<Space>
{children}
{render()}
</Space>
</div>
);
};
const CreateActionInitializer = () => {
const { insert } = useSchemaInitializer();
const handleClick = () => {
insert({
type: 'void',
'x-component': 'CreateAction',
'x-settings': 'createActionSettings',
});
};
return <SchemaInitializerItem title={'Add New'} onClick={handleClick}></SchemaInitializerItem>;
};
const RefreshActionInitializer = () => {
const { insert } = useSchemaInitializer();
const handleClick = () => {
insert({
type: 'void',
'x-component': 'RefreshAction',
});
};
return <SchemaInitializerItem title={'Refresh'} onClick={handleClick}></SchemaInitializerItem>;
};
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 <SchemaInitializerItem title={'Table'} items={menuItems} onClick={handleClick} />;
};
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 (
<SchemaInitializerSwitch
checked={exists}
title={itemConfig.title}
onClick={() => {
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 <SchemaToolbar title={`${compile(collection.title)}`} {...props} />;
};
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 <SchemaComponent schema={rootSchema}></SchemaComponent>;
};
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();

View File

@ -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<any>();
return <div>{field.title}</div>;
});
const ColumnView = observer(
() => {
const record = useCollectionRecord();
const [open, setOpen] = useState(false);
const showDrawer = () => {
setOpen(true);
};
const onClose = () => {
setOpen(false);
};
return (
<>
<Button type="link" onClick={showDrawer}>
view
</Button>
<Drawer onClose={onClose} open={open}>
<pre>{JSON.stringify(record, null, 2)}</pre>
</Drawer>
</>
);
},
{ 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: <RecursionField name={tableField.name} schema={tableField} onlyRenderSelf />,
dataIndex: name,
key: name,
render(value, record, index) {
return <RecursionField basePath={filed.address.concat(index)} onlyRenderProperties schema={tableField} />;
},
};
});
const tableColumns = useMemo(() => {
return [
...columns,
{
title: 'Actions',
dataIndex: 'actions',
key: 'actions',
render: (value, record) => {
return (
<CollectionRecordProvider record={record}>
<Space>
<ColumnView />
</Space>
</CollectionRecordProvider>
);
},
},
{
title: render(),
dataIndex: 'TABLE_COLUMN_INITIALIZER',
key: 'TABLE_COLUMN_INITIALIZER',
render: designable ? () => <div style={{ minWidth: 300 }} /> : null,
},
];
}, [columns, render, designable]);
return tableColumns;
}
function useTableProps(): TableProps<any> {
const { tableProps } = useDataBlockProps();
const { data, loading } = useDataBlockRequest<any[]>();
const dataSource = useMemo(() => data?.data || [], [data]);
const field = useField<any>();
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 (
<div>
{props.children}
<AddBlockButton />
</div>
);
},
{ 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 (
<>
<div>
{render()}
<Button type="primary" onClick={showDrawer}>
Add New
</Button>
</div>
<Drawer title={`${title} | Add New`} onClose={onClose} open={open}>
<p>Some contents...</p>
</Drawer>
</>
);
};
const createActionSettings = new SchemaSettings({
name: 'createActionSettings',
items: [
{
type: 'remove',
name: 'remove',
},
],
});
const RefreshAction = () => {
const { refresh } = useDataBlockRequest();
return <Button onClick={refresh}>Refresh</Button>;
};
const ActionBar = ({ children }) => {
const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']);
return (
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
marginBottom: 'var(--nb-spacing)',
}}
>
<Space>
{children}
{render()}
</Space>
</div>
);
};
const CreateActionInitializer = () => {
const { insert } = useSchemaInitializer();
const handleClick = () => {
insert({
type: 'void',
'x-component': 'CreateAction',
'x-settings': 'createActionSettings',
});
};
return <SchemaInitializerItem title={'Add New'} onClick={handleClick}></SchemaInitializerItem>;
};
const RefreshActionInitializer = () => {
const { insert } = useSchemaInitializer();
const handleClick = () => {
insert({
type: 'void',
'x-component': 'RefreshAction',
});
};
return <SchemaInitializerItem title={'Refresh'} onClick={handleClick}></SchemaInitializerItem>;
};
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 <SchemaInitializerItem title={'Table'} items={menuItems} onClick={handleClick} />;
};
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 (
<SchemaInitializerSwitch
checked={exists}
title={itemConfig.title}
onClick={() => {
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 <SchemaToolbar title={`${compile(collection.title)}`} {...props} />;
};
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 <SchemaComponent schema={rootSchema}></SchemaComponent>;
};
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();

View File

@ -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 <Table dataSource={dataSource} columns={columns} />;
};
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 <SchemaComponent schema={tableSchema} />;
};
const app = new Application({
plugins: [MyPlugin],
components: {
CardItem,
},
router: {
type: 'memory',
initialEntries: ['/'],
},
});
app.router.add('root', {
path: '/',
Component: Root,
});
export default app.getRootComponent();

View File

@ -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 <Table dataSource={dataSource} columns={columns} />;
};
const AddBlockButton = observer(
() => {
const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']);
return render();
},
{ displayName: 'AddBlockButton' },
);
const Page = observer(
(props) => {
return (
<div>
{props.children}
<AddBlockButton />
</div>
);
},
{ 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 <SchemaInitializerItem title={'Table'} onClick={handleClick} />;
};
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 <SchemaComponent schema={rootSchema}></SchemaComponent>;
};
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();

View File

@ -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<any[]>();
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 <Table loading={loading} rowKey="id" dataSource={dataSource} columns={columns} />;
};
const AddBlockButton = observer(
() => {
const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']);
return render();
},
{ displayName: 'AddBlockButton' },
);
const Page = observer(
(props) => {
return (
<div>
{props.children}
<AddBlockButton />
</div>
);
},
{ 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 <SchemaInitializerItem title={'Table'} items={menuItems} onClick={handleClick} />;
};
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 <SchemaComponent schema={rootSchema}></SchemaComponent>;
};
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();

View File

@ -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<any>();
return <div>{field.title}</div>;
});
const MyTable = () => {
const { data, loading } = useDataBlockRequest<any[]>();
const dataSource = useMemo(() => data?.data || [], [data]);
const collection = useCollection();
const field = useField<any>();
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: <RecursionField name={collectionField.name} schema={tableFieldSchema} onlyRenderSelf />,
dataIndex: collectionField.name,
render(value, record, index) {
return (
<RecursionField basePath={field.address.concat(index)} onlyRenderProperties schema={tableFieldSchema} />
);
},
};
});
}, [collection]);
return <Table loading={loading} rowKey={'id'} dataSource={dataSource} columns={columns} />;
};
const AddBlockButton = observer(
() => {
const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']);
return render();
},
{ displayName: 'AddBlockButton' },
);
const Page = observer(
(props) => {
return (
<div>
{props.children}
<AddBlockButton />
</div>
);
},
{ 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 <SchemaInitializerItem title={'Table'} items={menuItems} onClick={handleClick} />;
};
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 <SchemaComponent schema={rootSchema}></SchemaComponent>;
};
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();

View File

@ -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<any>();
return <div>{field.title}</div>;
});
function useTableProps(): TableProps<any> {
const { tableProps } = useDataBlockProps();
const { data, loading } = useDataBlockRequest<any[]>();
const dataSource = useMemo(() => data?.data || [], [data]);
const collection = useCollection();
const field = useField<any>();
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: <RecursionField name={collectionField.name} schema={tableFieldSchema} onlyRenderSelf />,
dataIndex: collectionField.name,
render(value, record, index) {
return (
<RecursionField basePath={field.address.concat(index)} onlyRenderProperties schema={tableFieldSchema} />
);
},
};
});
}, [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 (
<div>
{props.children}
<AddBlockButton />
</div>
);
},
{ 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 <SchemaInitializerItem title={'Table'} items={menuItems} onClick={handleClick} />;
};
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 <SchemaComponent schema={rootSchema}></SchemaComponent>;
};
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();

View File

@ -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<any>();
return <div>{field.title}</div>;
});
function useTableProps(): TableProps<any> {
const { tableProps } = useDataBlockProps();
const { data, loading } = useDataBlockRequest<any[]>();
const dataSource = useMemo(() => data?.data || [], [data]);
const collection = useCollection();
const field = useField<any>();
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: <RecursionField name={collectionField.name} schema={tableFieldSchema} onlyRenderSelf />,
dataIndex: collectionField.name,
render(value, record, index) {
return (
<RecursionField basePath={field.address.concat(index)} onlyRenderProperties schema={tableFieldSchema} />
);
},
};
});
}, [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 (
<div>
{props.children}
<AddBlockButton />
</div>
);
},
{ 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 <SchemaInitializerItem title={'Table'} items={menuItems} onClick={handleClick} />;
};
const MyToolbar = (props) => {
const collection = useCollection();
const compile = useCompile();
return <SchemaToolbar title={`${compile(collection.title)}`} {...props} />;
};
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 <SchemaComponent schema={rootSchema}></SchemaComponent>;
};
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();

View File

@ -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<any>();
return <div>{field.title}</div>;
});
function useTableProps(): TableProps<any> {
const { tableProps } = useDataBlockProps();
const { data, loading } = useDataBlockRequest<any[]>();
const dataSource = useMemo(() => data?.data || [], [data]);
const collection = useCollection();
const field = useField<any>();
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: <RecursionField name={collectionField.name} schema={tableFieldSchema} onlyRenderSelf />,
dataIndex: collectionField.name,
render(value, record, index) {
return (
<RecursionField basePath={field.address.concat(index)} onlyRenderProperties schema={tableFieldSchema} />
);
},
};
});
}, [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 (
<div>
{props.children}
<AddBlockButton />
</div>
);
},
{ 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 (
<>
<Button type="primary" onClick={showDrawer}>
Add New
</Button>
<Drawer title={`${title} | Add New`} onClose={onClose} open={open}>
<p>Some contents...</p>
</Drawer>
</>
);
};
const RefreshAction = () => {
const { refresh } = useDataBlockRequest();
return <Button onClick={refresh}>Refresh</Button>;
};
const ActionBar = () => {
const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']);
return (
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
marginBottom: 'var(--nb-spacing)',
}}
>
<Space>
<CreateAction></CreateAction>
<RefreshAction></RefreshAction>
{render()}
</Space>
</div>
);
};
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 <SchemaInitializerItem title={'Table'} items={menuItems} onClick={handleClick} />;
};
const MyToolbar = (props) => {
const collection = useCollection();
const compile = useCompile();
return <SchemaToolbar title={`${compile(collection.title)}`} {...props} />;
};
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 <SchemaComponent schema={rootSchema}></SchemaComponent>;
};
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();

View File

@ -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<any>();
return <div>{field.title}</div>;
});
function useTableProps(): TableProps<any> {
const { tableProps } = useDataBlockProps();
const { data, loading } = useDataBlockRequest<any[]>();
const dataSource = useMemo(() => data?.data || [], [data]);
const collection = useCollection();
const field = useField<any>();
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: <RecursionField name={collectionField.name} schema={tableFieldSchema} onlyRenderSelf />,
dataIndex: collectionField.name,
render(value, record, index) {
return (
<RecursionField basePath={field.address.concat(index)} onlyRenderProperties schema={tableFieldSchema} />
);
},
};
});
}, [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 (
<div>
{props.children}
<AddBlockButton />
</div>
);
},
{ 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 (
<>
<Button type="primary" onClick={showDrawer}>
Add New
</Button>
<Drawer title={`${title} | Add New`} onClose={onClose} open={open}>
<p>Some contents...</p>
</Drawer>
</>
);
};
const RefreshAction = () => {
const { refresh } = useDataBlockRequest();
return <Button onClick={refresh}>Refresh</Button>;
};
const ActionBar = ({ children }) => {
const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']);
return (
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
marginBottom: 'var(--nb-spacing)',
}}
>
<Space>
{children}
{render()}
</Space>
</div>
);
};
const CreateActionInitializer = () => {
const { insert } = useSchemaInitializer();
const handleClick = () => {
insert({
type: 'void',
'x-component': 'CreateAction',
});
};
return <SchemaInitializerItem title={'Add New'} onClick={handleClick}></SchemaInitializerItem>;
};
const RefreshActionInitializer = () => {
const { insert } = useSchemaInitializer();
const handleClick = () => {
insert({
type: 'void',
'x-component': 'RefreshAction',
});
};
return <SchemaInitializerItem title={'Refresh'} onClick={handleClick}></SchemaInitializerItem>;
};
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 <SchemaInitializerItem title={'Table'} items={menuItems} onClick={handleClick} />;
};
const MyToolbar = (props) => {
const collection = useCollection();
const compile = useCompile();
return <SchemaToolbar title={`${compile(collection.title)}`} {...props} />;
};
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 <SchemaComponent schema={rootSchema}></SchemaComponent>;
};
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();

View File

@ -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];
});
});
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
# AssociationProvider
用于传递关联数据字段以及对应的数据表信息,等同于 `CollectionFieldProvider` + `CollectionProvider`
```tsx | pure
const AssociationProvider = <CollectionFieldProvider name={fieldName}>
<CollectionProvider name={collectionManager.getCollectionName(fieldName)}>
{children}
</CollectionProvider>
</CollectionFieldProvider>
```
关于关系字段的更多信息,请参考 [关系字段](https://docs.nocobase.com/development/server/collections/association-fields) 以及 [CollectionField](/core/data-source/collection-field)。

View File

@ -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<T extends CollectionFieldInterface>(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<string, { label: string; order?: number }>): 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<string, { label: string; order?: number }>;
}
```
- 示例
```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 }
}
}
```

View File

@ -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<string, ISchema>
usePathOptions(field: CollectionFieldOptions): any
schemaInitialize(schema: ISchema, data: any): void
getOption<K extends keyof IField>(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)

View File

@ -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"` 则所有使用到此字段的地方都会变化。
<code src="./demos/collection-field/demo1.tsx"></code>
## 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) 中使用,用来获取和修改字段的属性。
<code src="./demos/collection-field/demo2.tsx"></code>

View File

@ -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 (
<CollectionManagerProvider dataSource='test'>
<div>...</div>
</CollectionManagerProvider>
);
};
```
## Hooks
### useCollectionManager()
用于获取 `CollectionManagerProvider` 传递的实例。
- 示例
```tsx | pure
const Demo = () => {
const collectionManager = useCollectionManager();
const collections = collectionManager.getCollections()
return <div>
<pre>{JSON.stringify(collections, null, 2)}</pre>
</div>;
};
```

View File

@ -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<Mixins = {}>(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<TestMixin>('users');
const collection = collectionManager.getCollection<TestMixin & TestMixin2>('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 字段
```

View File

@ -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<TestMixin>('users');
userCollection.test(); // 'test users'
}
```
- 使用方式2调用 `useCollection()` 获取当前上下文的数据表信息。
```tsx | pure
const Demo = () => {
const collection = useCollection<TestMixin>();
collection.test(); // 'test users'
}
```
### 多个 Mixins 的使用
如果添加了 Mixins可通过如下方式获得类型提示
```tsx | pure
const Demo = () => {
const collection = useCollection<TestMixin & Test2Mixin>();
}
```
<code src='./demos/data-source-manager/mixins.tsx'></code>

View File

@ -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 (
<CollectionProvider name="users">
<div>...</div>
</CollectionProvider>
)
}
```
## Hooks
### useCollection()
用于获取 `CollectionProvider` 传递的 `Collection` 实例。
```tsx | pure
const collection = useCollection()
console.log(collection instanceof Collection) // true
console.log(collection);
```
结合 Mixin 使用:
```tsx | pure
const collection = useCollection<TestMixin>()
const collection = useCollection<TestMixin & TestMixin2>()
```
## 示例
<code src="./demos/collection/demo1.tsx"></code>

View File

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

View File

@ -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<string, ISchema>;
/** 当前模板可用的字段类型 */
availableFieldInterfaces?: AvailableFieldInterfacesInclude | AvailableFieldInterfacesExclude;
/** 是否分割线 */
divider?: boolean;
/** 模板描述 */
description?: string;
/**配置字段中的操作按钮 */
configureActions?: Record<string, ISchema>;
//是否禁止删除字段
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<DefaultConfigurableKeys, any>
```
- 示例
```tsx | pure
import { getConfigurableProperties } from '@nocobase/client';
const sqlCollectionTemplate = new CollectionTemplate({
name: 'sql',
// ...
configurableProperties: {
...getConfigurableProperties('name', 'title', 'description'),
// ...
},
});
```

View File

@ -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<K extends keyof CollectionOptions>(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<K extends keyof CollectionOptions>(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
```

View File

@ -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']);
}
```

View File

@ -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<MyCollectionMixin>();
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 (
<div>
{dataSources.map(dataSource => (
<div key={dataSource.key}>{dataSource.displayName}</div>
))}
</div>
)
}
```
### 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 (
<div>
{dataSource.displayName}
</div>
)
}
```
### 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 (
<div>
{collections.map(({ key, displayName, collections }) => (
<div key={key}>
<h3>{displayName}</h3>
<ul>
{collections.map(collection => (
<li key={collection.name}>{collection.name}</li>
))}
</ul>
</div>
))}
</div>
)
}
```
### reload()
重载所有 `DataSource`
- 类型
```tsx | pure
class DataSourceManager {
reload(): Promise<void>;
}
```
- 示例
```tsx | pure
const MyComponent = () => {
const dataSourceManager = useDataSourceManager();
dataSourceManager.reload();
}
```

View File

@ -0,0 +1,66 @@
# DataSourceProvider
用于提供 `DataSource` 的实例。
## 组件
- 类型
```tsx | pure
interface DataSourceProviderProps {
dataSource?: string;
children?: ReactNode;
}
```
- 示例
```tsx | pure
const MyComponent = () => {
return (
<DataSourceProvider dataSource="my-data-source">
<MyChildComponent />
</DataSourceProvider>
);
}
```
## Hooks
### useDataSource()
获取 `DataSource` 实例。
- 类型
```tsx | pure
function useDataSource(): DataSource;
```
- 示例
```tsx | pure
const MyComponent = () => {
const dataSource = useDataSource();
return <div>{dataSource.displayName}</div>
}
```
### useDataSourceKey()
获取 `DataSource` 的 key。
- 类型
```tsx | pure
function useDataSourceKey(): string;
```
- 示例
```tsx | pure
const MyComponent = () => {
const dataSourceKey = useDataSourceKey();
return <div>{dataSourceKey}</div>
}
```

View File

@ -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<void>;
}
```
- 示例
```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'));
};
};
```

View File

@ -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 (
<SchemaComponentOptions scope={{ useSubmit }}>
<CollectionProvider name="tests">
<SchemaComponent schema={schema} />
</CollectionProvider>
</SchemaComponentOptions>
);
});
const app = new Application({
providers: [Root],
components: { Action, Form, Input, FormItem },
dataSourceManager: {
collections: [collection],
collectionMixins: [InheritanceCollectionMixin],
},
});
export default app.getRootComponent();

View File

@ -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 (
<CollectionProvider name="tests">
<SchemaComponent schema={schema} />
</CollectionProvider>
);
});
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();

View File

@ -0,0 +1,18 @@
import React from 'react';
import { CollectionProvider, useCollection } from '@nocobase/client';
import { createApp } from '../createApp';
const Demo = () => {
const collection = useCollection();
return <pre>{JSON.stringify(collection.getField('username'), null, 2)}</pre>;
};
const Root = () => {
return (
<CollectionProvider name="users">
<Demo />
</CollectionProvider>
);
};
export default createApp(Root);

View File

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

View File

@ -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<any>,
options: ApplicationOptions = {},
mocks: Record<string, any> = 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;
}

View File

@ -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<TestMixin & Test2Mixin>();
return (
<div>
<div>test: {collection.test()}</div>
<div>test2: {collection.test2()}</div>
<div>fields.length: {collection.getFields().length}</div>
</div>
);
};
const Root = () => {
return (
<CollectionProvider name="users">
<Demo />
</CollectionProvider>
);
};
class MyPlugin extends Plugin {
async load() {
this.app.dataSourceManager.addCollectionMixins([TestMixin, Test2Mixin]);
}
}
export default createApp(Root, {
plugins: [MyPlugin],
});

View File

@ -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 (
<ExtendCollectionsProvider collections={[ userCollection ]}>
<CollectionProvider name="users">
<ChildComponent />
</CollectionProvider>
</ExtendCollectionsProvider>
);
}
const ChildComponent = () => {
const collection = useCollection();
return (
<div>
<h1>{collection.name}</h1>
</div>
);
}
```
## Hooks
### useExtendCollections()
获取扩展的数据表。
```tsx | pure
import { useExtendCollections } from '@nocobase/client';
const MyComponent = () => {
const collections = useExtendCollections();
return (
<div>
{collections.map((collection) => (
<div key={collection.name}>{collection.title}</div>
))}
</div>
);
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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',
},
},
];
},

View File

@ -28,7 +28,6 @@ class PluginHello extends Plugin {
export const SchemaSettingsBlockTitleItem = function BlockTitleItem() {
const { dn } = useSchemaSettings();
console.log(dn.current);
return (
<SchemaSettingsModalItem

View File

@ -5,3 +5,9 @@
### tval
用于输出多语言的字符串。
- 类型
- 示例

View File

@ -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 <RecordProvider record={record}></RecordProvider>
const record = new CollectionRecord({ current: item });
return <CollectionRecordProvider record={record}></CollectionRecordProvider>
})}
```
a:get(view、edit)
```jsx | pure
const record = new Record({ current: item });
<RecordProvider record={item}></RecordProvider>
const record = new CollectionRecord({ current: item });
<CollectionRecordProvider record={item}></CollectionRecordProvider>
```
a:create
```jsx | pure
const record = new Record({ current, isNew: true });
<RecordProvider record={record}></RecordProvider>
const record = new CollectionRecord({ current, isNew: true });
<CollectionRecordProvider record={record}></CollectionRecordProvider>
```
a.b:list
```jsx | pure
<RecordProvider record={recordA}>
<CollectionRecordProvider record={recordA}>
{list.map((item) => {
const recordB = new Record({
const recordB = new CollectionRecord({
current: item,
});
return <RecordProvider record={recordB}></RecordProvider>
return <CollectionRecordProvider record={recordB}></CollectionRecordProvider>
})}
</RecordProvider>
</CollectionRecordProvider>
```
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);
<RecordProvider record={recordA}>
<RecordProvider record={recordB}></RecordProvider>
</RecordProvider>
<CollectionRecordProvider record={recordA}>
<CollectionRecordProvider record={recordB}></CollectionRecordProvider>
</CollectionRecordProvider>
```
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,
});
<RecordProvider record={recordA}>
<RecordProvider record={recordB}></RecordProvider>
</RecordProvider>
<CollectionRecordProvider record={recordA}>
<CollectionRecordProvider record={recordB}></CollectionRecordProvider>
</CollectionRecordProvider>
```
## 区块
@ -178,9 +178,9 @@ DataBlockProvider 内容
<BlockProvider>
<CollectionProvider>
<UseRequestProvider>
<RecordProvider record={recordA}>
<CollectionRecordProvider record={recordA}>
{props.children}
</RecordProvider>
</CollectionRecordProvider>
</UseRequestProvider>
</CollectionProvider>
</BlockProvider>
@ -189,7 +189,7 @@ DataBlockProvider 内容
有当前父级记录的区块,如对多关系的表格区块,区块本身不套 RecordPicker
```tsx | pure
<RecordProvider record={recordA}>
<CollectionRecordProvider record={recordA}>
<DataBlockProvider>
<ActionBar>
<Action type="filter" />
@ -208,7 +208,7 @@ DataBlockProvider 内容
<Table useProps />
<Pagination />
</DataBlockProvider>
</RecordProvider>
</CollectionRecordProvider>
```
DataBlockProvider 内容
@ -228,14 +228,14 @@ DataBlockProvider 内容
有父级记录也有当前记录的区块,如关系的表单
```tsx | pure
<RecordProvider record={recordA}>
<CollectionRecordProvider record={recordA}>
<DataBlockProvider>
<Form useProps />
</DataBlockProvider>
<DataBlockProvider>
<Form useProps />
</DataBlockProvider>
</RecordProvider>
</CollectionRecordProvider>
```
DataBlockProvider 内容
@ -245,9 +245,9 @@ DataBlockProvider 内容
<AssociationProvider>
<CollectionProvider>
<UseRequestProvider>
<RecordProvider record={recordA}>
<CollectionRecordProvider record={recordA}>
{props.children}
</RecordProvider>
</CollectionRecordProvider>
</UseRequestProvider>
</CollectionProvider>
</AssociationProvider>

View File

@ -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 {
- schemaSettingsSchema 设置工具,具体参考:[SchemaSettingsManager](/core/ui-schema/schema-initializer-manager)
- schemaInitializersSchema 添加工具,具体参考:[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()

View File

@ -34,6 +34,10 @@ class Plugin<T = any> {
return this.app.schemaSettingsManager;
}
get dataSourceManager() {
return this.app.dataSourceManager;
}
async afterAdd() {}
async beforeLoad() {}

View File

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

View File

@ -0,0 +1,259 @@
# CollectionRecordProvider
用于提供 [CollectionRecord](./collection-record) 实例。
## 组件
- 类型
```tsx | pure
interface CollectionRecordProviderProps<DataType = {}, ParentDataType = {}> {
isNew?: boolean;
record?: CollectionRecord<DataType, ParentDataType> | DataType;
parentRecord?: CollectionRecord<ParentDataType> | 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' } });
<CollectionRecordProvider record={record} />
// 最终向子组件传递的数据为: 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 <pre>{JSON.stringify(record, null, 2)}</pre>;
}
export default () => <CollectionRecordProvider record={record}><Demo /></CollectionRecordProvider>
```
- record 参数为普通对象
```tsx | pure
<CollectionRecordProvider record={{ id: 1, name: 'foo' }} />
// 最终向子组件传递的数据为: const record = new CollectionRecord({ data: { id: 1, name: 'foo' } });
```
```tsx
import { useCollectionRecord, CollectionRecordProvider } from '@nocobase/client';
const Demo = () => {
const record = useCollectionRecord();
return <pre>{JSON.stringify(record, null, 2)}</pre>;
}
export default () => <CollectionRecordProvider record={{ id: 1, name: 'foo' }}><Demo /></CollectionRecordProvider>
```
- record 参数为 CollectionRecord 实例且带有父记录
```tsx | pure
const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } });
const record = new CollectionRecord({ data: { id: 1, name: 'foo' }, parentRecord });
<CollectionRecordProvider record={record} />
// 最终向子组件传递的数据为: 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 <pre>{JSON.stringify(record, null, 2)}</pre>;
}
export default () => <CollectionRecordProvider record={record}><Demo /></CollectionRecordProvider>
```
- record 参数为 CollectionRecord 实例,父记录通过 `parentRecord` 参数传递
```tsx | pure
const parentRecord = new CollectionRecord({ data: { id: 1, role: 'admin' } });
const record = new CollectionRecord({ data: { id: 1, name: 'foo' } });
<CollectionRecordProvider record={record} parentRecord={parentRecord} />
// 首先设置父记录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 <pre>{JSON.stringify(record, null, 2)}</pre>;
}
export default () => <CollectionRecordProvider record={record} parentRecord={parentRecord}><Demo /></CollectionRecordProvider>
```
- record 参数为普通对象,父记录也是普通对象
```tsx | pure
<CollectionRecordProvider record={{ id: 1, name: 'foo' }} parentRecord={{ id: 1, role: 'admin' }} />
// 首先实例化父记录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 <pre>{JSON.stringify(record, null, 2)}</pre>;
}
export default () => <CollectionRecordProvider record={{ id: 1, name: 'foo' }} parentRecord={{ id: 1, role: 'admin' }} ><Demo /></CollectionRecordProvider>
```
## 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 <div>
<div>record: <pre>{JSON.stringify(record, null, 2)}</pre></div>
<div style={{ marginTop: 10 }}>record.data: <pre>{JSON.stringify(record.data, null, 2)}</pre></div>
<div style={{ marginTop: 10 }}>record.parentRecord: <pre>{JSON.stringify(record.parentRecord, null, 2)}</pre></div>
<div style={{ marginTop: 10 }}>record.parentRecord.data: <pre>{JSON.stringify(record.parentRecord.data, null, 2)}</pre></div>
</div>;
}
export default () => <CollectionRecordProvider record={{ id: 1, name: 'foo' }} parentRecord={{ id: 1, role: 'admin' }} ><Demo /></CollectionRecordProvider>
```
### 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 <div>
<div>data === record.data: { JSON.stringify(data === record.data) }</div>
<div style={{ marginTop: 10 }}>data: <pre>{JSON.stringify(data, null, 2)}</pre></div>
<div style={{ marginTop: 10 }}>record.data: <pre>{JSON.stringify(record.data, null, 2)}</pre></div>
</div>;
}
export default () => <CollectionRecordProvider record={{ id: 1, name: 'foo' }} ><Demo /></CollectionRecordProvider>
```
### 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 <div>
<div>parentRecord === record.parentRecord: { JSON.stringify(parentRecord === record.parentRecord) }</div>
<div style={{ marginTop: 10 }}>parentRecord: <pre>{JSON.stringify(parentRecord, null, 2)}</pre></div>
<div style={{ marginTop: 10 }}>record.parentRecord: <pre>{JSON.stringify(record.parentRecord, null, 2)}</pre></div>
</div>;
}
export default () => <CollectionRecordProvider record={{ id: 1, name: 'foo' }} parentRecord={{ id: 1, role: 'admin' }} ><Demo /></CollectionRecordProvider>
```
### 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 <div>
<div style={{ marginTop: 10 }}>parentData: <pre>{JSON.stringify(parentData, null, 2)}</pre></div>
<div style={{ marginTop: 10 }}>parentRecord.data: <pre>{JSON.stringify(parentRecord.data, null, 2)}</pre></div>
<div style={{ marginTop: 10 }}>record.parentRecord.data: <pre>{JSON.stringify(record.parentRecord.data, null, 2)}</pre></div>
</div>;
}
export default () => <CollectionRecordProvider record={{ id: 1, name: 'foo' }} parentRecord={{ id: 1, role: 'admin' }} ><Demo /></CollectionRecordProvider>
```

View File

@ -0,0 +1,194 @@
# CollectionRecord
数据表中的一条记录。
## 类型
```tsx | pure
interface CollectionRecordOptions<DataType = {}, ParentDataType = {}> {
isNew?: boolean;
data?: DataType;
parentRecord?: CollectionRecord<ParentDataType>;
/**
* 当前记录所属的 collection name
*/
collectionName?: string;
}
class CollectionRecord<DataType = {}, ParentDataType = {}> {
public isNew?: boolean;
public data?: DataType;
public parentRecord?: CollectionRecord<ParentDataType>;
public collectionName?: string;
constructor(options: CollectionRecordOptions<DataType, ParentDataType>) {}
setData(data: DataType) {
this.data = data;
}
setParentRecord(parentRecord: CollectionRecord<ParentDataType>) {
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);
```

View File

@ -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 <DataBlockContext.Provider>
<CollectionDataSourceProvider>
<CollectionProvider> / <AssociationProvider>
<BlockResourceProvider>
<BlockRequestProvider>
{action !== 'list' && <CollectionRecordProvider record={blocRequest.data}>
{props.children}
</Record>}
</BlockRequestProvider>
</BlockResourceProvider> / </AssociationProvider>
</CollectionProvider>
</CollectionDataSourceProvider>
</DataBlockContext.Provider>
}
```
上述组件封装到 `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',
}
```
### 完整示例
<code src="./demos/data-block-provider/complete-demo.tsx"></code>
## 属性
### 静态属性和动态属性
- 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<string, any>;
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 <DataBlockContext.Provider value={props}>
<CollectionDataSourceProvider>
<CollectionProvider name={props.collection}> / <CollectionProvider name={props.association}>
<BlockResourceProvider {...props}>
<BlockRequestProvider resource={resource}>
{action !== 'list' && <CollectionRecordProvider record={blocRequest.data}>
{props.children}
</Record>}
</BlockRequestProvider>
</BlockResourceProvider>
</CollectionProvider>
</CollectionDataSourceProvider>
</DataBlock.Provider>
}
```
### 属性组合和场景
这些属性根据不同的场景,共有 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<T extends {} = {}> {
props: AllDataBlockProps & T;
dn: Designable;
}
const useDataBlock: <T extends {}>() => Result<T>
```
- 详解
`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
<code src="./demos/data-block-provider/collection-table-list.tsx"></code>
#### Form get & update
<code src="./demos/data-block-provider/collection-form-get-and-update.tsx"></code>
#### Form create
<code src="./demos/data-block-provider/collection-form-create.tsx"></code>
#### Form record & update
<code src="./demos/data-block-provider/collection-form-record-and-update.tsx"></code>
### association
association 与 collection 类似,只是需要提供 `sourceId`,我们以 `Table list` 为例。
#### Table list & sourceId
<code src="./demos/data-block-provider/association-table-list-and-source-id.tsx"></code>
#### Table list & parentRecord
如果不提供 `sourceId`,则需要提供 `parentRecord`,我们以 `Table list` 为例。
<code src="./demos/data-block-provider/association-table-list-and-parent-record.tsx"></code>

View File

@ -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 (
<Table
dataSource={data?.data || []}
loading={loading}
pagination={{
total: data?.meta.total,
pageSize: data?.meta.pageSize,
page: data?.meta.page,
}}
/>
)
}
```
## 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` 对象,需要自己通过 `<CollectionRecordProvider />` 设置上下文。
```tsx | pure
const MyTable = () => {
const { data } = useDataBlockRequest();
return (
<Table
dataSource={data?.data || []}
columns={[
{
title: 'ID',
dataIndex: 'id',
},
{
title: 'Action',
render: (v, record) => {
return (
<CollectionRecordProvider record={record}>
<MyAction />
</CollectionRecordProvider>
)
},
},
]}
pagination={{
total: data?.meta.total,
pageSize: data?.meta.pageSize,
page: data?.meta.page,
}}
/>
)
}
const MyAction = () => {
const record = useCollectionRecord();
return (
<Button onClick={() => {
console.log(record.data);
}}>Dialog</Button>
)
}
```

View File

@ -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();
}
```
可运行示例

View File

@ -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<any> {
const { data, loading } = useDataBlockRequest<any[]>();
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 <SchemaComponent schema={schema}></SchemaComponent>;
};
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;

View File

@ -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<any> {
const { data, loading } = useDataBlockRequest<any[]>();
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 (
<>
<Select
defaultValue={state.userId}
options={[
{ key: 1, value: '1', label: 'Tom' },
{ key: 2, value: '2', label: 'Jack' },
]}
onChange={(v) => {
setState({ userId: v });
}}
></Select>
<SchemaComponent schema={schema}></SchemaComponent>
</>
);
};
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;

View File

@ -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<DemoFormFieldType>;
const DemoForm: FC<DemoFormProps> = withDynamicSchemaProps((props) => {
return (
<Form labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} style={{ maxWidth: 600 }} autoComplete="off" {...props}>
<Form.Item<DemoFormFieldType>
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input />
</Form.Item>
<Form.Item<DemoFormFieldType>
label="Age"
name="age"
rules={[{ required: true, message: 'Please input your age!' }]}
>
<InputNumber />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
});
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 <SchemaComponent schema={schema}></SchemaComponent>;
};
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;

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