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>
101
.github/workflows/build-pro-image.yml
vendored
Normal 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\"
|
||||
}"
|
1
.github/workflows/nocobase-test-backend.yml
vendored
@ -8,7 +8,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'packages/core/acl/**'
|
||||
|
4
.github/workflows/nocobase-test-e2e.yml
vendored
@ -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
@ -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}"]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -8,6 +8,7 @@ NocoBase is in early stage of development and is subject to frequent changes, pl
|
||||
|
||||
## Recent major updates
|
||||
|
||||
- [v0.20:Support for multiple data sources - 2024/03/03](https://docs-cn.nocobase.com/welcome/release/v0200-changelog)
|
||||
- [v0.19: Application process optimization - 2024/01/08](https://blog.nocobase.com/posts/release-v019/)
|
||||
- [v0.18: Establish a sound testing system - 2023/12/21](https://blog.nocobase.com/posts/release-v018/)
|
||||
- [v0.17: New SchemaInitializer and SchemaSettings - 2023/12/11](https://blog.nocobase.com/posts/release-v017/)
|
||||
|
@ -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/)
|
||||
|
@ -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",
|
||||
|
@ -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]);
|
||||
|
@ -186,6 +186,7 @@ export class ACLRole {
|
||||
const resource = this.resources.get(resourceName);
|
||||
|
||||
let action = null;
|
||||
|
||||
if (resource) {
|
||||
action = resource.getAction(actionName);
|
||||
}
|
||||
|
@ -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[];
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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: [
|
||||
|
@ -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);
|
||||
|
@ -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'));
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -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'),
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
4
packages/core/client/.dumi/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["**/*"]
|
||||
}
|
@ -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': [
|
||||
// {
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
```tsx | pure
|
||||
export interface ApplicationOptions {
|
||||
apiClient?: APIClientOptions;
|
||||
apiClient?: APIClientOptions | APIClient;
|
||||
ws?: WebSocketClientOptions | boolean;
|
||||
i18n?: i18next;
|
||||
providers?: (ComponentType | ComponentAndProps)[];
|
||||
@ -19,6 +19,7 @@ export interface ApplicationOptions {
|
||||
schemaSettings?: SchemaSetting[];
|
||||
schemaInitializers?: SchemaInitializer[];
|
||||
loadRemotePlugins?: boolean;
|
||||
dataSourceManager?: DataSourceManagerOptions;
|
||||
}
|
||||
```
|
||||
|
||||
@ -33,7 +34,7 @@ export interface ApplicationOptions {
|
||||
- schemaSettings:Schema 设置工具,具体参考:[SchemaSettingsManager](/core/ui-schema/schema-initializer-manager)
|
||||
- schemaInitializers:Schema 添加工具,具体参考:[SchemaInitializerManager](/core/ui-schema/schema-initializer-manager)
|
||||
- loadRemotePlugins:用于控制是否加载远程插件,默认为 `false`,即不加载远程插件(方便单测和 DEMO 环境)。
|
||||
|
||||
- dataSourceManager:数据源管理器,具体参考:[DataSourceManager](/core/data-source/data-source-manager)
|
||||
- 示例
|
||||
|
||||
```tsx
|
||||
@ -108,6 +109,10 @@ class Application {
|
||||
|
||||
详细介绍,请参考:[SchemaInitializerManager](/core/ui-schema/schema-initializer-manager)
|
||||
|
||||
### app.dataSourceManager
|
||||
|
||||
详细介绍,请参考:[dataSourceManager](/core/data-source/data-source-manager)
|
||||
|
||||
## 实例方法
|
||||
|
||||
### app.getRootComponent()
|
||||
|
@ -34,6 +34,10 @@ class Plugin<T = any> {
|
||||
return this.app.schemaSettingsManager;
|
||||
}
|
||||
|
||||
get dataSourceManager() {
|
||||
return this.app.dataSourceManager;
|
||||
}
|
||||
|
||||
async afterAdd() {}
|
||||
|
||||
async beforeLoad() {}
|
||||
|
@ -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>
|
||||
```
|
@ -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);
|
||||
```
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
```
|
@ -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();
|
||||
}
|
||||
```
|
||||
|
||||
可运行示例
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 4.8 KiB |
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
@ -0,0 +1 @@
|
||||
import { Application } from '@nocobase/client';
|
@ -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;
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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];
|
||||
});
|
||||
});
|
||||
}
|
1149
packages/core/client/docs/en-US/core/data-block/tmp.md
Normal 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)。
|
@ -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 }
|
||||
}
|
||||
}
|
||||
```
|
@ -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)
|
||||
|
@ -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>
|
||||
|
||||
|
||||
|
@ -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>;
|
||||
};
|
||||
```
|
@ -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 字段
|
||||
```
|
@ -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>
|
@ -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>
|
||||
|
@ -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 ]
|
||||
```
|
@ -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'),
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
293
packages/core/client/docs/en-US/core/data-source/collection.md
Normal 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
|
||||
```
|
@ -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']);
|
||||
}
|
||||
```
|
@ -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();
|
||||
}
|
||||
```
|
||||
|
@ -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>
|
||||
}
|
||||
```
|
176
packages/core/client/docs/en-US/core/data-source/data-source.md
Normal 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'));
|
||||
};
|
||||
};
|
||||
```
|
@ -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();
|
@ -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();
|
@ -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);
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -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;
|
||||
}
|
@ -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],
|
||||
});
|
@ -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>
|
||||
);
|
||||
}
|
||||
```
|
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 50 KiB |
@ -54,11 +54,14 @@ const myInitializer = new SchemaInitializer({
|
||||
],
|
||||
});
|
||||
|
||||
const AddBlockButton = observer(() => {
|
||||
const AddBlockButton = observer(
|
||||
() => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']);
|
||||
return render();
|
||||
});
|
||||
},
|
||||
{ displayName: 'AddBlockButton' },
|
||||
);
|
||||
|
||||
const Page = observer(
|
||||
(props) => {
|
||||
|
@ -62,11 +62,14 @@ const myInitializer = new SchemaInitializer({
|
||||
],
|
||||
});
|
||||
|
||||
const AddBlockButton = observer(() => {
|
||||
const AddBlockButton = observer(
|
||||
() => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']);
|
||||
return render();
|
||||
});
|
||||
},
|
||||
{ displayName: 'AddBlockButton' },
|
||||
);
|
||||
|
||||
const Page = observer(
|
||||
(props) => {
|
||||
|
@ -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();
|
||||
|
@ -75,11 +75,14 @@ const myInitializer = new SchemaInitializer({
|
||||
],
|
||||
});
|
||||
|
||||
const AddBlockButton = observer(() => {
|
||||
const AddBlockButton = observer(
|
||||
() => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']);
|
||||
return render();
|
||||
});
|
||||
},
|
||||
{ displayName: 'AddBlockButton' },
|
||||
);
|
||||
|
||||
const Page = observer(
|
||||
(props) => {
|
||||
|
@ -43,11 +43,14 @@ const myInitializer = new SchemaInitializer({
|
||||
Component: MyInitializerComponent,
|
||||
});
|
||||
|
||||
const AddBlockButton = observer(() => {
|
||||
const AddBlockButton = observer(
|
||||
() => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']);
|
||||
return render();
|
||||
});
|
||||
},
|
||||
{ displayName: 'AddBlockButton' },
|
||||
);
|
||||
|
||||
const Page = observer(
|
||||
(props) => {
|
||||
|
@ -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',
|
||||
componentProps: {
|
||||
title: 'A 1',
|
||||
},
|
||||
onClick: () => {
|
||||
alert('a-1');
|
||||
},
|
||||
@ -52,8 +54,10 @@ const mySettings = new SchemaSettings({
|
||||
{
|
||||
name: 'a2',
|
||||
type: 'item',
|
||||
componentProps: {
|
||||
title: 'A 2',
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
|
@ -28,7 +28,6 @@ class PluginHello extends Plugin {
|
||||
|
||||
export const SchemaSettingsBlockTitleItem = function BlockTitleItem() {
|
||||
const { dn } = useSchemaSettings();
|
||||
console.log(dn.current);
|
||||
|
||||
return (
|
||||
<SchemaSettingsModalItem
|
||||
|
@ -5,3 +5,9 @@
|
||||
### tval
|
||||
|
||||
用于输出多语言的字符串。
|
||||
|
||||
- 类型
|
||||
|
||||
|
||||
|
||||
- 示例
|
||||
|
@ -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>
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
```tsx | pure
|
||||
export interface ApplicationOptions {
|
||||
apiClient?: APIClientOptions;
|
||||
apiClient?: APIClientOptions | APIClient;
|
||||
ws?: WebSocketClientOptions | boolean;
|
||||
i18n?: i18next;
|
||||
providers?: (ComponentType | ComponentAndProps)[];
|
||||
@ -19,6 +19,7 @@ export interface ApplicationOptions {
|
||||
schemaSettings?: SchemaSetting[];
|
||||
schemaInitializers?: SchemaInitializer[];
|
||||
loadRemotePlugins?: boolean;
|
||||
dataSourceManager?: DataSourceManagerOptions;
|
||||
}
|
||||
```
|
||||
|
||||
@ -33,7 +34,7 @@ export interface ApplicationOptions {
|
||||
- schemaSettings:Schema 设置工具,具体参考:[SchemaSettingsManager](/core/ui-schema/schema-initializer-manager)
|
||||
- schemaInitializers:Schema 添加工具,具体参考:[SchemaInitializerManager](/core/ui-schema/schema-initializer-manager)
|
||||
- loadRemotePlugins:用于控制是否加载远程插件,默认为 `false`,即不加载远程插件(方便单测和 DEMO 环境)。
|
||||
|
||||
- dataSourceManager:数据源管理器,具体参考:[DataSourceManager](/core/data-source/data-source-manager)
|
||||
- 示例
|
||||
|
||||
```tsx
|
||||
@ -108,6 +109,10 @@ class Application {
|
||||
|
||||
详细介绍,请参考:[SchemaInitializerManager](/core/ui-schema/schema-initializer-manager)
|
||||
|
||||
### app.dataSourceManager
|
||||
|
||||
详细介绍,请参考:[dataSourceManager](/core/data-source/data-source-manager)
|
||||
|
||||
## 实例方法
|
||||
|
||||
### app.getRootComponent()
|
||||
|
@ -34,6 +34,10 @@ class Plugin<T = any> {
|
||||
return this.app.schemaSettingsManager;
|
||||
}
|
||||
|
||||
get dataSourceManager() {
|
||||
return this.app.dataSourceManager;
|
||||
}
|
||||
|
||||
async afterAdd() {}
|
||||
|
||||
async beforeLoad() {}
|
||||
|
@ -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();
|
||||
```
|
@ -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>
|
||||
```
|
@ -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);
|
||||
```
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
```
|
@ -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();
|
||||
}
|
||||
```
|
||||
|
||||
可运行示例
|
@ -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;
|
@ -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;
|
@ -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;
|