From a9e5e1442959ddb6705122ae815ea8a451be4697 Mon Sep 17 00:00:00 2001 From: chenos Date: Fri, 17 Mar 2023 09:20:56 +0800 Subject: [PATCH] feat: tree collection (#1561) * feat: adjacency list * fix: error * feat: collection extender * Revert "feat: collection extender" This reverts commit a942eee7699724fe831a98bae3f67a42db91a28b. * fix: registerBuiltInListeners * chore: getAllNodeIds # Conflicts: # packages/plugins/acl/src/server.ts * fix: get list data tree ids # Conflicts: # packages/plugins/acl/src/__tests__/list-action.test.ts * feat: suport add child * feat: demo3 * feat: suport add child * feat: support add child * feat: support add child * fix: tree true * feat: suport expend and collapse * feat: support expend and collapse * feat: support expend and collapse * feat: table block of the selector supports tree table * feat: expand and collapse are only displayed in the tree table * fix: when the tree table is closed and opened, it needs to be refreshed to take effect * fix: test * refactor: add child is hidden when the tree table is closed * refactor: tree table filter the children field * refactor: tree table filter the children field * refactor: tree table filter the children field * refactor: expand and collapse button does not support modify name and icon * refactor: parent cannot be modified when adding child * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: filter out the node itself when selecting parent * refactor: filter out the node itself when selecting parent * refactor: tree collection locale * refactor: tree collection locale * fix: parameter exception when creating data selector * fix: translation * refactor: tree collection locale * feat: data selector Support tree table * fix: failed to uncheck when multiple data selectors are selected * fix: open or disabled the tree table, and add child does not respond immediately * feat: data selector Support tree table * fix: can not hide add child button * fix: improve code * fix: tree table * fix: dynamic children column --------- Co-authored-by: Chareice Co-authored-by: katherinehhh --- packages/core/actions/package.json | 3 - .../actions/src/__tests__/get-action.test.ts | 6 +- .../actions/src/__tests__/list-action.test.ts | 198 ++++++++++++++++ packages/core/actions/src/actions/list.ts | 23 +- .../src/block-provider/FormBlockProvider.tsx | 13 ++ .../src/block-provider/TableBlockProvider.tsx | 47 +++- .../block-provider/TableSelectorProvider.tsx | 35 ++- .../client/src/block-provider/hooks/index.ts | 10 +- .../Configuration/AddCollectionAction.tsx | 9 +- .../collection-manager/hooks/useCollection.ts | 6 +- .../src/collection-manager/interfaces/m2o.tsx | 2 +- .../collection-manager/templates/index.tsx | 1 + .../src/collection-manager/templates/tree.tsx | 74 ++++++ .../templates/treeCollection.tsx | 0 .../src/collection-manager/templates/types.ts | 2 + .../client/src/collection-manager/types.ts | 1 + packages/core/client/src/locale/en_US.ts | 5 + packages/core/client/src/locale/ja_JP.ts | 5 + packages/core/client/src/locale/zh_CN.ts | 9 + .../antd/action/Action.Designer.tsx | 41 ++-- .../antd/record-picker/InputRecordPicker.tsx | 16 +- .../schema-component/antd/table-v2/Table.tsx | 53 ++++- .../antd/table-v2/TableBlockDesigner.tsx | 28 ++- .../antd/table-v2/TableSelectorDesigner.tsx | 24 +- .../antd/table-v2/demos/demo3.tsx | 110 +++++++++ .../schema-component/antd/table-v2/index.md | 6 +- .../schema-component/antd/table-v2/utils.ts | 23 ++ .../schema-initializer/SchemaInitializer.tsx | 110 ++++----- .../buttons/TableActionColumnInitializers.tsx | 14 ++ .../buttons/TableActionInitializers.tsx | 17 +- .../items/CreateChildInitializer.tsx | 55 +++++ .../items/CreateFormBlockInitializer.tsx | 84 +++---- .../items/CreateSubmitActionInitializer.tsx | 30 +-- .../items/ExpandActionInitializer.tsx | 89 +++++++ .../src/schema-initializer/items/index.tsx | 7 + .../client/src/schema-initializer/utils.ts | 14 +- .../core/database/src/__tests__/tree.test.ts | 217 ++++++++++++++++++ packages/core/database/src/collection.ts | 22 +- packages/core/database/src/database.ts | 7 +- .../database/src/listeners/adjacency-list.ts | 60 +++++ packages/core/database/src/listeners/index.ts | 7 + packages/core/database/src/repository.ts | 3 +- .../acl/src/__tests__/list-action.test.ts | 124 ++++++++++ packages/plugins/acl/src/server.ts | 12 +- .../hooks/afterCreateForForeignKeyField.ts | 10 +- 45 files changed, 1435 insertions(+), 197 deletions(-) create mode 100644 packages/core/client/src/collection-manager/templates/tree.tsx delete mode 100644 packages/core/client/src/collection-manager/templates/treeCollection.tsx create mode 100644 packages/core/client/src/schema-component/antd/table-v2/demos/demo3.tsx create mode 100644 packages/core/client/src/schema-initializer/items/CreateChildInitializer.tsx create mode 100644 packages/core/client/src/schema-initializer/items/ExpandActionInitializer.tsx create mode 100644 packages/core/database/src/__tests__/tree.test.ts create mode 100644 packages/core/database/src/listeners/adjacency-list.ts create mode 100644 packages/core/database/src/listeners/index.ts diff --git a/packages/core/actions/package.json b/packages/core/actions/package.json index 8407cf2ed..a997626ef 100644 --- a/packages/core/actions/package.json +++ b/packages/core/actions/package.json @@ -10,9 +10,6 @@ "@nocobase/database": "0.9.1-alpha.2", "@nocobase/resourcer": "0.9.1-alpha.2" }, - "devDependencies": { - "@nocobase/test": "0.9.1-alpha.2" - }, "repository": { "type": "git", "url": "git+https://github.com/nocobase/nocobase.git", diff --git a/packages/core/actions/src/__tests__/get-action.test.ts b/packages/core/actions/src/__tests__/get-action.test.ts index 6318d1a64..473b14c78 100644 --- a/packages/core/actions/src/__tests__/get-action.test.ts +++ b/packages/core/actions/src/__tests__/get-action.test.ts @@ -1,5 +1,5 @@ -import { MockServer, mockServer } from '@nocobase/test'; import { registerActions } from '@nocobase/actions'; +import { MockServer, mockServer } from './'; describe('get action', () => { let app: MockServer; @@ -10,9 +10,7 @@ describe('get action', () => { let Profile; beforeEach(async () => { - app = mockServer({ - dataWrapping: false, - }); + app = mockServer(); registerActions(app); PostTag = app.collection({ diff --git a/packages/core/actions/src/__tests__/list-action.test.ts b/packages/core/actions/src/__tests__/list-action.test.ts index a8d8d4595..3ffc8a77f 100644 --- a/packages/core/actions/src/__tests__/list-action.test.ts +++ b/packages/core/actions/src/__tests__/list-action.test.ts @@ -129,3 +129,201 @@ describe('list action', () => { expect(response.body.count).toEqual(0); }); }); + +describe('list-tree', () => { + let app; + beforeEach(async () => { + app = actionMockServer(); + registerActions(app); + }); + + afterEach(async () => { + await app.destroy(); + }); + + it('should be tree', async () => { + const values = [ + { + name: '1', + __index: '0', + children: [ + { + name: '1-1', + __index: '0.children.0', + children: [ + { + name: '1-1-1', + __index: '0.children.0.children.0', + children: [ + { + name: '1-1-1-1', + __index: '0.children.0.children.0.children.0', + }, + ], + }, + ], + }, + ], + }, + { + name: '2', + __index: '1', + children: [ + { + name: '2-1', + __index: '1.children.0', + children: [ + { + name: '2-1-1', + __index: '1.children.0.children.0', + children: [ + { + name: '2-1-1-1', + __index: '1.children.0.children.0.children.0', + }, + ], + }, + ], + }, + ], + }, + ]; + + const db = app.db; + const collection = db.collection({ + name: 'categories', + tree: 'adjacency-list', + fields: [ + { + type: 'string', + name: 'name', + }, + { + type: 'string', + name: 'description', + }, + { + type: 'belongsTo', + name: 'parent', + treeParent: true, + }, + { + type: 'hasMany', + name: 'children', + treeChildren: true, + }, + ], + }); + await db.sync(); + + await db.getRepository('categories').create({ + values, + }); + + const response = await app + .agent() + .resource('categories') + .list({ + tree: true, + fields: ['id', 'name'], + sort: ['id'], + }); + + expect(response.status).toEqual(200); + expect(response.body.rows).toMatchObject(values); + }); + + it.only('should be tree', async () => { + const values = [ + { + name: '1', + __index: '0', + children2: [ + { + name: '1-1', + __index: '0.children2.0', + children2: [ + { + name: '1-1-1', + __index: '0.children2.0.children2.0', + children2: [ + { + name: '1-1-1-1', + __index: '0.children2.0.children2.0.children2.0', + }, + ], + }, + ], + }, + ], + }, + { + name: '2', + __index: '1', + children2: [ + { + name: '2-1', + __index: '1.children2.0', + children2: [ + { + name: '2-1-1', + __index: '1.children2.0.children2.0', + children2: [ + { + name: '2-1-1-1', + __index: '1.children2.0.children2.0.children2.0', + }, + ], + }, + ], + }, + ], + }, + ]; + + const db = app.db; + const collection = db.collection({ + name: 'categories', + tree: 'adjacency-list', + fields: [ + { + type: 'string', + name: 'name', + }, + { + type: 'string', + name: 'description', + }, + { + type: 'belongsTo', + name: 'parent', + foreignKey: 'cid', + treeParent: true, + }, + { + type: 'hasMany', + name: 'children2', + foreignKey: 'cid', + treeChildren: true, + }, + ], + }); + await db.sync(); + + await db.getRepository('categories').create({ + values, + }); + + const response = await app + .agent() + .resource('categories') + .list({ + tree: true, + fields: ['id', 'name'], + sort: ['id'], + }); + + expect(response.status).toEqual(200); + expect(response.body.rows).toMatchObject(values); + }); +}); diff --git a/packages/core/actions/src/actions/list.ts b/packages/core/actions/src/actions/list.ts index 4e61dfc10..be006fb86 100644 --- a/packages/core/actions/src/actions/list.ts +++ b/packages/core/actions/src/actions/list.ts @@ -1,4 +1,4 @@ -import { ActionParams } from '@nocobase/resourcer'; +import { assign } from '@nocobase/utils'; import { Context } from '..'; import { getRepositoryFromParams } from '../utils'; @@ -22,9 +22,20 @@ function totalPage(total, pageSize): number { return Math.ceil(total / pageSize); } -function findArgs(params: ActionParams) { - const { fields, filter, appends, except, sort } = params; - return { filter, fields, appends, except, sort }; +function findArgs(ctx: Context) { + const resourceName = ctx.action.resourceName; + const params = ctx.action.params; + if (params.tree) { + const [collectionName, associationName] = resourceName.split('.'); + const collection = ctx.db.getCollection(resourceName); + // tree collection 或者关系表是 tree collection + if (collection.options.tree && !(associationName && collectionName === collection.name)) { + const foreignKey = collection.treeParentField?.foreignKey || 'parentId'; + assign(params, { filter: { [foreignKey]: null } }, { filter: 'andMerge' }); + } + } + const { tree, fields, filter, appends, except, sort } = params; + return { tree, filter, fields, appends, except, sort }; } async function listWithPagination(ctx: Context) { @@ -34,7 +45,7 @@ async function listWithPagination(ctx: Context) { const [rows, count] = await repository.findAndCount({ context: ctx, - ...findArgs(ctx.action.params), + ...findArgs(ctx), ...pageArgsToLimitArgs(parseInt(String(page)), parseInt(String(pageSize))), }); @@ -50,7 +61,7 @@ async function listWithPagination(ctx: Context) { async function listWithNonPaged(ctx: Context) { const repository = getRepositoryFromParams(ctx); - const rows = await repository.find({ context: ctx, ...findArgs(ctx.action.params) }); + const rows = await repository.find({ context: ctx, ...findArgs(ctx) }); ctx.body = rows; } diff --git a/packages/core/client/src/block-provider/FormBlockProvider.tsx b/packages/core/client/src/block-provider/FormBlockProvider.tsx index e239cc552..35b336048 100644 --- a/packages/core/client/src/block-provider/FormBlockProvider.tsx +++ b/packages/core/client/src/block-provider/FormBlockProvider.tsx @@ -7,6 +7,7 @@ import { useCollection } from '../collection-manager'; import { RecordProvider, useRecord } from '../record-provider'; import { useDesignable } from '../schema-component'; import { BlockProvider, useBlockRequestContext } from './BlockProvider'; +import { useActionContext } from '../schema-component'; export const FormBlockContext = createContext({}); @@ -88,6 +89,18 @@ export const useFormBlockContext = () => { export const useFormBlockProps = () => { const ctx = useFormBlockContext(); + const record = useRecord(); + const { fieldSchema } = useActionContext(); + const { addChild } = fieldSchema?.['x-component-props']||{}; + useEffect(() => { + if (addChild) { + ctx.form.query('parent').take((field) => { + field.disabled = true; + field.value = new Proxy({ ...record }, {}); + }); + } + }); + useEffect(() => { ctx.form.setInitialValues(ctx.service?.data?.data); }, []); diff --git a/packages/core/client/src/block-provider/TableBlockProvider.tsx b/packages/core/client/src/block-provider/TableBlockProvider.tsx index c7cfb335c..3bde23564 100644 --- a/packages/core/client/src/block-provider/TableBlockProvider.tsx +++ b/packages/core/client/src/block-provider/TableBlockProvider.tsx @@ -1,17 +1,18 @@ import { ArrayField, createForm } from '@formily/core'; import { FormContext, Schema, useField, useFieldSchema } from '@formily/react'; import uniq from 'lodash/uniq'; -import React, { createContext, useContext, useEffect, useMemo } from 'react'; +import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { useCollectionManager } from '../collection-manager'; +import { SchemaComponentOptions, useFixedSchema } from '../schema-component'; import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider'; -import { useFixedSchema } from '../schema-component'; export const TableBlockContext = createContext({}); const InternalTableBlockProvider = (props) => { - const { params, showIndex, dragSort, rowKey } = props; + const { params, showIndex, dragSort, rowKey, childrenColumnName } = props; const field = useField(); const { resource, service } = useBlockRequestContext(); + const [expandFlag, setExpandFlag] = useState(false); // if (service.loading) { // return ; // } @@ -26,6 +27,9 @@ const InternalTableBlockProvider = (props) => { showIndex, dragSort, rowKey, + expandFlag, + childrenColumnName, + setExpandFlag: () => setExpandFlag(!expandFlag), }} > @@ -81,21 +85,44 @@ export const useAssociationNames = (collection) => { }; export const TableBlockProvider = (props) => { + const resourceName = props.resource; const params = { ...props.params }; const appends = useAssociationNames(props.collection); - const form = useMemo(() => createForm(), []); + const fieldSchema = useFieldSchema(); + const { getCollection, getCollectionField } = useCollectionManager(); + const collection = getCollection(props.collection); + const { treeTable } = fieldSchema?.['x-decorator-props']||{}; if (props.dragSort) { params['sort'] = ['sort']; } + let childrenColumnName = 'children'; + if (collection.tree && treeTable !== false) { + if (resourceName.includes('.')) { + const f = getCollectionField(resourceName); + if (f?.treeChildren) { + childrenColumnName = f.name; + params['tree'] = true; + } + } else { + const f = collection.fields.find(f => f.treeChildren); + if (f) { + childrenColumnName = f.name; + } + params['tree'] = true; + } + } if (!Object.keys(params).includes('appends')) { params['appends'] = appends; } + const form = useMemo(() => createForm(), [treeTable]); return ( - - - - - + + + + + + + ); }; @@ -120,6 +147,7 @@ export const useTableBlockProps = () => { } }, [ctx?.service?.loading]); return { + childrenColumnName: ctx.childrenColumnName, loading: ctx?.service?.loading, showIndex: ctx.showIndex, dragSort: ctx.dragSort, @@ -132,6 +160,7 @@ export const useTableBlockProps = () => { } : false, onRowSelectionChange(selectedRowKeys) { + console.log(selectedRowKeys); ctx.field.data = ctx?.field?.data || {}; ctx.field.data.selectedRowKeys = selectedRowKeys; }, diff --git a/packages/core/client/src/block-provider/TableSelectorProvider.tsx b/packages/core/client/src/block-provider/TableSelectorProvider.tsx index 1dd28e5f7..8620d020b 100644 --- a/packages/core/client/src/block-provider/TableSelectorProvider.tsx +++ b/packages/core/client/src/block-provider/TableSelectorProvider.tsx @@ -1,11 +1,11 @@ import { ArrayField } from '@formily/core'; import { Schema, useField, useFieldSchema } from '@formily/react'; import uniq from 'lodash/uniq'; -import React, { createContext, useContext, useEffect } from 'react'; +import React, { createContext, useContext, useEffect, useState } from 'react'; import { useCollectionManager } from '../collection-manager'; import { RecordProvider, useRecord } from '../record-provider'; +import { SchemaComponentOptions } from '../schema-component'; import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider'; -import { useFormBlockContext } from './FormBlockProvider'; export const TableSelectorContext = createContext({}); @@ -13,6 +13,7 @@ const InternalTableSelectorProvider = (props) => { const { params, rowKey, extraFilter } = props; const field = useField(); const { resource, service } = useBlockRequestContext(); + const [expandFlag, setExpandFlag] = useState(false); // if (service.loading) { // return ; // } @@ -26,6 +27,10 @@ const InternalTableSelectorProvider = (props) => { params, extraFilter, rowKey, + expandFlag, + setExpandFlag: () => { + setExpandFlag(!expandFlag); + }, }} > @@ -99,19 +104,29 @@ const useAssociationNames = (collection) => { export const TableSelectorProvider = (props) => { const fieldSchema = useFieldSchema(); - const ctx = useFormBlockContext(); const { getCollectionJoinField, getCollectionFields } = useCollectionManager(); const record = useRecord(); - + const { getCollection } = useCollectionManager(); + const collection = getCollection(props.collection); + const { treeTable } = fieldSchema?.['x-decorator-props'] || {}; const collectionFieldSchema = recursiveParent(fieldSchema, 'CollectionField'); - // const value = ctx.form.query(collectionFieldSchema?.name).value(); const collectionField = getCollectionJoinField(collectionFieldSchema?.['x-collection-field']); - const params = { ...props.params }; const appends = useAssociationNames(props.collection); if (props.dragSort) { params['sort'] = ['sort']; } + if (collection.tree && treeTable !== false) { + params['tree'] = true; + if (collectionFieldSchema.name === 'parent') { + params.filter = { + ...(params.filter ?? {}), + id: record.id && { + $ne: record.id, + }, + }; + } + } if (!Object.keys(params).includes('appends')) { params['appends'] = appends; } @@ -181,9 +196,11 @@ export const TableSelectorProvider = (props) => { } } return ( - - - + + + + + ); }; diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index b12a751bc..2e5b73fe5 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -128,12 +128,12 @@ function getFormValues(filterByTk, field, form, fieldNames, getField, resource) export const useCreateActionProps = () => { const form = useForm(); const { field, resource, __parent } = useBlockRequestContext(); - const { visible, setVisible } = useActionContext(); + const { visible, setVisible, fieldSchema } = useActionContext(); const history = useHistory(); const { t } = useTranslation(); const actionSchema = useFieldSchema(); const actionField = useField(); - const { fields, getField } = useCollection(); + const { fields, getField, getTreeParentField } = useCollection(); const compile = useCompile(); const filterByTk = useFilterByTk(); const currentRecord = useRecord(); @@ -148,11 +148,17 @@ export const useCreateActionProps = () => { overwriteValues, skipValidator, } = actionSchema?.['x-action-settings'] ?? {}; + const { addChild } = fieldSchema?.['x-component-props']; const assignedValues = parse(originalAssignedValues)({ currentTime: new Date(), currentRecord, currentUser }); if (!skipValidator) { await form.submit(); } const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource); + if (addChild) { + const treeParentField = getTreeParentField(); + values[treeParentField?.name ?? 'parent'] = currentRecord; + values[treeParentField?.foreignKey ?? 'parentId'] = currentRecord.id; + } actionField.data = field.data || {}; actionField.data.loading = true; try { diff --git a/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx b/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx index 9bf610f1c..844425f97 100644 --- a/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx @@ -84,7 +84,7 @@ const getSchema = (schema, category, compile): ISchema => { 'x-component': 'Action', 'x-component-props': { type: 'primary', - useAction: () => useCreateCollection(), + useAction: () => useCreateCollection(schema), }, }, }, @@ -188,16 +188,19 @@ const useDefaultCollectionFields = (values) => { return defaults; }; -const useCreateCollection = () => { +const useCreateCollection = (schema?: any) => { const form = useForm(); const { refreshCM } = useCollectionManager(); const ctx = useActionContext(); const { refresh } = useResourceActionContext(); - const { resource } = useResourceContext(); + const { resource, collection } = useResourceContext(); return { async run() { await form.submit(); const values = cloneDeep(form.values); + if (schema?.events?.beforeSubmit) { + schema.events.beforeSubmit(values); + } const fields = useDefaultCollectionFields(values); if (values.autoCreateReverseField) { } else { diff --git a/packages/core/client/src/collection-manager/hooks/useCollection.ts b/packages/core/client/src/collection-manager/hooks/useCollection.ts index ae4532c7c..b8cee4244 100644 --- a/packages/core/client/src/collection-manager/hooks/useCollection.ts +++ b/packages/core/client/src/collection-manager/hooks/useCollection.ts @@ -28,9 +28,13 @@ export const useCollection = () => { ...collection, resource, getField(name: SchemaKey): CollectionFieldOptions { - const fields = totalFields; + const fields = totalFields as any[]; return fields?.find((field) => field.name === name); }, + getTreeParentField() { + const fields = totalFields; + return fields?.find((field) => field.treeParent); + }, fields: totalFields, getPrimaryKey: () => { if (collection.targetKey || collection.filterTargetKey) { diff --git a/packages/core/client/src/collection-manager/interfaces/m2o.tsx b/packages/core/client/src/collection-manager/interfaces/m2o.tsx index 5d2d9c7cd..65268c3d8 100644 --- a/packages/core/client/src/collection-manager/interfaces/m2o.tsx +++ b/packages/core/client/src/collection-manager/interfaces/m2o.tsx @@ -5,7 +5,7 @@ import { recordPickerSelector, recordPickerViewer, relationshipType, - reverseFieldProperties, + reverseFieldProperties } from './properties'; import { IField } from './types'; diff --git a/packages/core/client/src/collection-manager/templates/index.tsx b/packages/core/client/src/collection-manager/templates/index.tsx index 09afc72d4..2bf123f94 100644 --- a/packages/core/client/src/collection-manager/templates/index.tsx +++ b/packages/core/client/src/collection-manager/templates/index.tsx @@ -1,3 +1,4 @@ export * from './calendar'; export * from './general'; +export * from './tree'; diff --git a/packages/core/client/src/collection-manager/templates/tree.tsx b/packages/core/client/src/collection-manager/templates/tree.tsx new file mode 100644 index 000000000..a89a78193 --- /dev/null +++ b/packages/core/client/src/collection-manager/templates/tree.tsx @@ -0,0 +1,74 @@ +import { getConfigurableProperties } from './properties'; +import { ICollectionTemplate } from './types'; + +export const tree: ICollectionTemplate = { + name: 'tree', + title: '{{t("Tree collection")}}', + order: 3, + color: 'blue', + default: { + tree: 'adjacencyList', + fields: [ + { + interface: 'integer', + name: 'parentId', + type: 'bigInt', + isForeignKey: true, + uiSchema: { + type: 'number', + title: '{{t("Parent ID")}}', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + }, + }, + { + interface: 'm2o', + type: 'belongsTo', + name: 'parent', + foreignKey: 'parentId', + treeParent: true, + uiSchema: { + title: '{{t("Parent")}}', + 'x-component': 'RecordPicker', + 'x-component-props': { + // mode: 'tags', + multiple: false, + fieldNames: { + label: 'id', + value: 'id', + }, + }, + }, + }, + { + interface: 'o2m', + type: 'hasMany', + name: 'children', + foreignKey: 'parentId', + treeChildren: true, + uiSchema: { + title: '{{t("Children")}}', + 'x-component': 'RecordPicker', + 'x-component-props': { + // mode: 'tags', + multiple: true, + fieldNames: { + label: 'id', + value: 'id', + }, + }, + }, + }, + ], + }, + events: { + beforeSubmit(values) { + if (Array.isArray(values?.fields)) { + values?.fields.map((f) => { + f.target = values.name; + }); + } + }, + }, + configurableProperties: getConfigurableProperties('title', 'name', 'inherits', 'category', 'moreOptions'), +}; diff --git a/packages/core/client/src/collection-manager/templates/treeCollection.tsx b/packages/core/client/src/collection-manager/templates/treeCollection.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/core/client/src/collection-manager/templates/types.ts b/packages/core/client/src/collection-manager/templates/types.ts index 3047795e7..8528a463b 100644 --- a/packages/core/client/src/collection-manager/templates/types.ts +++ b/packages/core/client/src/collection-manager/templates/types.ts @@ -9,6 +9,7 @@ export interface ICollectionTemplate { order?: number; /** 默认配置 */ default?: CollectionOptions; + events?: any; /** UI 可配置的 CollectionOptions 参数(添加或编辑的 Collection 表单的字段) */ configurableProperties?: Record; /** 当前模板可用的字段类型 */ @@ -48,4 +49,5 @@ interface CollectionOptions { inherits?: string | string[]; /* 字段列表 */ fields?: FieldOptions[]; + [key: string]: any; } diff --git a/packages/core/client/src/collection-manager/types.ts b/packages/core/client/src/collection-manager/types.ts index 17986f239..21c361cb0 100644 --- a/packages/core/client/src/collection-manager/types.ts +++ b/packages/core/client/src/collection-manager/types.ts @@ -24,6 +24,7 @@ export interface CollectionOptions { sortable?: any; fields?: FieldOptions[]; inherits?: string[]; + tree?: string; } export interface ICollectionProviderProps { diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts index 854f5079b..cf7383c18 100644 --- a/packages/core/client/src/locale/en_US.ts +++ b/packages/core/client/src/locale/en_US.ts @@ -108,6 +108,11 @@ export default { "Add block": "Add block", "Add new": "Add new", "Add record": "Add record", + 'Add child':'Add child', + 'Collapse all':'Collapse all', + 'Expand all':'Expand all', + 'Expand/Collapse':'Expand/Collapse', + "Tree table":"Tree table", "Custom field display name": "Custom field display name", "Display fields": "Display collection fields", "Edit record": "Edit record", diff --git a/packages/core/client/src/locale/ja_JP.ts b/packages/core/client/src/locale/ja_JP.ts index 0ee613a1b..f5123cf84 100644 --- a/packages/core/client/src/locale/ja_JP.ts +++ b/packages/core/client/src/locale/ja_JP.ts @@ -42,6 +42,11 @@ export default { "Category name":"分類名", "Delete category":"分類の削除", "Collection category":"Collection category", + 'Add child':'サブレコードの追加', + 'Collapse all':'すべて閉じる', + 'Expand all':'すべて展開', + 'Expand/Collapse':'展開と終了', + "Tree table":"ツリーテーブル", "Visible":"表示", "Read only":"読み取り専用(編集不可)", "Easy reading":"読み取り専用(読取りモード)", diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index f692c31b4..78d61e586 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -58,6 +58,15 @@ export default { "Sort":"排序", "Categories":"数据表类别", "Category name":"分类名称", + 'Add child':'添加子记录', + 'Collapse all':'全部收起', + 'Expand all':'全部展开', + 'Expand/Collapse':'展开/折叠', + 'Tree collection': '树结构表', + "Tree table":"树表格", + 'Parent ID': '父记录ID', + 'Parent': '父记录', + 'Children': '子记录', "Roles & Permissions": "角色和权限", "Edit profile": "个人资料", "Change password": "修改密码", diff --git a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx index 556a88e75..ef64b8739 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx @@ -50,6 +50,7 @@ export const ActionDesigner = (props) => { const actionType = fieldSchema['x-action'] ?? ''; const isLinkageAction = Object.keys(useFormBlockContext()).length > 0 && Object.keys(useRecord()).length > 0; const isChildCollectionAction = getChildrenCollections(name).length > 0 && fieldSchema['x-action'] === 'create'; + const isSupportEditButton = fieldSchema['x-action'] !== 'expandAll'; useEffect(() => { const schemaUid = uid(); const schema: ISchema = { @@ -85,6 +86,7 @@ export const ActionDesigner = (props) => { title: t('Button title'), default: fieldSchema.title, 'x-component-props': {}, + 'x-visible': isSupportEditButton, // description: `原字段标题:${collectionField?.uiSchema?.title}`, }, icon: { @@ -93,6 +95,7 @@ export const ActionDesigner = (props) => { title: t('Button icon'), default: fieldSchema?.['x-component-props']?.icon, 'x-component-props': {}, + 'x-visible': isSupportEditButton, // description: `原字段标题:${collectionField?.uiSchema?.title}`, }, type: { @@ -114,27 +117,25 @@ export const ActionDesigner = (props) => { } as ISchema } onSubmit={({ title, icon, type }) => { - if (title) { - fieldSchema.title = title; - field.title = title; - field.componentProps.icon = icon; - field.componentProps.danger = type === 'danger'; - field.componentProps.type = type; - fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; - fieldSchema['x-component-props'].icon = icon; - fieldSchema['x-component-props'].danger = type === 'danger'; - fieldSchema['x-component-props'].type = type; - dn.emit('patch', { - schema: { - ['x-uid']: fieldSchema['x-uid'], - title, - 'x-component-props': { - ...fieldSchema['x-component-props'], - }, + fieldSchema.title = title; + field.title = title; + field.componentProps.icon = icon; + field.componentProps.danger = type === 'danger'; + field.componentProps.type = type; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props'].icon = icon; + fieldSchema['x-component-props'].danger = type === 'danger'; + fieldSchema['x-component-props'].type = type; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + title, + 'x-component-props': { + ...fieldSchema['x-component-props'], }, - }); - dn.refresh(); - } + }, + }); + dn.refresh(); }} /> {isLinkageAction && } diff --git a/packages/core/client/src/schema-component/antd/record-picker/InputRecordPicker.tsx b/packages/core/client/src/schema-component/antd/record-picker/InputRecordPicker.tsx index 320f8353a..ada0980e9 100644 --- a/packages/core/client/src/schema-component/antd/record-picker/InputRecordPicker.tsx +++ b/packages/core/client/src/schema-component/antd/record-picker/InputRecordPicker.tsx @@ -12,6 +12,17 @@ import { useFieldNames } from './useFieldNames'; import { getLabelFormatValue, useLabelUiSchema } from './util'; const RecordPickerContext = createContext(null); +function flatData(data) { + let newArr = []; + for (let i = 0; i < data.length; i++) { + const children = data[i]['children']; + if (Array.isArray(children)) { + newArr.push(...flatData(children)); + } + newArr.push({ ...data[i] }); + } + return newArr; +} const useTableSelectorProps = () => { const field = useField(); @@ -28,11 +39,12 @@ const useTableSelectorProps = () => { }, onRowSelectionChange(selectedRowKeys, selectedRows) { if (multiple) { - const scopeRows = field.value || []; + const scopeRows = flatData(field.value) || []; const allSelectedRows = rcSelectRows || []; const otherRows = differenceBy(allSelectedRows, scopeRows, rowKey || 'id'); const unionSelectedRows = unionBy(otherRows, selectedRows, rowKey || 'id'); const unionSelectedRowKeys = unionSelectedRows.map((item) => item[rowKey || 'id']); + console.log(unionSelectedRows, unionSelectedRowKeys); setSelectedRows?.(unionSelectedRows); onRowSelectionChange?.(unionSelectedRowKeys, unionSelectedRows); } else { @@ -92,7 +104,7 @@ export const InputRecordPicker: React.FC = (props) => { setOptions(opts); setSelectedRows(opts); } - }, [value,fieldNames?.label]); + }, [value, fieldNames?.label]); const getValue = () => { if (multiple == null) return null; diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx index c2cbc9675..3a462be90 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx @@ -10,9 +10,15 @@ import { default as classNames, default as cls } from 'classnames'; import React, { RefCallback, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { DndContext, useDesignable } from '../..'; -import { RecordIndexProvider, RecordProvider, useSchemaInitializer } from '../../../'; +import { + RecordIndexProvider, + RecordProvider, + useSchemaInitializer, + useTableBlockContext, + useTableSelectorContext +} from '../../../'; import { useACLFieldWhitelist } from '../../../acl/ACLProvider'; -import { isCollectionFieldComponent, isColumnComponent } from './utils'; +import { extractIndex, getIdsWithChildren, isCollectionFieldComponent, isColumnComponent } from './utils'; const useTableColumns = () => { const field = useField(); @@ -106,7 +112,7 @@ const TableIndex = (props) => { const { index } = props; return (
- {index + 1} + {index}
); }; @@ -162,10 +168,16 @@ export const Table: any = observer((props: any) => { required, ...others } = { ...others1, ...others2 } as any; + const schema = useFieldSchema(); + const isTableSelector = schema?.parent?.['x-decorator'] === 'TableSelectorProvider'; + const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext(); + const { expandFlag } = ctx; const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {})); const paginationProps = usePaginationProps(pagination1, pagination2); const requiredValidator = field.required || required; - + const { treeTable } = schema?.parent?.['x-decorator-props'] || {}; + const [expandedKeys, setExpandesKeys] = useState([]); + const [allIncludesChildren, setAllIncludesChildren] = useState([]); useEffect(() => { field.setValidator((value) => { if (requiredValidator) { @@ -174,6 +186,26 @@ export const Table: any = observer((props: any) => { return; }); }, [requiredValidator]); + // useEffect(() => { + // const data = field.value; + // field.value = null; + // field.value = data; + // }, [treeTable]); + + useEffect(() => { + if (treeTable !== false) { + const keys = getIdsWithChildren(field.value?.slice()); + setAllIncludesChildren(keys); + } + }, [field.value]); + useEffect(() => { + if (expandFlag) { + setExpandesKeys(allIncludesChildren); + } else { + setExpandesKeys([]); + } + }, [expandFlag]); + const components = useMemo(() => { return { header: { @@ -275,7 +307,10 @@ export const Table: any = observer((props: any) => { const current = props?.pagination?.current; const pageSize = props?.pagination?.pageSize || 20; if (current) { - index = index + (current - 1) * pageSize; + index = index + (current - 1) * pageSize + 1; + } + if (record.__index) { + index = extractIndex(record.__index); } return (
{ css` position: relative; display: flex; + float: left; align-items: center; justify-content: space-evenly; padding-right: 8px; @@ -419,6 +455,13 @@ export const Table: any = observer((props: any) => { tableLayout={'auto'} scroll={scroll} columns={columns} + expandable={{ + onExpand: (flag, record) => { + const newKeys = flag ? [...expandedKeys, record.id] : expandedKeys.filter((i) => record.id !== i); + setExpandesKeys(newKeys); + }, + expandedRowKeys: expandedKeys, + }} dataSource={field?.value?.slice?.()} /> diff --git a/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx b/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx index ab2def01e..cd308e1cb 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useTableBlockContext } from '../../../block-provider'; import { mergeFilter } from '../../../block-provider/SharedFilterProvider'; -import { useCollection } from '../../../collection-manager'; +import { useCollection, useCollectionManager } from '../../../collection-manager'; import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { useSchemaTemplate } from '../../../schema-templates'; @@ -13,6 +13,7 @@ import { useFixedBlockDesignerSetting } from '../page'; export const TableBlockDesigner = () => { const { name, title, sortable } = useCollection(); + const { getCollectionField } = useCollectionManager(); const field = useField(); const fieldSchema = useFieldSchema(); const dataSource = useCollectionFilterOptions(name); @@ -36,12 +37,33 @@ export const TableBlockDesigner = () => { }; }); const template = useSchemaTemplate(); - const { dragSort } = field.decoratorProps; + const collection = useCollection(); + const { dragSort, resource } = field.decoratorProps; + const treeChildren = resource.includes('.') ? getCollectionField(resource)?.treeChildren : !!collection?.tree; const fixedBlockDesignerSetting = useFixedBlockDesignerSetting(); - return ( + {collection?.tree && ( + { + field.decoratorProps.treeTable = flag; + fieldSchema['x-decorator-props'].treeTable = flag; + const params = { + ...service.params?.[0], + tree: flag ? true : null, + }; + dn.emit('patch', { + schema: fieldSchema, + }); + dn.refresh(); + service.run(params); + }} + /> + )} {sortable && ( { const { name, title } = useCollection(); @@ -33,6 +33,7 @@ export const TableSelectorDesigner = () => { }; }); const template = useSchemaTemplate(); + const collection = useCollection(); const { dragSort } = field.decoratorProps; return ( @@ -77,6 +78,27 @@ export const TableSelectorDesigner = () => { }); }} /> + {collection?.tree && ( + { + field.form.clearFormGraph(`${field.address}.*`); + field.decoratorProps.treeTable = flag; + fieldSchema['x-decorator-props'].treeTable = flag; + const params = { + ...service.params?.[0], + tree: flag ? true : null, + }; + dn.emit('patch', { + schema: fieldSchema, + }); + dn.refresh(); + service.run(params); + }} + /> + )} {!dragSort && ( { + const { rowKey } = props; + const field = useField(); + const schema = useFieldSchema(); + const columnSchemas = schema.reduceProperties((buf, s) => { + if (s['x-component'] === 'ArrayTable.Column') { + buf.push(s); + } + return buf; + }, []); + + const columns = columnSchemas.map((s) => { + return { + render: (value, record) => { + return ; + }, + } as TableColumnType; + }); + + return ; +}); + +const Value = connect((props) => { + return
  • value: {props.value}
  • ; +}); + +const schema: ISchema = { + type: 'object', + properties: { + objArr: { + type: 'array', + default: [ + { __path: '0', id: 1, value: 't1' }, + { + __path: '1', + id: 2, + value: 't2', + children: [ + { + __path: '1.children.0', + id: 5, + value: 't5', + parentId: 2, + }, + ], + }, + { + __path: '2', + id: 3, + value: 't3', + children: [ + { + __path: '2.children.0', + id: 4, + value: 't4', + parentId: 3, + children: [ + { + __path: '2.children.0.children.0', + id: 6, + value: 't6', + parentId: 4, + }, + { + __path: '2.children.0.children.1', + id: 7, + value: 't7', + parentId: 4, + }, + ], + }, + ], + }, + ], + 'x-component': 'ArrayTable', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + properties: { + c1: { + type: 'void', + 'x-component': 'ArrayTable.Column', + properties: { + value: { + type: 'string', + 'x-component': 'Value', + }, + }, + }, + }, + }, + }, +}; + +export default () => { + return ( + + + + ); +}; diff --git a/packages/core/client/src/schema-component/antd/table-v2/index.md b/packages/core/client/src/schema-component/antd/table-v2/index.md index 04fd3c51d..3d22cef19 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/index.md +++ b/packages/core/client/src/schema-component/antd/table-v2/index.md @@ -9,8 +9,4 @@ group: ## TableBlock - - -## TableField - - + diff --git a/packages/core/client/src/schema-component/antd/table-v2/utils.ts b/packages/core/client/src/schema-component/antd/table-v2/utils.ts index dd35c8876..31599cfd3 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/utils.ts +++ b/packages/core/client/src/schema-component/antd/table-v2/utils.ts @@ -7,3 +7,26 @@ export const isCollectionFieldComponent = (schema: ISchema) => { export const isColumnComponent = (schema: Schema) => { return schema['x-component']?.endsWith('.Column') > -1; }; + +export function extractIndex(str) { + const numbers = []; + str?.split('.').forEach(function (element) { + if (!isNaN(element)) { + numbers.push(String(Number(element) + 1)); + } + }); + return numbers.join('.'); +} + +export function getIdsWithChildren(nodes) { + const ids = []; + if (nodes) { + for (let node of nodes) { + if (node.children && node.children.length > 0) { + ids.push(node.id); + ids.push(...getIdsWithChildren(node.children)); + } + } + } + return ids; +} diff --git a/packages/core/client/src/schema-initializer/SchemaInitializer.tsx b/packages/core/client/src/schema-initializer/SchemaInitializer.tsx index ac206dac4..5691c0812 100644 --- a/packages/core/client/src/schema-initializer/SchemaInitializer.tsx +++ b/packages/core/client/src/schema-initializer/SchemaInitializer.tsx @@ -10,7 +10,7 @@ import { SchemaInitializerButtonProps, SchemaInitializerItemComponent, SchemaInitializerItemOptions, - SchemaInitializerItemProps, + SchemaInitializerItemProps } from './types'; const defaultWrap = (s: ISchema) => s; @@ -47,59 +47,63 @@ SchemaInitializer.Button = observer((props: SchemaInitializerButtonProps) => { } }; const renderItems = (items: any) => { - return items?.map((item, indexA) => { - if (item.type === 'divider') { - return ; - } - if (item.type === 'item' && item.component) { - const Component = findComponent(item.component); - item.key = `${item.key || item.title}-${indexA}`; - return ( - Component && ( - - { + return v && (v?.visible ? v.visible() : true); + }) + ?.map((item, indexA) => { + if (item.type === 'divider') { + return ; + } + if (item.type === 'item' && item.component) { + const Component = findComponent(item.component); + item.key = `${item.key || item.title}-${indexA}`; + return ( + Component && ( + - - ) - ); - } - if (item.type === 'itemGroup') { - return ( - !!item.children?.length && ( - - {renderItems(item.children)} - - ) - ); - } - if (item.type === 'subMenu') { - return ( - !!item.children?.length && ( - - {renderItems(item.children)} - - ) - ); - } - }); + > + + + ) + ); + } + if (item.type === 'itemGroup') { + return ( + !!item.children?.length && ( + + {renderItems(item.children)} + + ) + ); + } + if (item.type === 'subMenu') { + return ( + !!item.children?.length && ( + + {renderItems(item.children)} + + ) + ); + } + }); }; const menu = {renderItems(items)}; if (!designable && props.designable !== true) { diff --git a/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx index 67efeeef8..669d576a0 100644 --- a/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { SchemaInitializer, SchemaSettings } from '../..'; import { useAPIClient } from '../../api-client'; +import { useCollection } from '../../collection-manager'; import { createDesignable, useDesignable } from '../../schema-component'; export const Resizable = (props) => { @@ -49,6 +50,8 @@ export const TableActionColumnInitializers = (props: any) => { const api = useAPIClient(); const { refresh } = useDesignable(); const { t } = useTranslation(); + const collection = useCollection(); + const { treeTable } = fieldSchema?.parent?.parent['x-decorator-props']||{}; return ( { 'x-decorator': 'ACLActionProvider', }, }, + collection.tree && + treeTable !== false && { + type: 'item', + title: t('Add child'), + component: 'CreateChildInitializer', + schema: { + 'x-component': 'Action.Link', + 'x-action': 'create', + 'x-decorator': 'ACLActionProvider', + }, + }, ], }, { diff --git a/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx index feef2aaa6..40c6c493d 100644 --- a/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx @@ -1,4 +1,5 @@ -import { Schema } from '@formily/react'; +import { Schema, useFieldSchema } from '@formily/react'; +import { useCollection } from '../../'; // 表格操作配置 export const TableActionInitializers = { @@ -49,6 +50,20 @@ export const TableActionInitializers = { 'x-align': 'right', }, }, + { + type: 'item', + title: "{{t('Expand/Collapse')}}", + component: 'ExpandActionInitializer', + schema: { + 'x-align': 'right', + }, + visible: () => { + const schema = useFieldSchema(); + const collection = useCollection(); + const { treeTable } = schema?.parent?.['x-decorator-props']||{}; + return collection.tree && treeTable !== false; + }, + }, ], }, { diff --git a/packages/core/client/src/schema-initializer/items/CreateChildInitializer.tsx b/packages/core/client/src/schema-initializer/items/CreateChildInitializer.tsx new file mode 100644 index 000000000..dfdfc726a --- /dev/null +++ b/packages/core/client/src/schema-initializer/items/CreateChildInitializer.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { ActionInitializer } from './ActionInitializer'; +export const CreateChildInitializer = (props) => { + + const schema = { + type: 'void', + title: '{{ t("Add child") }}', + 'x-action': 'create', + 'x-designer': 'Action.Designer', + 'x-component': 'Action', + 'x-visible': '{{treeTable}}', + 'x-component-props': { + icon: 'PlusOutlined', + openMode: 'drawer', + type: 'primary', + addChild: true, + }, + properties: { + drawer: { + type: 'void', + title: '{{ t("Add record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'TabPaneInitializersForCreateFormBlock', + properties: { + tab1: { + type: 'void', + title: '{{t("Add new")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'CreateFormBlockInitializers', + properties: {}, + }, + }, + }, + }, + }, + }, + }, + }, + }; + return ; +}; diff --git a/packages/core/client/src/schema-initializer/items/CreateFormBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/CreateFormBlockInitializer.tsx index 5ae60bcad..86b0093d5 100644 --- a/packages/core/client/src/schema-initializer/items/CreateFormBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/CreateFormBlockInitializer.tsx @@ -1,49 +1,49 @@ -import React from "react"; +import React from 'react'; import { FormOutlined } from '@ant-design/icons'; -import { useBlockAssociationContext } from "../../block-provider"; -import { useCollection } from "../../collection-manager"; -import { useSchemaTemplateManager } from "../../schema-templates"; -import { SchemaInitializer } from "../SchemaInitializer"; -import { createFormBlockSchema, useRecordCollectionDataSourceItems } from "../utils"; +import { useBlockAssociationContext } from '../../block-provider'; +import { useCollection } from '../../collection-manager'; +import { useSchemaTemplateManager } from '../../schema-templates'; +import { SchemaInitializer } from '../SchemaInitializer'; +import { createFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; export const CreateFormBlockInitializer = (props) => { - const { onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; - const { getTemplateSchemaByMode } = useSchemaTemplateManager(); - const association = useBlockAssociationContext(); - const collection = useCollection(); - return ( - } - {...others} - onClick={async ({ item }) => { - if (item.template) { - const s = await getTemplateSchemaByMode(item); - if (item.template.componentName === 'FormItem') { - const blockSchema = createFormBlockSchema({ - actionInitializers: 'CreateFormActionInitializers', - association, - collection: collection.name, - template: s, - }); - if (item.mode === 'reference') { - blockSchema['x-template-key'] = item.template.key; - } - insert(blockSchema); - } else { - insert(s); + const { onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; + const { getTemplateSchemaByMode } = useSchemaTemplateManager(); + const association = useBlockAssociationContext(); + const collection = useCollection(); + return ( + } + {...others} + onClick={async ({ item }) => { + if (item.template) { + const s = await getTemplateSchemaByMode(item); + if (item.template.componentName === 'FormItem') { + const blockSchema = createFormBlockSchema({ + actionInitializers: 'CreateFormActionInitializers', + association, + collection: collection.name, + template: s, + }); + if (item.mode === 'reference') { + blockSchema['x-template-key'] = item.template.key; } + insert(blockSchema); } else { - insert( - createFormBlockSchema({ - actionInitializers: 'CreateFormActionInitializers', - association, - collection: collection.name, - }), - ); + insert(s); } - }} - items={useRecordCollectionDataSourceItems('FormItem')} - /> - ); - }; + } else { + insert( + createFormBlockSchema({ + actionInitializers: 'CreateFormActionInitializers', + association, + collection: collection.name, + }), + ); + } + }} + items={useRecordCollectionDataSourceItems('FormItem')} + /> + ); +}; diff --git a/packages/core/client/src/schema-initializer/items/CreateSubmitActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/CreateSubmitActionInitializer.tsx index b845f9adb..740f3688f 100644 --- a/packages/core/client/src/schema-initializer/items/CreateSubmitActionInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/CreateSubmitActionInitializer.tsx @@ -1,18 +1,18 @@ -import React from "react"; - -import { ActionInitializer } from "./ActionInitializer"; +import React from 'react'; +import { useFieldSchema } from '@formily/react'; +import { ActionInitializer } from './ActionInitializer'; export const CreateSubmitActionInitializer = (props) => { - const schema = { - title: '{{ t("Submit") }}', - 'x-action': 'submit', - 'x-component': 'Action', - 'x-designer': 'Action.Designer', - 'x-component-props': { - type: 'primary', - htmlType: 'submit', - useProps: '{{ useCreateActionProps }}', - }, - }; - return ; + const schema = { + title: '{{ t("Submit") }}', + 'x-action': 'submit', + 'x-component': 'Action', + 'x-designer': 'Action.Designer', + 'x-component-props': { + type: 'primary', + htmlType: 'submit', + useProps: '{{ useCreateActionProps }}', + }, }; + return ; +}; diff --git a/packages/core/client/src/schema-initializer/items/ExpandActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/ExpandActionInitializer.tsx new file mode 100644 index 000000000..7180d2f7f --- /dev/null +++ b/packages/core/client/src/schema-initializer/items/ExpandActionInitializer.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { Button } from 'antd'; +import { css } from '@emotion/css'; +import { useTranslation } from 'react-i18next'; +import { useFieldSchema } from '@formily/react'; +import { ActionInitializer } from './ActionInitializer'; +import { useTableBlockContext, useTableSelectorContext } from '../../'; +import { NodeCollapseOutlined, NodeExpandOutlined } from '@ant-design/icons'; + +export const ExpandActionInitializer = (props) => { + const schema = { + 'x-action': 'expandAll', + 'x-component': 'Action', + 'x-designer': 'Action.Designer', + 'x-component-props': { + icon: '{{ expandIcon}}', + useProps: '{{ useExpandAllActionProps }}', + component: 'ExpandActionComponent', + useAction: () => { + return { + run() {}, + }; + }, + }, + }; + return ; +}; + +const actionDesignerCss = css` + position: relative; + &:hover { + .general-schema-designer { + display: block; + } + } + .general-schema-designer { + position: absolute; + z-index: 999; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: none; + background: rgba(241, 139, 98, 0.06); + border: 0; + top: 0; + bottom: 0; + left: 0; + right: 0; + pointer-events: none; + > .general-schema-designer-icons { + position: absolute; + right: 2px; + top: 2px; + line-height: 16px; + pointer-events: all; + .ant-space-item { + background-color: #f18b62; + color: #fff; + line-height: 16px; + width: 16px; + padding-left: 1px; + } + } + } +`; + +export const ExpandActionComponent = (props) => { + const { t } = useTranslation(); + const schema = useFieldSchema(); + const isTableSelector = schema.parent?.parent?.['x-decorator'] === 'TableSelectorProvider'; + const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext(); + return ( +
    + {ctx.params['tree'] && ( + + )} +
    + ); +}; diff --git a/packages/core/client/src/schema-initializer/items/index.tsx b/packages/core/client/src/schema-initializer/items/index.tsx index d844fe7c8..f8e7a76f9 100644 --- a/packages/core/client/src/schema-initializer/items/index.tsx +++ b/packages/core/client/src/schema-initializer/items/index.tsx @@ -42,3 +42,10 @@ export * from './TableSelectorInitializer'; export * from './UpdateActionInitializer'; export * from './UpdateSubmitActionInitializer'; export * from './ViewActionInitializer'; +export * from './CreateChildInitializer'; +export * from './ExpandActionInitializer'; +// association filter +export * from '../../schema-component/antd/association-filter/AssociationFilter'; +export * from '../../schema-component/antd/association-filter/ActionBarAssociationFilterAction'; +export * from '../../schema-component/antd/association-filter/AssociationFilterDesignerDisplayField'; +export * from '../../schema-component/antd/association-filter/AssociationFilterDesignerDelete'; diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts index f943883bf..36bc962ba 100644 --- a/packages/core/client/src/schema-initializer/utils.ts +++ b/packages/core/client/src/schema-initializer/utils.ts @@ -76,7 +76,9 @@ export const useTableColumnInitializerFields = () => { const { name, currentFields = [] } = useCollection(); const { getInterface } = useCollectionManager(); return currentFields - .filter((field) => field?.interface && field?.interface !== 'subTable' && !field?.isForeignKey) + .filter( + (field) => field?.interface && field?.interface !== 'subTable' && !field?.isForeignKey && !field?.treeChildren, + ) .map((field) => { const interfaceConfig = getInterface(field.interface); const schema = { @@ -113,7 +115,9 @@ export const useAssociatedTableColumnInitializerFields = () => { const subFields = getCollectionFields(field.target); const items = subFields // ?.filter((subField) => subField?.interface && !['o2o', 'oho', 'obo', 'o2m', 'm2o', 'subTable', 'linkTo'].includes(subField?.interface)) - ?.filter((subField) => subField?.interface && !['subTable'].includes(subField?.interface)) + ?.filter( + (subField) => subField?.interface && !['subTable'].includes(subField?.interface) && !subField?.treeChildren, + ) ?.map((subField) => { const interfaceConfig = getInterface(subField.interface); const schema = { @@ -198,7 +202,7 @@ export const useFormItemInitializerFields = (options?: any) => { const { snapshot } = useActionContext(); return currentFields - ?.filter((field) => field?.interface && !field?.isForeignKey) + ?.filter((field) => field?.interface && !field?.isForeignKey && !field?.treeChildren) ?.map((field) => { const interfaceConfig = getInterface(field.interface); const schema = { @@ -246,7 +250,9 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => { ?.map((field) => { const subFields = getCollectionFields(field.target); const items = subFields - ?.filter((subField) => subField?.interface && !['subTable'].includes(subField?.interface)) + ?.filter( + (subField) => subField?.interface && !['subTable'].includes(subField?.interface) && !subField.treeChildren, + ) ?.map((subField) => { const interfaceConfig = getInterface(subField.interface); const schema = { diff --git a/packages/core/database/src/__tests__/tree.test.ts b/packages/core/database/src/__tests__/tree.test.ts new file mode 100644 index 000000000..d8c5b296f --- /dev/null +++ b/packages/core/database/src/__tests__/tree.test.ts @@ -0,0 +1,217 @@ +import { Database } from '../database'; +import { mockDatabase } from './'; + +describe('sort', function () { + let db: Database; + + beforeEach(async () => { + db = mockDatabase(); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should be auto completed', () => { + const collection = db.collection({ + name: 'categories', + tree: 'adjacency-list', + fields: [ + { + type: 'belongsTo', + name: 'parent', + treeParent: true, + }, + { + type: 'hasMany', + name: 'children', + treeChildren: true, + }, + ], + }); + expect(collection.treeChildrenField?.name).toBe('children'); + expect(collection.treeParentField?.name).toBe('parent'); + expect(collection.getField('parent').options.target).toBe('categories'); + expect(collection.getField('parent').options.foreignKey).toBe('parentId'); + expect(collection.getField('children').options.target).toBe('categories'); + expect(collection.getField('children').options.foreignKey).toBe('parentId'); + }); + + it('should be auto completed', () => { + const collection = db.collection({ + name: 'categories', + tree: 'adjacency-list', + fields: [ + { + type: 'belongsTo', + name: 'parent', + foreignKey: 'cid', + treeParent: true, + }, + { + type: 'hasMany', + name: 'children', + foreignKey: 'cid', + treeChildren: true, + }, + ], + }); + expect(collection.treeChildrenField?.name).toBe('children'); + expect(collection.treeParentField?.name).toBe('parent'); + expect(collection.getField('parent').options.target).toBe('categories'); + expect(collection.getField('parent').options.foreignKey).toBe('cid'); + expect(collection.getField('children').options.target).toBe('categories'); + expect(collection.getField('children').options.foreignKey).toBe('cid'); + }); + + const values = [ + { + name: '1', + __index: '0', + children: [ + { + name: '1-1', + __index: '0.children.0', + children: [ + { + name: '1-1-1', + __index: '0.children.0.children.0', + children: [ + { + name: '1-1-1-1', + __index: '0.children.0.children.0.children.0', + }, + ], + }, + ], + }, + ], + }, + { + name: '2', + __index: '1', + children: [ + { + name: '2-1', + __index: '1.children.0', + children: [ + { + name: '2-1-1', + __index: '1.children.0.children.0', + children: [ + { + name: '2-1-1-1', + __index: '1.children.0.children.0.children.0', + }, + ], + }, + ], + }, + ], + }, + ]; + + it('should be tree', async () => { + const collection = db.collection({ + name: 'categories', + tree: 'adjacency-list', + fields: [ + { + type: 'string', + name: 'name', + }, + { + type: 'string', + name: 'description', + }, + { + type: 'belongsTo', + name: 'parent', + treeParent: true, + }, + { + type: 'hasMany', + name: 'children', + treeChildren: true, + }, + ], + }); + await db.sync(); + + await db.getRepository('categories').create({ + values, + }); + + const instances = await db.getRepository('categories').find({ + filter: { + parentId: null, + }, + tree: true, + fields: ['id', 'name'], + sort: 'id', + }); + + expect(instances.map((i) => i.toJSON())).toMatchObject(values); + + const instance = await db.getRepository('categories').findOne({ + filterByTk: 1, + tree: true, + fields: ['id', 'name'], + }); + + expect(instance.toJSON()).toMatchObject(values[0]); + }); + + it('should be tree', async () => { + const collection = db.collection({ + name: 'categories', + tree: 'adjacency-list', + fields: [ + { + type: 'string', + name: 'name', + }, + { + type: 'string', + name: 'description', + }, + { + type: 'belongsTo', + name: 'parent', + foreignKey: 'cid', + treeParent: true, + }, + { + type: 'hasMany', + name: 'children', + foreignKey: 'cid', + treeChildren: true, + }, + ], + }); + await db.sync(); + + await db.getRepository('categories').create({ + values, + }); + + const instances = await db.getRepository('categories').find({ + filter: { + cid: null, + }, + tree: true, + fields: ['id', 'name'], + sort: 'id', + }); + + expect(instances.map((i) => i.toJSON())).toMatchObject(values); + + const instance = await db.getRepository('categories').findOne({ + filterByTk: 1, + tree: true, + fields: ['id', 'name'], + }); + + expect(instance.toJSON()).toMatchObject(values[0]); + }); +}); diff --git a/packages/core/database/src/collection.ts b/packages/core/database/src/collection.ts index 92fa3687c..919653948 100644 --- a/packages/core/database/src/collection.ts +++ b/packages/core/database/src/collection.ts @@ -7,10 +7,10 @@ import { QueryInterfaceDropTableOptions, SyncOptions, Transactionable, - Utils, + Utils } from 'sequelize'; import { Database } from './database'; -import { Field, FieldOptions } from './fields'; +import { BelongsToField, Field, FieldOptions, HasManyField } from './fields'; import { Model } from './model'; import { Repository } from './repository'; import { checkIdentifier, md5, snakeCase } from './utils'; @@ -48,6 +48,7 @@ export interface CollectionOptions extends Omit */ magicAttribute?: string; + tree?: string; [key: string]: any; } @@ -82,6 +83,22 @@ export class Collection< return this.context.database; } + get treeParentField(): BelongsToField | null { + for (const [_, field] of this.fields) { + if (field.options.treeParent) { + return field; + } + } + } + + get treeChildrenField(): HasManyField | null { + for (const [_, field] of this.fields) { + if (field.options.treeChildren) { + return field; + } + } + } + constructor(options: CollectionOptions, context: CollectionContext) { super(); this.context = context; @@ -243,6 +260,7 @@ export class Collection< this.checkFieldType(name, options); const { database } = this.context; + this.emit('field.beforeAdd', name, options, { collection: this }); const field = database.buildField( { name, ...options }, diff --git a/packages/core/database/src/database.ts b/packages/core/database/src/database.ts index f710bf193..33c10d76d 100644 --- a/packages/core/database/src/database.ts +++ b/packages/core/database/src/database.ts @@ -15,7 +15,7 @@ import { Sequelize, SyncOptions, Transactionable, - Utils, + Utils } from 'sequelize'; import { SequelizeStorage, Umzug } from 'umzug'; import { Collection, CollectionOptions, RepositoryType } from './collection'; @@ -58,11 +58,12 @@ import { SyncListener, UpdateListener, UpdateWithAssociationsListener, - ValidateListener, + ValidateListener } from './types'; import { patchSequelizeQueryInterface, snakeCase } from './utils'; import DatabaseUtils from './database-utils'; +import { registerBuiltInListeners } from './listeners'; import { BaseValueParser, registerFieldValueParsers } from './value-parsers'; import buildQueryInterface from './query-interface/query-interface-builder'; import QueryInterface from './query-interface/query-interface'; @@ -363,6 +364,8 @@ export class Database extends EventEmitter implements AsyncEmitter { options.schema = this.options.schema; } }); + + registerBuiltInListeners(this); } addMigration(item: MigrationItem) { diff --git a/packages/core/database/src/listeners/adjacency-list.ts b/packages/core/database/src/listeners/adjacency-list.ts new file mode 100644 index 000000000..1f01af372 --- /dev/null +++ b/packages/core/database/src/listeners/adjacency-list.ts @@ -0,0 +1,60 @@ +import lodash from 'lodash'; +import { Collection, CollectionOptions } from '../collection'; +import { Model } from '../model'; + +export const beforeDefineAdjacencyListCollection = (options: CollectionOptions) => { + if (!options.tree) { + return; + } + (options.fields || []).forEach((field) => { + if (field.treeParent || field.treeChildren) { + if (!field.target) { + field.target = options.name; + } + if (!field.foreignKey) { + field.foreignKey = 'parentId'; + } + } + }); +}; + +export const afterDefineAdjacencyListCollection = (collection: Collection) => { + if (!collection.options.tree) { + return; + } + collection.model.afterFind(async (instances, options: any) => { + if (!options.tree) { + return; + } + const foreignKey = collection.treeParentField?.foreignKey ?? 'parentId'; + const childrenKey = collection.treeChildrenField?.name ?? 'children'; + const arr: Model[] = Array.isArray(instances) ? instances : [instances]; + let index = 0; + for (const instance of arr) { + const opts = { + ...lodash.pick(options, ['tree', 'fields', 'appends', 'except', 'sort']), + }; + let __index = `${index++}`; + if (options.parentIndex) { + __index = `${options.parentIndex}.${__index}`; + } + instance.setDataValue('__index', __index); + const children = await collection.repository.find({ + filter: { + [foreignKey]: instance.id, + }, + transaction: options.transaction, + ...opts, + // @ts-ignore + parentIndex: `${__index}.${childrenKey}`, + context: options.context, + }); + if (children?.length > 0) { + instance.setDataValue( + childrenKey, + children.map((r) => r.toJSON()), + ); + } + } + }); +}; diff --git a/packages/core/database/src/listeners/index.ts b/packages/core/database/src/listeners/index.ts new file mode 100644 index 000000000..f928c3cb9 --- /dev/null +++ b/packages/core/database/src/listeners/index.ts @@ -0,0 +1,7 @@ +import { Database } from '../database'; +import { afterDefineAdjacencyListCollection, beforeDefineAdjacencyListCollection } from './adjacency-list'; + +export const registerBuiltInListeners = (db: Database) => { + db.on('beforeDefineCollection', beforeDefineAdjacencyListCollection); + db.on('afterDefineCollection', afterDefineAdjacencyListCollection); +}; diff --git a/packages/core/database/src/repository.ts b/packages/core/database/src/repository.ts index fc26bbffb..d58d0f37d 100644 --- a/packages/core/database/src/repository.ts +++ b/packages/core/database/src/repository.ts @@ -11,7 +11,7 @@ import { Op, Transactionable, UpdateOptions as SequelizeUpdateOptions, - WhereOperators, + WhereOperators } from 'sequelize'; import { Collection } from './collection'; import { Database } from './database'; @@ -106,6 +106,7 @@ export interface CommonFindOptions extends Transactionable { except?: Except; sort?: Sort; context?: any; + tree?: boolean; } export type FindOneOptions = Omit; diff --git a/packages/plugins/acl/src/__tests__/list-action.test.ts b/packages/plugins/acl/src/__tests__/list-action.test.ts index 90330e056..a26a10049 100644 --- a/packages/plugins/acl/src/__tests__/list-action.test.ts +++ b/packages/plugins/acl/src/__tests__/list-action.test.ts @@ -283,4 +283,128 @@ describe('list association action with acl', () => { expect(data['meta']['allowedActions'].view).toContain(1); expect(data['meta']['allowedActions'].view).toContain(2); }); + + it('tree list action allowActions', async () => { + await db.getRepository('roles').create({ + values: { + name: 'newRole', + }, + }); + + const user = await db.getRepository('users').create({ + values: { + roles: ['newRole'], + }, + }); + + const userPlugin = app.getPlugin('users'); + const agent = app + .agent() + .set('X-With-ACL-Meta', true) + .auth( + userPlugin.jwtService.sign({ + userId: user.get('id'), + }), + { type: 'bearer' }, + ); + + app.acl.allow('table_a', ['*']); + app.acl.allow('collections', ['*']); + + await agent.resource('collections').create({ + values: { + autoGenId: true, + createdBy: false, + updatedBy: false, + createdAt: false, + updatedAt: false, + sortable: false, + name: 'table_a', + template: 'tree', + tree: 'adjacencyList', + fields: [ + { + interface: 'integer', + name: 'parentId', + type: 'bigInt', + isForeignKey: true, + uiSchema: { + type: 'number', + title: '{{t("Parent ID")}}', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + }, + target: 'table_a', + }, + { + interface: 'm2o', + type: 'belongsTo', + name: 'parent', + foreignKey: 'parentId', + uiSchema: { + title: '{{t("Parent")}}', + 'x-component': 'RecordPicker', + 'x-component-props': { multiple: false, fieldNames: { label: 'id', value: 'id' } }, + }, + target: 'table_a', + }, + { + interface: 'o2m', + type: 'hasMany', + name: 'children', + foreignKey: 'parentId', + uiSchema: { + title: '{{t("Children")}}', + 'x-component': 'RecordPicker', + 'x-component-props': { multiple: true, fieldNames: { label: 'id', value: 'id' } }, + }, + target: 'table_a', + }, + { + name: 'id', + type: 'bigInt', + autoIncrement: true, + primaryKey: true, + allowNull: false, + uiSchema: { type: 'number', title: '{{t("ID")}}', 'x-component': 'InputNumber', 'x-read-pretty': true }, + interface: 'id', + }, + ], + title: 'table_a', + }, + }); + + await agent.resource('table_a').create({ + values: {}, + }); + + await agent.resource('table_a').create({ + values: { + parent: { + id: 1, + }, + }, + }); + + await agent.resource('table_a').create({ + values: {}, + }); + + await agent.resource('table_a').create({ + values: { + parent: { + id: 3, + }, + }, + }); + + const res = await agent.resource('table_a').list({ + filter: JSON.stringify({ + parentId: null, + }), + tree: true, + }); + + expect(res.body.meta.allowedActions.view.sort()).toMatchObject([1, 2, 3, 4]); + }); }); diff --git a/packages/plugins/acl/src/server.ts b/packages/plugins/acl/src/server.ts index 7cbdfb605..22459fe7d 100644 --- a/packages/plugins/acl/src/server.ts +++ b/packages/plugins/acl/src/server.ts @@ -673,7 +673,15 @@ export class PluginACL extends Plugin { ]); } - const ids = listData.map((item) => item[primaryKeyField]); + const ids = (() => { + if (collection.options.tree) { + if (listData.length == 0) return []; + const getAllNodeIds = (data) => [data[primaryKeyField], ...(data.children || []).flatMap(getAllNodeIds)]; + return listData.map((tree) => getAllNodeIds(tree.toJSON())).flat(); + } + + return listData.map((item) => item[primaryKeyField]); + })(); const conditions = []; @@ -823,4 +831,4 @@ export class PluginACL extends Plugin { } } -export default PluginACL; +export default PluginACL; \ No newline at end of file diff --git a/packages/plugins/collection-manager/src/hooks/afterCreateForForeignKeyField.ts b/packages/plugins/collection-manager/src/hooks/afterCreateForForeignKeyField.ts index ea19961f2..7391f5ed7 100644 --- a/packages/plugins/collection-manager/src/hooks/afterCreateForForeignKeyField.ts +++ b/packages/plugins/collection-manager/src/hooks/afterCreateForForeignKeyField.ts @@ -99,7 +99,7 @@ export function afterCreateForForeignKeyField(db: Database) { }); } - return async (model, { transaction, context }) => { + const hook = async (model, { transaction, context }) => { // skip if no app context if (!context) { return; @@ -174,4 +174,12 @@ export function afterCreateForForeignKeyField(db: Database) { }); } }; + + return async (model, options) => { + try { + await hook(model, options); + } catch (error) { + + } + }; }