From 6c837ee08cfc9eb083d6995ecc83cfa42702cc2c Mon Sep 17 00:00:00 2001 From: anuoua Date: Sun, 12 Feb 2023 14:43:48 +0800 Subject: [PATCH] feat: association snapshot (#1438) * feat: snapshota * feat: snapshota i18n & bugs * feat: snapshota association value fix * feat: snapshota remove require true * feat: snapshota params.values null fix * feat: snapshota i18n * feat: snapshota CR fix * feat: snapshota field change fix * feat: snapshota magicstring fix * feat: snapshota field del fix * feat: snapshota CR fix * feat: snapshota tag fix * feat: snapshota depth < 3 * fix: improve code * feat: snapshota test * feat: snapshota remove disabled * fix: disabled isOverride --------- Co-authored-by: chenos --- .../src/block-provider/BlockProvider.tsx | 2 +- .../block-provider/KanbanBlockProvider.tsx | 2 +- .../src/block-provider/TableBlockProvider.tsx | 3 +- .../block-provider/TableSelectorProvider.tsx | 4 +- .../collection-manager/CollectionProvider.tsx | 4 +- .../antd/form-item/FormItem.tsx | 6 +- .../record-picker/ReadPrettyRecordPicker.tsx | 5 +- .../antd/table-v2/Table.Column.Designer.tsx | 2 +- .../src/__tests__/data/field_linkto.ts | 12 ++ .../src/__tests__/data/field_m2m.ts | 15 ++ .../src/__tests__/data/field_o2m.ts | 14 ++ .../src/__tests__/data/field_o2o.ts | 14 ++ .../src/__tests__/data/snap_linkto.ts | 12 ++ .../src/__tests__/data/snap_m2m.ts | 12 ++ .../src/__tests__/data/snap_o2m.ts | 12 ++ .../src/__tests__/data/snap_o2o.ts | 13 ++ .../src/__tests__/data/table_a.ts | 37 +++++ .../src/__tests__/data/table_b.ts | 37 +++++ .../src/__tests__/data/table_m2m.ts | 37 +++++ .../src/__tests__/fieldsHistory.test.ts | 3 +- .../src/__tests__/snapshots.test.ts | 155 ++++++++++++++++++ .../SnapshotBlockInitializers.tsx | 34 ---- .../src/client/SnapshotRecordPicker.tsx | 29 +--- .../client/components/AppendsTreeSelect.tsx | 122 ++++++++++++++ .../SnapshotOwnerCollectionFieldsSelect.tsx | 22 +++ .../snapshot-field/src/client/index.tsx | 6 +- .../snapshot-field/src/client/interface.ts | 81 ++++++--- .../snapshot-field/src/client/locale/en-US.ts | 6 +- .../snapshot-field/src/client/locale/zh-CN.ts | 5 +- .../src/server/fields/snapshot-field.ts | 54 +++++- .../snapshot-field/src/server/plugin.ts | 13 ++ 31 files changed, 675 insertions(+), 98 deletions(-) create mode 100644 packages/plugins/snapshot-field/src/__tests__/data/field_linkto.ts create mode 100644 packages/plugins/snapshot-field/src/__tests__/data/field_m2m.ts create mode 100644 packages/plugins/snapshot-field/src/__tests__/data/field_o2m.ts create mode 100644 packages/plugins/snapshot-field/src/__tests__/data/field_o2o.ts create mode 100644 packages/plugins/snapshot-field/src/__tests__/data/snap_linkto.ts create mode 100644 packages/plugins/snapshot-field/src/__tests__/data/snap_m2m.ts create mode 100644 packages/plugins/snapshot-field/src/__tests__/data/snap_o2m.ts create mode 100644 packages/plugins/snapshot-field/src/__tests__/data/snap_o2o.ts create mode 100644 packages/plugins/snapshot-field/src/__tests__/data/table_a.ts create mode 100644 packages/plugins/snapshot-field/src/__tests__/data/table_b.ts create mode 100644 packages/plugins/snapshot-field/src/__tests__/data/table_m2m.ts create mode 100644 packages/plugins/snapshot-field/src/__tests__/snapshots.test.ts create mode 100644 packages/plugins/snapshot-field/src/client/components/AppendsTreeSelect.tsx create mode 100644 packages/plugins/snapshot-field/src/client/components/SnapshotOwnerCollectionFieldsSelect.tsx diff --git a/packages/core/client/src/block-provider/BlockProvider.tsx b/packages/core/client/src/block-provider/BlockProvider.tsx index badbfc81c..d64407f17 100644 --- a/packages/core/client/src/block-provider/BlockProvider.tsx +++ b/packages/core/client/src/block-provider/BlockProvider.tsx @@ -92,7 +92,7 @@ export const useResourceAction = (props, opts = {}) => { */ const { resource, action, fieldName: tableFieldName } = props; const { fields } = useCollection(); - const appends = fields?.filter((field) => field.target && field.interface !== 'snapshot').map((field) => field.name); + const appends = fields?.filter((field) => field.target).map((field) => field.name); const params = useActionParams(props); const api = useAPIClient(); const fieldSchema = useFieldSchema(); diff --git a/packages/core/client/src/block-provider/KanbanBlockProvider.tsx b/packages/core/client/src/block-provider/KanbanBlockProvider.tsx index 74d738f36..3c274aa3c 100644 --- a/packages/core/client/src/block-provider/KanbanBlockProvider.tsx +++ b/packages/core/client/src/block-provider/KanbanBlockProvider.tsx @@ -75,7 +75,7 @@ const useAssociationNames = (collection) => { const collectionFields = getCollectionFields(collection); const associationFields = new Set(); for (const collectionField of collectionFields) { - if (collectionField.target && collectionField.interface !== 'snapshot') { + if (collectionField.target) { associationFields.add(collectionField.name); const fields = getCollectionFields(collectionField.target); for (const field of fields) { diff --git a/packages/core/client/src/block-provider/TableBlockProvider.tsx b/packages/core/client/src/block-provider/TableBlockProvider.tsx index 002896834..c7cfb335c 100644 --- a/packages/core/client/src/block-provider/TableBlockProvider.tsx +++ b/packages/core/client/src/block-provider/TableBlockProvider.tsx @@ -6,7 +6,6 @@ import { useCollectionManager } from '../collection-manager'; import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider'; import { useFixedSchema } from '../schema-component'; - export const TableBlockContext = createContext({}); const InternalTableBlockProvider = (props) => { @@ -39,7 +38,7 @@ export const useAssociationNames = (collection) => { const collectionFields = getCollectionFields(collection); const associationFields = new Set(); for (const collectionField of collectionFields) { - if (collectionField.target && collectionField.interface !== 'snapshot') { + if (collectionField.target) { associationFields.add(collectionField.name); const fields = getCollectionFields(collectionField.target); for (const field of fields) { diff --git a/packages/core/client/src/block-provider/TableSelectorProvider.tsx b/packages/core/client/src/block-provider/TableSelectorProvider.tsx index 159b91bbe..1dd28e5f7 100644 --- a/packages/core/client/src/block-provider/TableSelectorProvider.tsx +++ b/packages/core/client/src/block-provider/TableSelectorProvider.tsx @@ -37,7 +37,7 @@ const InternalTableSelectorProvider = (props) => { const useAssociationNames2 = (collection) => { const { getCollectionFields } = useCollectionManager(); const names = getCollectionFields(collection) - ?.filter((field) => field.target && field.interface !== 'snapshot') + ?.filter((field) => field.target) .map((field) => field.name); return names; }; @@ -55,7 +55,7 @@ const useAssociationNames = (collection) => { const collectionFields = getCollectionFields(collection); const associationFields = new Set(); for (const collectionField of collectionFields) { - if (collectionField.target && collectionField.interface !== 'snapshot') { + if (collectionField.target) { associationFields.add(collectionField.name); const fields = getCollectionFields(collectionField.target); for (const field of fields) { diff --git a/packages/core/client/src/collection-manager/CollectionProvider.tsx b/packages/core/client/src/collection-manager/CollectionProvider.tsx index 2150ce225..63aa61ea2 100644 --- a/packages/core/client/src/collection-manager/CollectionProvider.tsx +++ b/packages/core/client/src/collection-manager/CollectionProvider.tsx @@ -3,7 +3,9 @@ import { CollectionContext } from './context'; import { useCollectionManager } from './hooks'; import { CollectionOptions } from './types'; -export const CollectionProvider: React.FC<{ allowNull?: boolean; name?: string; collection?: CollectionOptions }> = (props) => { +export const CollectionProvider: React.FC<{ allowNull?: boolean; name?: string; collection?: CollectionOptions }> = ( + props, +) => { const { allowNull, name, collection, children } = props; const { getCollection } = useCollectionManager(); const value = getCollection(collection || name); diff --git a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx index 8c412c06a..81d7a9385 100644 --- a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx +++ b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx @@ -67,7 +67,9 @@ FormItem.Designer = (props) => { const interfaceConfig = getInterface(collectionField?.interface); const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema); const originalTitle = collectionField?.uiSchema?.title; - const targetFields = collectionField?.target ? getCollectionFields(collectionField.target) : []; + const targetFields = collectionField?.target + ? getCollectionFields(collectionField.target) + : getCollectionFields(collectionField?.targetCollection) ?? []; const fieldComponentOptions = useFieldComponentOptions(); const isSubFormAssocitionField = field.address.segments.includes('__form_grid'); const initialValue = { @@ -466,7 +468,7 @@ FormItem.Designer = (props) => { }} /> )} - {collectionField?.target && fieldSchema['x-component'] === 'CollectionField' && ( + {options.length > 0 && fieldSchema['x-component'] === 'CollectionField' && ( { return collectionField ? (
- + {renderRecords()} diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx index b69c36f18..eead4655f 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx @@ -31,7 +31,7 @@ export const TableColumnDesigner = (props) => { const { dn } = useDesignable(); const fieldNames = fieldSchema?.['x-component-props']?.['fieldNames'] || uiSchema?.['x-component-props']?.['fieldNames']; - const options = useLabelFields(collectionField?.target); + const options = useLabelFields(collectionField?.target ?? collectionField?.targetCollection); const intefaceCfg = getInterface(collectionField?.interface); return ( diff --git a/packages/plugins/snapshot-field/src/__tests__/data/field_linkto.ts b/packages/plugins/snapshot-field/src/__tests__/data/field_linkto.ts new file mode 100644 index 000000000..61b3bc3a4 --- /dev/null +++ b/packages/plugins/snapshot-field/src/__tests__/data/field_linkto.ts @@ -0,0 +1,12 @@ +export const field_linkto = { + name: 'field_linkto', + type: 'belongsToMany', + uiSchema: { + 'x-component': 'RecordPicker', + 'x-component-props': { multiple: true, fieldNames: { label: 'id', value: 'id' } }, + title: 'field_linkto', + }, + interface: 'linkTo', + target: 'table_a', + collectionName: 'table_b', +}; diff --git a/packages/plugins/snapshot-field/src/__tests__/data/field_m2m.ts b/packages/plugins/snapshot-field/src/__tests__/data/field_m2m.ts new file mode 100644 index 000000000..cbbeb085a --- /dev/null +++ b/packages/plugins/snapshot-field/src/__tests__/data/field_m2m.ts @@ -0,0 +1,15 @@ +export const field_m2m = { + foreignKey: 'fk_table_b', + otherKey: 'fk_table_a', + name: 'field_m2m', + type: 'belongsToMany', + uiSchema: { + 'x-component': 'RecordPicker', + 'x-component-props': { multiple: true, fieldNames: { label: 'id', value: 'id' } }, + title: 'field_m2m', + }, + interface: 'm2m', + through: 'table_m2m', + target: 'table_a', + collectionName: 'table_b', +}; diff --git a/packages/plugins/snapshot-field/src/__tests__/data/field_o2m.ts b/packages/plugins/snapshot-field/src/__tests__/data/field_o2m.ts new file mode 100644 index 000000000..b53c95076 --- /dev/null +++ b/packages/plugins/snapshot-field/src/__tests__/data/field_o2m.ts @@ -0,0 +1,14 @@ +export const field_o2m = { + foreignKey: 'fk_table_b', + onDelete: 'SET NULL', + name: 'field_o2m', + type: 'hasMany', + uiSchema: { + 'x-component': 'RecordPicker', + 'x-component-props': { multiple: true, fieldNames: { label: 'id', value: 'id' } }, + title: 'field_o2m', + }, + interface: 'o2m', + target: 'table_a', + collectionName: 'table_b', +}; diff --git a/packages/plugins/snapshot-field/src/__tests__/data/field_o2o.ts b/packages/plugins/snapshot-field/src/__tests__/data/field_o2o.ts new file mode 100644 index 000000000..c2bb3785f --- /dev/null +++ b/packages/plugins/snapshot-field/src/__tests__/data/field_o2o.ts @@ -0,0 +1,14 @@ +export const field_o2o = { + foreignKey: 'fk_table_b', + onDelete: 'SET NULL', + name: 'field_o2o', + type: 'hasOne', + uiSchema: { + 'x-component': 'RecordPicker', + 'x-component-props': { multiple: false, fieldNames: { label: 'id', value: 'id' } }, + title: 'field_o2o', + }, + interface: 'oho', + target: 'table_a', + collectionName: 'table_b', +}; diff --git a/packages/plugins/snapshot-field/src/__tests__/data/snap_linkto.ts b/packages/plugins/snapshot-field/src/__tests__/data/snap_linkto.ts new file mode 100644 index 000000000..e45a90c51 --- /dev/null +++ b/packages/plugins/snapshot-field/src/__tests__/data/snap_linkto.ts @@ -0,0 +1,12 @@ +export const snap_linkto = { + name: 'snap_linkto', + type: 'snapshot', + uiSchema: { + 'x-component': 'SnapshotRecordPicker', + 'x-component-props': { multiple: true, fieldNames: { label: 'id', value: 'id' } }, + title: 'snap_linkto', + }, + interface: 'snapshot', + targetField: 'field_linkto', + collectionName: 'table_b', +}; diff --git a/packages/plugins/snapshot-field/src/__tests__/data/snap_m2m.ts b/packages/plugins/snapshot-field/src/__tests__/data/snap_m2m.ts new file mode 100644 index 000000000..f9e60cb93 --- /dev/null +++ b/packages/plugins/snapshot-field/src/__tests__/data/snap_m2m.ts @@ -0,0 +1,12 @@ +export const snap_m2m = { + name: 'snap_m2m', + type: 'snapshot', + uiSchema: { + 'x-component': 'SnapshotRecordPicker', + 'x-component-props': { multiple: true, fieldNames: { label: 'id', value: 'id' } }, + title: 'snap_m2m', + }, + interface: 'snapshot', + targetField: 'field_m2m', + collectionName: 'table_b', +}; diff --git a/packages/plugins/snapshot-field/src/__tests__/data/snap_o2m.ts b/packages/plugins/snapshot-field/src/__tests__/data/snap_o2m.ts new file mode 100644 index 000000000..7d1a81167 --- /dev/null +++ b/packages/plugins/snapshot-field/src/__tests__/data/snap_o2m.ts @@ -0,0 +1,12 @@ +export const snap_o2m = { + name: 'snap_o2m', + type: 'snapshot', + uiSchema: { + 'x-component': 'SnapshotRecordPicker', + 'x-component-props': { multiple: true, fieldNames: { label: 'id', value: 'id' } }, + title: 'snap_o2m', + }, + interface: 'snapshot', + targetField: 'field_o2m', + collectionName: 'table_b', +}; diff --git a/packages/plugins/snapshot-field/src/__tests__/data/snap_o2o.ts b/packages/plugins/snapshot-field/src/__tests__/data/snap_o2o.ts new file mode 100644 index 000000000..f85c14eea --- /dev/null +++ b/packages/plugins/snapshot-field/src/__tests__/data/snap_o2o.ts @@ -0,0 +1,13 @@ +export const snap_o2o = { + name: 'snap_o2o', + type: 'snapshot', + uiSchema: { + 'x-component': 'SnapshotRecordPicker', + 'x-component-props': { multiple: true, fieldNames: { label: 'id', value: 'id' } }, + title: 'snap_o2o', + }, + interface: 'snapshot', + targetField: 'field_o2o', + appends: ['createdBy'], + collectionName: 'table_b', +}; diff --git a/packages/plugins/snapshot-field/src/__tests__/data/table_a.ts b/packages/plugins/snapshot-field/src/__tests__/data/table_a.ts new file mode 100644 index 000000000..2baedb247 --- /dev/null +++ b/packages/plugins/snapshot-field/src/__tests__/data/table_a.ts @@ -0,0 +1,37 @@ +export const table_a = { + logging: true, + autoGenId: true, + createdBy: true, + updatedBy: false, + createdAt: false, + updatedAt: false, + sortable: true, + name: 'table_a', + template: 'general', + fields: [ + { + name: 'id', + type: 'bigInt', + autoIncrement: true, + primaryKey: true, + allowNull: false, + uiSchema: { type: 'number', title: '{{t("ID")}}', 'x-component': 'InputNumber', 'x-read-pretty': true }, + interface: 'id', + }, + { + name: 'createdBy', + interface: 'createdBy', + type: 'belongsTo', + target: 'users', + foreignKey: 'createdById', + uiSchema: { + type: 'object', + title: '{{t("Created by")}}', + 'x-component': 'RecordPicker', + 'x-component-props': { fieldNames: { value: 'id', label: 'nickname' } }, + 'x-read-pretty': true, + }, + }, + ], + title: 'table_a', +}; diff --git a/packages/plugins/snapshot-field/src/__tests__/data/table_b.ts b/packages/plugins/snapshot-field/src/__tests__/data/table_b.ts new file mode 100644 index 000000000..ac505779f --- /dev/null +++ b/packages/plugins/snapshot-field/src/__tests__/data/table_b.ts @@ -0,0 +1,37 @@ +export const table_b = { + logging: true, + autoGenId: true, + createdBy: true, + updatedBy: false, + createdAt: false, + updatedAt: false, + sortable: true, + name: 'table_b', + template: 'general', + fields: [ + { + name: 'id', + type: 'bigInt', + autoIncrement: true, + primaryKey: true, + allowNull: false, + uiSchema: { type: 'number', title: '{{t("ID")}}', 'x-component': 'InputNumber', 'x-read-pretty': true }, + interface: 'id', + }, + { + name: 'createdBy', + interface: 'createdBy', + type: 'belongsTo', + target: 'users', + foreignKey: 'createdById', + uiSchema: { + type: 'object', + title: '{{t("Created by")}}', + 'x-component': 'RecordPicker', + 'x-component-props': { fieldNames: { value: 'id', label: 'nickname' } }, + 'x-read-pretty': true, + }, + }, + ], + title: 'table_b', +}; diff --git a/packages/plugins/snapshot-field/src/__tests__/data/table_m2m.ts b/packages/plugins/snapshot-field/src/__tests__/data/table_m2m.ts new file mode 100644 index 000000000..d1efa13c1 --- /dev/null +++ b/packages/plugins/snapshot-field/src/__tests__/data/table_m2m.ts @@ -0,0 +1,37 @@ +export const table_m2m = { + logging: true, + autoGenId: true, + createdBy: true, + updatedBy: false, + createdAt: false, + updatedAt: false, + sortable: true, + name: 'table_m2m', + template: 'general', + fields: [ + { + name: 'id', + type: 'bigInt', + autoIncrement: true, + primaryKey: true, + allowNull: false, + uiSchema: { type: 'number', title: '{{t("ID")}}', 'x-component': 'InputNumber', 'x-read-pretty': true }, + interface: 'id', + }, + { + name: 'createdBy', + interface: 'createdBy', + type: 'belongsTo', + target: 'users', + foreignKey: 'createdById', + uiSchema: { + type: 'object', + title: '{{t("Created by")}}', + 'x-component': 'RecordPicker', + 'x-component-props': { fieldNames: { value: 'id', label: 'nickname' } }, + 'x-read-pretty': true, + }, + }, + ], + title: 'table_m2m', +}; diff --git a/packages/plugins/snapshot-field/src/__tests__/fieldsHistory.test.ts b/packages/plugins/snapshot-field/src/__tests__/fieldsHistory.test.ts index 0d7311d23..0317b5758 100644 --- a/packages/plugins/snapshot-field/src/__tests__/fieldsHistory.test.ts +++ b/packages/plugins/snapshot-field/src/__tests__/fieldsHistory.test.ts @@ -1,6 +1,5 @@ import { mockServer, MockServer } from '@nocobase/test'; import SnapshotFieldPlugin from '../server'; -import path from 'path'; describe('actions', () => { let app: MockServer; @@ -22,7 +21,7 @@ describe('actions', () => { await app.destroy(); }); - it.only('fieldsHistory collectionName and name conflict between tables', async () => { + it('fieldsHistory collectionName and name conflict between tables', async () => { const agent = app.agent(); const field = { diff --git a/packages/plugins/snapshot-field/src/__tests__/snapshots.test.ts b/packages/plugins/snapshot-field/src/__tests__/snapshots.test.ts new file mode 100644 index 000000000..3d04a0daa --- /dev/null +++ b/packages/plugins/snapshot-field/src/__tests__/snapshots.test.ts @@ -0,0 +1,155 @@ +import { mockServer, MockServer } from '@nocobase/test'; +import SnapshotFieldPlugin from '../server'; +import { field_linkto } from './data/field_linkto'; +import { field_m2m } from './data/field_m2m'; +import { field_o2m } from './data/field_o2m'; +import { field_o2o } from './data/field_o2o'; +import { snap_linkto } from './data/snap_linkto'; +import { snap_m2m } from './data/snap_m2m'; +import { snap_o2m } from './data/snap_o2m'; +import { snap_o2o } from './data/snap_o2o'; +import { table_a } from './data/table_a'; +import { table_b } from './data/table_b'; +import { table_m2m } from './data/table_m2m'; + +describe('actions', () => { + let app: MockServer; + + beforeEach(async () => { + app = mockServer({ + registerActions: true, + acl: false, + plugins: ['error-handler', 'users', 'ui-schema-storage', 'collection-manager'], + }); + + app.plugin(SnapshotFieldPlugin, { name: 'snapshot-field' }); + + await app.loadAndInstall({ clean: true }); + }); + + afterEach(async () => { + await app.cleanDb(); + await app.destroy(); + }); + + it.only('associations save', async () => { + const agent = app.agent(); + + await agent.resource('collections').create({ + values: table_a, + }); + + await agent.resource('collections').create({ + values: table_b, + }); + + await agent.resource('collections').create({ + values: table_m2m, + }); + + await agent.resource('fields').create({ + values: field_o2m, + }); + + await agent.resource('fields').create({ + values: field_m2m, + }); + + await agent.resource('fields').create({ + values: field_o2o, + }); + + await agent.resource('fields').create({ + values: field_linkto, + }); + + await agent.resource('fields').create({ + values: snap_o2m, + }); + + await agent.resource('fields').create({ + values: snap_m2m, + }); + + await agent.resource('fields').create({ + values: snap_o2o, + }); + + await agent.resource('fields').create({ + values: snap_linkto, + }); + + await agent.resource('table_a').create({ + values: {}, + }); + + await agent.resource('table_b').create({ + values: { + field_o2m: [{ createdById: 1, id: 1, fk_table_b: null }], + field_m2m: [{ createdById: 1, id: 1, fk_table_b: null }], + field_o2o: { createdById: 1, id: 1, fk_table_b: null }, + field_linkto: [{ createdById: 1, id: 1, fk_table_b: null }], + }, + }); + + const res = await agent.resource('table_b').list(); + const { snap_o2o: o2o, snap_m2m: m2m, snap_o2m: o2m, snap_linkto: linkto } = res.body.data[0]; + + const snapshotItem = { createdById: null, id: 1, fk_table_b: 1 }; + + expect(o2o).toMatchObject({ + collectionName: 'table_b', + data: snapshotItem, + }); + expect(m2m).toMatchObject({ + collectionName: 'table_b', + data: [snapshotItem], + }); + expect(o2m).toMatchObject({ + collectionName: 'table_b', + data: [snapshotItem], + }); + expect(linkto).toMatchObject({ + collectionName: 'table_b', + data: [snapshotItem], + }); + + await agent.resource('table_a').create({ + values: {}, + }); + + await agent.resource('collections.fields', 'table_b').destroy({ filter: { name: 'field_o2m' } }); + + const { statusCode: code2 } = await agent.resource('table_b').create({ + values: { + field_m2m: [{ createdById: 1, id: 2, fk_table_b: null }], + field_o2o: { createdById: 1, id: 2, fk_table_b: null }, + field_linkto: [{ createdById: 1, id: 2, fk_table_b: null }], + }, + }); + + expect(code2).toBe(200); + + const { statusCode: code3 } = await agent + .resource('collections.fields', 'table_a') + .destroy({ filter: { name: 'createdBy' } }); + + expect(code3).toBe(200); + + const { statusCode: code4 } = await agent.resource('table_a').create({ + values: {}, + }); + + expect(code4).toBe(200); + + const { statusCode: code5 } = await agent.resource('table_b').create({ + values: { + field_m2m: [{ createdById: 1, id: 3, fk_table_b: null }], + field_o2o: { createdById: 1, id: 3, fk_table_b: null }, + field_linkto: [{ createdById: 1, id: 3, fk_table_b: null }], + }, + }); + + expect(code5).toBe(200); + }); +}); diff --git a/packages/plugins/snapshot-field/src/client/SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializers.tsx b/packages/plugins/snapshot-field/src/client/SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializers.tsx index ea9cbbfc9..ca69b3576 100644 --- a/packages/plugins/snapshot-field/src/client/SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializers.tsx +++ b/packages/plugins/snapshot-field/src/client/SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializers.tsx @@ -41,38 +41,4 @@ export const SnapshotBlockInitializers = (props: any) => { ]} /> ); - - // const { t } = useSnapshotTranslation(); - // return ( - // - // ); }; diff --git a/packages/plugins/snapshot-field/src/client/SnapshotRecordPicker.tsx b/packages/plugins/snapshot-field/src/client/SnapshotRecordPicker.tsx index f9055cf27..b4627d030 100644 --- a/packages/plugins/snapshot-field/src/client/SnapshotRecordPicker.tsx +++ b/packages/plugins/snapshot-field/src/client/SnapshotRecordPicker.tsx @@ -1,41 +1,22 @@ -import React from 'react'; import { connect, mapReadPretty, useFieldSchema } from '@formily/react'; -import { - InputRecordPicker, - ReadPrettyRecordPicker, - useActionContext, - useCollection, - useCollectionHistory, -} from '@nocobase/client'; +import { ReadPrettyRecordPicker, useCollection } from '@nocobase/client'; +import React from 'react'; import { SnapshotHistoryCollectionProvider } from './SnapshotHistoryCollectionProvider'; -const useSnapshotFieldTargetCollectionName = () => { +const ReadPrettyRecordPickerWrapper = (props) => { const fieldSchema = useFieldSchema(); const { getField } = useCollection(); const collectionField = getField(fieldSchema.name); - const { historyCollections } = useCollectionHistory(); - return historyCollections.find((i) => i.name === collectionField.target)?.name; -}; - -const ReadPrettyRecordPickerWrapper = (props) => { - const collectionName = useSnapshotFieldTargetCollectionName(); return ( - + ); }; const SnapshotRecordPickerInner: any = connect( - (props) => { - const actionCtx = useActionContext(); - - const isUpdateAction = actionCtx.fieldSchema['x-action'] === 'update'; - - return isUpdateAction ? : ; - }, - // mapProps(mapSuffixProps), + ReadPrettyRecordPickerWrapper, mapReadPretty(ReadPrettyRecordPickerWrapper), ); diff --git a/packages/plugins/snapshot-field/src/client/components/AppendsTreeSelect.tsx b/packages/plugins/snapshot-field/src/client/components/AppendsTreeSelect.tsx new file mode 100644 index 000000000..ef81dbc24 --- /dev/null +++ b/packages/plugins/snapshot-field/src/client/components/AppendsTreeSelect.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import { TreeSelect, Tag } from 'antd'; +import type { DefaultOptionType } from 'rc-tree-select/lib/TreeSelect'; +import { useForm } from '@formily/react'; +import { CollectionFieldOptions, useCollectionManager, useCompile, useRecord } from '@nocobase/client'; +import { useSnapshotTranslation } from '../locale'; +import { useTopRecord } from '../interface'; + +export type TreeCacheMapNode = { + parent?: TreeCacheMapNode; + title: string; + path: string; + children?: TreeCacheMapNode[]; +}; + +export type AppendsTreeSelectProps = { + value: string[]; + onChange: (value: string[]) => void; +}; + +type TreeOptionType = Omit & { value: string }; + +export const AppendsTreeSelect: React.FC = (props) => { + const { value = [], onChange, ...restProps } = props; + const record = useTopRecord(); + const { getCollectionFields, getCollectionField } = useCollectionManager(); + const compile = useCompile(); + const formValues = useForm().values; + const { t } = useSnapshotTranslation(); + + const fieldsToOptions = ( + fields: CollectionFieldOptions[] = [], + fieldPath: CollectionFieldOptions[] = [], + ): TreeOptionType[] => { + const filter = (i: CollectionFieldOptions) => + !!i.target && !!i.interface && fieldPath.length < 3 && !fieldPath.find((p) => p.target === i.target); + return fields.filter(filter).map((i) => ({ + title: compile(i.uiSchema?.title) ?? i.name, + value: fieldPath + .map((p) => p.name) + .concat(i.name) + .join('.'), + children: fieldsToOptions(getCollectionFields(i.target), [...fieldPath, i]), + })); + }; + + const treeData = fieldsToOptions( + getCollectionFields(getCollectionField(`${record.name}.${formValues.targetField}`)?.target), + ); + + const valueMap: Record = {}; + + function loops(list: TreeOptionType[], parent?: TreeCacheMapNode) { + return (list || []).map(({ children, value, title }) => { + const node: TreeCacheMapNode = (valueMap[value] = { + parent, + path: value, + title, + }); + node.children = loops(children, node); + return node; + }); + } + + loops(treeData); + + const handleChange = (newNodes: DefaultOptionType[]) => { + const newValue = newNodes.map((i) => i.value) as string[]; + const valueSet = new Set(newValue); + const delValue = value.find((i) => !newValue.includes(i)); + + if (delValue) { + const delNode = valueMap[delValue]; + const delNodeValue = (node: TreeCacheMapNode) => { + valueSet.delete(node.path); + node.children?.forEach((child) => delNodeValue(child)); + }; + delNodeValue(delNode); + } else { + newValue.forEach((v) => { + let current = valueMap[v]; + while ((current = current.parent)) { + valueSet.add(current.path); + } + }); + } + onChange(Array.from(valueSet)); + }; + + const TreeTag = (props) => { + const { value, onClose, disabled, closable } = props; + let node = valueMap[value]; + let text = node?.title; + while ((node = node?.parent)) { + text = `${node.title} / ${text}`; + } + return ( + + {text} + + ); + }; + + const filterdValue = Array.isArray(value) ? value.filter((i) => i in valueMap) : value; + + return ( + void} + treeData={treeData} + {...restProps} + /> + ); +}; diff --git a/packages/plugins/snapshot-field/src/client/components/SnapshotOwnerCollectionFieldsSelect.tsx b/packages/plugins/snapshot-field/src/client/components/SnapshotOwnerCollectionFieldsSelect.tsx new file mode 100644 index 000000000..f77cbe0a7 --- /dev/null +++ b/packages/plugins/snapshot-field/src/client/components/SnapshotOwnerCollectionFieldsSelect.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Select, SelectProps } from 'antd'; +import { useTopRecord } from '../interface'; +import { useCollectionManager, useCompile } from '@nocobase/client'; + +export type SnapshotOwnerCollectionFieldsSelectProps = Omit; + +export const useSnapshotOwnerCollectionFields = () => { + const record = useTopRecord(); + const { getCollection } = useCollectionManager(); + const collection = getCollection(record.name); + const compile = useCompile(); + + return collection.fields + .filter((i) => !!i.target && !!i.interface) + .map((i) => ({ ...i, label: compile(i.uiSchema?.title), value: i.name })); +}; + +export const SnapshotOwnerCollectionFieldsSelect: React.FC = (props) => { + const options = useSnapshotOwnerCollectionFields(); + return