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 <chenlinxh@gmail.com>
This commit is contained in:
parent
7fda978fe8
commit
6c837ee08c
@ -92,7 +92,7 @@ export const useResourceAction = (props, opts = {}) => {
|
|||||||
*/
|
*/
|
||||||
const { resource, action, fieldName: tableFieldName } = props;
|
const { resource, action, fieldName: tableFieldName } = props;
|
||||||
const { fields } = useCollection();
|
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 params = useActionParams(props);
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
|
@ -75,7 +75,7 @@ const useAssociationNames = (collection) => {
|
|||||||
const collectionFields = getCollectionFields(collection);
|
const collectionFields = getCollectionFields(collection);
|
||||||
const associationFields = new Set();
|
const associationFields = new Set();
|
||||||
for (const collectionField of collectionFields) {
|
for (const collectionField of collectionFields) {
|
||||||
if (collectionField.target && collectionField.interface !== 'snapshot') {
|
if (collectionField.target) {
|
||||||
associationFields.add(collectionField.name);
|
associationFields.add(collectionField.name);
|
||||||
const fields = getCollectionFields(collectionField.target);
|
const fields = getCollectionFields(collectionField.target);
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
|
@ -6,7 +6,6 @@ import { useCollectionManager } from '../collection-manager';
|
|||||||
import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider';
|
import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider';
|
||||||
import { useFixedSchema } from '../schema-component';
|
import { useFixedSchema } from '../schema-component';
|
||||||
|
|
||||||
|
|
||||||
export const TableBlockContext = createContext<any>({});
|
export const TableBlockContext = createContext<any>({});
|
||||||
|
|
||||||
const InternalTableBlockProvider = (props) => {
|
const InternalTableBlockProvider = (props) => {
|
||||||
@ -39,7 +38,7 @@ export const useAssociationNames = (collection) => {
|
|||||||
const collectionFields = getCollectionFields(collection);
|
const collectionFields = getCollectionFields(collection);
|
||||||
const associationFields = new Set();
|
const associationFields = new Set();
|
||||||
for (const collectionField of collectionFields) {
|
for (const collectionField of collectionFields) {
|
||||||
if (collectionField.target && collectionField.interface !== 'snapshot') {
|
if (collectionField.target) {
|
||||||
associationFields.add(collectionField.name);
|
associationFields.add(collectionField.name);
|
||||||
const fields = getCollectionFields(collectionField.target);
|
const fields = getCollectionFields(collectionField.target);
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
|
@ -37,7 +37,7 @@ const InternalTableSelectorProvider = (props) => {
|
|||||||
const useAssociationNames2 = (collection) => {
|
const useAssociationNames2 = (collection) => {
|
||||||
const { getCollectionFields } = useCollectionManager();
|
const { getCollectionFields } = useCollectionManager();
|
||||||
const names = getCollectionFields(collection)
|
const names = getCollectionFields(collection)
|
||||||
?.filter((field) => field.target && field.interface !== 'snapshot')
|
?.filter((field) => field.target)
|
||||||
.map((field) => field.name);
|
.map((field) => field.name);
|
||||||
return names;
|
return names;
|
||||||
};
|
};
|
||||||
@ -55,7 +55,7 @@ const useAssociationNames = (collection) => {
|
|||||||
const collectionFields = getCollectionFields(collection);
|
const collectionFields = getCollectionFields(collection);
|
||||||
const associationFields = new Set();
|
const associationFields = new Set();
|
||||||
for (const collectionField of collectionFields) {
|
for (const collectionField of collectionFields) {
|
||||||
if (collectionField.target && collectionField.interface !== 'snapshot') {
|
if (collectionField.target) {
|
||||||
associationFields.add(collectionField.name);
|
associationFields.add(collectionField.name);
|
||||||
const fields = getCollectionFields(collectionField.target);
|
const fields = getCollectionFields(collectionField.target);
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
|
@ -3,7 +3,9 @@ import { CollectionContext } from './context';
|
|||||||
import { useCollectionManager } from './hooks';
|
import { useCollectionManager } from './hooks';
|
||||||
import { CollectionOptions } from './types';
|
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 { allowNull, name, collection, children } = props;
|
||||||
const { getCollection } = useCollectionManager();
|
const { getCollection } = useCollectionManager();
|
||||||
const value = getCollection(collection || name);
|
const value = getCollection(collection || name);
|
||||||
|
@ -67,7 +67,9 @@ FormItem.Designer = (props) => {
|
|||||||
const interfaceConfig = getInterface(collectionField?.interface);
|
const interfaceConfig = getInterface(collectionField?.interface);
|
||||||
const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema);
|
const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema);
|
||||||
const originalTitle = collectionField?.uiSchema?.title;
|
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 fieldComponentOptions = useFieldComponentOptions();
|
||||||
const isSubFormAssocitionField = field.address.segments.includes('__form_grid');
|
const isSubFormAssocitionField = field.address.segments.includes('__form_grid');
|
||||||
const initialValue = {
|
const initialValue = {
|
||||||
@ -466,7 +468,7 @@ FormItem.Designer = (props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{collectionField?.target && fieldSchema['x-component'] === 'CollectionField' && (
|
{options.length > 0 && fieldSchema['x-component'] === 'CollectionField' && (
|
||||||
<SchemaSettings.SelectItem
|
<SchemaSettings.SelectItem
|
||||||
key="title-field"
|
key="title-field"
|
||||||
title={t('Title field')}
|
title={t('Title field')}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Field } from '@formily/core';
|
import { observer, RecursionField, useFieldSchema } from '@formily/react';
|
||||||
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
|
||||||
import { toArr } from '@formily/shared';
|
import { toArr } from '@formily/shared';
|
||||||
import React, { Fragment, useRef, useState } from 'react';
|
import React, { Fragment, useRef, useState } from 'react';
|
||||||
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
|
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
|
||||||
@ -97,7 +96,7 @@ export const ReadPrettyRecordPicker: React.FC = observer((props: any) => {
|
|||||||
return collectionField ? (
|
return collectionField ? (
|
||||||
<div>
|
<div>
|
||||||
<BlockAssociationContext.Provider value={`${collectionField.collectionName}.${collectionField.name}`}>
|
<BlockAssociationContext.Provider value={`${collectionField.collectionName}.${collectionField.name}`}>
|
||||||
<CollectionProvider name={collectionField.target}>
|
<CollectionProvider name={collectionField.target ?? collectionField.targetCollection}>
|
||||||
<EllipsisWithTooltip ellipsis={ellipsis} ref={ellipsisWithTooltipRef}>
|
<EllipsisWithTooltip ellipsis={ellipsis} ref={ellipsisWithTooltipRef}>
|
||||||
{renderRecords()}
|
{renderRecords()}
|
||||||
</EllipsisWithTooltip>
|
</EllipsisWithTooltip>
|
||||||
|
@ -31,7 +31,7 @@ export const TableColumnDesigner = (props) => {
|
|||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
const fieldNames =
|
const fieldNames =
|
||||||
fieldSchema?.['x-component-props']?.['fieldNames'] || uiSchema?.['x-component-props']?.['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);
|
const intefaceCfg = getInterface(collectionField?.interface);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -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',
|
||||||
|
};
|
@ -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',
|
||||||
|
};
|
@ -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',
|
||||||
|
};
|
@ -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',
|
||||||
|
};
|
@ -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',
|
||||||
|
};
|
@ -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',
|
||||||
|
};
|
@ -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',
|
||||||
|
};
|
@ -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',
|
||||||
|
};
|
@ -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',
|
||||||
|
};
|
@ -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',
|
||||||
|
};
|
@ -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',
|
||||||
|
};
|
@ -1,6 +1,5 @@
|
|||||||
import { mockServer, MockServer } from '@nocobase/test';
|
import { mockServer, MockServer } from '@nocobase/test';
|
||||||
import SnapshotFieldPlugin from '../server';
|
import SnapshotFieldPlugin from '../server';
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
describe('actions', () => {
|
describe('actions', () => {
|
||||||
let app: MockServer;
|
let app: MockServer;
|
||||||
@ -22,7 +21,7 @@ describe('actions', () => {
|
|||||||
await app.destroy();
|
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 agent = app.agent();
|
||||||
|
|
||||||
const field = {
|
const field = {
|
||||||
|
155
packages/plugins/snapshot-field/src/__tests__/snapshots.test.ts
Normal file
155
packages/plugins/snapshot-field/src/__tests__/snapshots.test.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
@ -41,38 +41,4 @@ export const SnapshotBlockInitializers = (props: any) => {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// const { t } = useSnapshotTranslation();
|
|
||||||
// return (
|
|
||||||
// <SchemaInitializer.Button
|
|
||||||
// wrap={gridRowColWrap}
|
|
||||||
// title={t('Add block')}
|
|
||||||
// icon={'PlusOutlined'}
|
|
||||||
// items={[
|
|
||||||
// {
|
|
||||||
// type: 'itemGroup',
|
|
||||||
// title: '{{ t("Data blocks") }}',
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// type: 'item',
|
|
||||||
// title: '{{t("Details")}}',
|
|
||||||
// component: 'SnapshotBlockInitializersDetailItem',
|
|
||||||
// actionInitializers: 'CalendarFormActionInitializers',
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'itemGroup',
|
|
||||||
// title: '{{t("Other blocks")}}',
|
|
||||||
// children: [
|
|
||||||
// {
|
|
||||||
// type: 'item',
|
|
||||||
// title: '{{t("Markdown")}}',
|
|
||||||
// component: 'MarkdownBlockInitializer',
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// ]}
|
|
||||||
// />
|
|
||||||
// );
|
|
||||||
};
|
};
|
||||||
|
@ -1,41 +1,22 @@
|
|||||||
import React from 'react';
|
|
||||||
import { connect, mapReadPretty, useFieldSchema } from '@formily/react';
|
import { connect, mapReadPretty, useFieldSchema } from '@formily/react';
|
||||||
import {
|
import { ReadPrettyRecordPicker, useCollection } from '@nocobase/client';
|
||||||
InputRecordPicker,
|
import React from 'react';
|
||||||
ReadPrettyRecordPicker,
|
|
||||||
useActionContext,
|
|
||||||
useCollection,
|
|
||||||
useCollectionHistory,
|
|
||||||
} from '@nocobase/client';
|
|
||||||
import { SnapshotHistoryCollectionProvider } from './SnapshotHistoryCollectionProvider';
|
import { SnapshotHistoryCollectionProvider } from './SnapshotHistoryCollectionProvider';
|
||||||
|
|
||||||
const useSnapshotFieldTargetCollectionName = () => {
|
const ReadPrettyRecordPickerWrapper = (props) => {
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { getField } = useCollection();
|
const { getField } = useCollection();
|
||||||
const collectionField = getField(fieldSchema.name);
|
const collectionField = getField(fieldSchema.name);
|
||||||
const { historyCollections } = useCollectionHistory();
|
|
||||||
return historyCollections.find((i) => i.name === collectionField.target)?.name;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ReadPrettyRecordPickerWrapper = (props) => {
|
|
||||||
const collectionName = useSnapshotFieldTargetCollectionName();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SnapshotHistoryCollectionProvider collectionName={collectionName}>
|
<SnapshotHistoryCollectionProvider collectionName={collectionField?.targetCollection}>
|
||||||
<ReadPrettyRecordPicker {...props} />
|
<ReadPrettyRecordPicker {...props} />
|
||||||
</SnapshotHistoryCollectionProvider>
|
</SnapshotHistoryCollectionProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SnapshotRecordPickerInner: any = connect(
|
const SnapshotRecordPickerInner: any = connect(
|
||||||
(props) => {
|
ReadPrettyRecordPickerWrapper,
|
||||||
const actionCtx = useActionContext();
|
|
||||||
|
|
||||||
const isUpdateAction = actionCtx.fieldSchema['x-action'] === 'update';
|
|
||||||
|
|
||||||
return isUpdateAction ? <ReadPrettyRecordPickerWrapper {...props} /> : <InputRecordPicker {...props} />;
|
|
||||||
},
|
|
||||||
// mapProps(mapSuffixProps),
|
|
||||||
mapReadPretty(ReadPrettyRecordPickerWrapper),
|
mapReadPretty(ReadPrettyRecordPickerWrapper),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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<DefaultOptionType, 'value'> & { value: string };
|
||||||
|
|
||||||
|
export const AppendsTreeSelect: React.FC<AppendsTreeSelectProps> = (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<string, TreeCacheMapNode> = {};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Tag closable={closable && !disabled} onClose={onClose}>
|
||||||
|
{text}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterdValue = Array.isArray(value) ? value.filter((i) => i in valueMap) : value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TreeSelect
|
||||||
|
value={filterdValue}
|
||||||
|
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||||
|
placeholder={t('Please select')}
|
||||||
|
showCheckedStrategy="SHOW_ALL"
|
||||||
|
allowClear
|
||||||
|
multiple
|
||||||
|
treeCheckStrictly
|
||||||
|
treeCheckable
|
||||||
|
tagRender={TreeTag}
|
||||||
|
onChange={handleChange as unknown as () => void}
|
||||||
|
treeData={treeData}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -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<SelectProps, 'options'>;
|
||||||
|
|
||||||
|
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<SnapshotOwnerCollectionFieldsSelectProps> = (props) => {
|
||||||
|
const options = useSnapshotOwnerCollectionFields();
|
||||||
|
return <Select options={options} {...props} />;
|
||||||
|
};
|
@ -6,12 +6,14 @@ import {
|
|||||||
SchemaInitializerContext,
|
SchemaInitializerContext,
|
||||||
SchemaInitializerProvider,
|
SchemaInitializerProvider,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import React, { useContext, useState, useEffect } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import { useSnapshotInterface } from './interface';
|
import { useSnapshotInterface } from './interface';
|
||||||
import { SnapshotRecordPicker } from './SnapshotRecordPicker';
|
import { SnapshotRecordPicker } from './SnapshotRecordPicker';
|
||||||
import { SnapshotBlockInitializers } from './SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializers';
|
import { SnapshotBlockInitializers } from './SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializers';
|
||||||
import { SnapshotBlockInitializersDetailItem } from './SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializersDetailItem';
|
import { SnapshotBlockInitializersDetailItem } from './SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializersDetailItem';
|
||||||
import { SnapshotBlockProvider } from './SnapshotBlock/SnapshotBlockProvider';
|
import { SnapshotBlockProvider } from './SnapshotBlock/SnapshotBlockProvider';
|
||||||
|
import { AppendsTreeSelect } from './components/AppendsTreeSelect';
|
||||||
|
import { SnapshotOwnerCollectionFieldsSelect } from './components/SnapshotOwnerCollectionFieldsSelect';
|
||||||
|
|
||||||
export default React.memo((props) => {
|
export default React.memo((props) => {
|
||||||
const initializers = useContext(SchemaInitializerContext);
|
const initializers = useContext(SchemaInitializerContext);
|
||||||
@ -39,6 +41,8 @@ export default React.memo((props) => {
|
|||||||
SnapshotRecordPicker,
|
SnapshotRecordPicker,
|
||||||
SnapshotBlockProvider,
|
SnapshotBlockProvider,
|
||||||
SnapshotBlockInitializersDetailItem,
|
SnapshotBlockInitializersDetailItem,
|
||||||
|
AppendsTreeSelect,
|
||||||
|
SnapshotOwnerCollectionFieldsSelect,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
@ -1,10 +1,31 @@
|
|||||||
|
import type { Field } from '@formily/core';
|
||||||
import { ISchema } from '@formily/react';
|
import { ISchema } from '@formily/react';
|
||||||
import { IField, interfacesProperties } from '@nocobase/client';
|
import { IField, interfacesProperties, useRecord } from '@nocobase/client';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useSnapshotTranslation } from './locale';
|
import { useSnapshotTranslation } from './locale';
|
||||||
|
|
||||||
const { defaultProps, recordPickerSelector } = interfacesProperties;
|
const { defaultProps } = interfacesProperties;
|
||||||
|
|
||||||
|
const APPENDS = 'appends';
|
||||||
|
const TARGET_FIELD = 'targetField';
|
||||||
|
|
||||||
|
export const useTopRecord = () => {
|
||||||
|
let record = useRecord();
|
||||||
|
|
||||||
|
while (record && Object.keys(record.__parent).length > 0) {
|
||||||
|
record = record.__parent;
|
||||||
|
}
|
||||||
|
return record;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTargetFieldChange = (field: Field) => {
|
||||||
|
field.value; // for watch
|
||||||
|
const targetField = field.query(`.${APPENDS}`).take() as Field | undefined;
|
||||||
|
console.log(field.path);
|
||||||
|
if (!targetField) return;
|
||||||
|
!targetField.getState().disabled && targetField.setValue([]);
|
||||||
|
};
|
||||||
|
|
||||||
export const useSnapshotInterface = () => {
|
export const useSnapshotInterface = () => {
|
||||||
const { t } = useSnapshotTranslation();
|
const { t } = useSnapshotTranslation();
|
||||||
@ -56,7 +77,6 @@ export const useSnapshotInterface = () => {
|
|||||||
// title,
|
// title,
|
||||||
'x-component': 'SnapshotRecordPicker',
|
'x-component': 'SnapshotRecordPicker',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
// mode: 'tags',
|
|
||||||
multiple: true,
|
multiple: true,
|
||||||
fieldNames: {
|
fieldNames: {
|
||||||
label: 'id',
|
label: 'id',
|
||||||
@ -66,33 +86,52 @@ export const useSnapshotInterface = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
schemaInitialize(schema: ISchema, { field, readPretty, action, block }) {
|
schemaInitialize(schema: ISchema, { field, readPretty, action, block }) {
|
||||||
if (readPretty || action === 'update') {
|
|
||||||
schema['properties'] = {
|
schema['properties'] = {
|
||||||
viewer: cloneDeep(recordPickerViewer),
|
viewer: cloneDeep(recordPickerViewer),
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
schema['properties'] = {
|
|
||||||
selector: cloneDeep(recordPickerSelector),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
initialize: (values: any) => {},
|
initialize: (values: any) => {},
|
||||||
properties: {
|
properties: {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
target: {
|
[TARGET_FIELD]: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
title: t('Target collection'),
|
title: t('Association field'),
|
||||||
required: true,
|
required: true,
|
||||||
'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'],
|
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Select',
|
'x-component': 'SnapshotOwnerCollectionFieldsSelect',
|
||||||
'x-disabled': '{{ !createOnly }}',
|
'x-disabled': '{{ !createOnly || isOverride }}',
|
||||||
|
'x-reactions': [
|
||||||
|
{
|
||||||
|
target: APPENDS,
|
||||||
|
when: '{{$self.value != undefined}}',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
visible: true,
|
||||||
},
|
},
|
||||||
'uiSchema.x-component-props.multiple': {
|
},
|
||||||
type: 'boolean',
|
otherwise: {
|
||||||
'x-content': t('Allow linking to multiple records'),
|
state: {
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
[APPENDS]: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('Deep copy fields'),
|
||||||
|
description: t('When a record is created, relational data is backed up in a snapshot'),
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Checkbox',
|
'x-component': 'AppendsTreeSelect',
|
||||||
|
'x-reactions': [
|
||||||
|
{
|
||||||
|
dependencies: [TARGET_FIELD],
|
||||||
|
when: '{{$deps[0]}}',
|
||||||
|
fulfill: {
|
||||||
|
run: '{{$self.setValue($self.value)}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -6,5 +6,9 @@ export default {
|
|||||||
'Allow linking to multiple records': 'Allow linking to multiple records',
|
'Allow linking to multiple records': 'Allow linking to multiple records',
|
||||||
'Snapshot to description':
|
'Snapshot to description':
|
||||||
'It is used to create a snapshot of the table, save the current data of the pointed table, and save it only when the record to which it belongs is created, and will not be updated later.',
|
'It is used to create a snapshot of the table, save the current data of the pointed table, and save it only when the record to which it belongs is created, and will not be updated later.',
|
||||||
'Target collection': 'Target collection',
|
'Association field': 'Association field',
|
||||||
|
'Deep copy fields': 'Deep copy fields',
|
||||||
|
'Please select': 'Please select',
|
||||||
|
'When a record is created, relational data is backed up in a snapshot':
|
||||||
|
'When a record is created, relational data is backed up in a snapshot',
|
||||||
};
|
};
|
||||||
|
@ -5,5 +5,8 @@ export default {
|
|||||||
'Snapshot to description': '用于创建表的快照,保存指向的表的当前数据,仅在其所属的记录创建时保存,后续不再更新。',
|
'Snapshot to description': '用于创建表的快照,保存指向的表的当前数据,仅在其所属的记录创建时保存,后续不再更新。',
|
||||||
'View record': '查看数据',
|
'View record': '查看数据',
|
||||||
'Allow linking to multiple records': '允许关联多条记录',
|
'Allow linking to multiple records': '允许关联多条记录',
|
||||||
'Target collection': '关系表',
|
'Association field': '关系字段',
|
||||||
|
'Deep copy fields': '深复制的字段',
|
||||||
|
'Please select': '请选择',
|
||||||
|
'When a record is created, association data is backed up in a snapshot': '创建记录时,关系数据会备份到快照里',
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,64 @@
|
|||||||
import { Field, BaseColumnFieldOptions } from '@nocobase/database';
|
import { BaseColumnFieldOptions, CreateOptions, Field, Model } from '@nocobase/database';
|
||||||
import { DataTypes } from 'sequelize';
|
import { DataTypes } from 'sequelize';
|
||||||
|
|
||||||
export class SnapshotField extends Field {
|
export class SnapshotField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.JSON;
|
return DataTypes.JSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createSnapshot = async (model: Model, { transaction, values }: CreateOptions) => {
|
||||||
|
const { name, targetField } = this.options;
|
||||||
|
const collectionName = this.collection.name;
|
||||||
|
const primaryKey = this.collection.model.primaryKeyAttribute;
|
||||||
|
|
||||||
|
if (!this.collection.hasField(targetField)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repository = this.database.getRepository<any>(`${collectionName}.${targetField}`, model.get(primaryKey));
|
||||||
|
const appends = (this.options.appends || []).filter((appendName) =>
|
||||||
|
repository.targetCollection.hasField(appendName),
|
||||||
|
);
|
||||||
|
|
||||||
|
let data = await repository.find({
|
||||||
|
transaction,
|
||||||
|
appends,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
data = data.map((i) => i.toJSON());
|
||||||
|
} else if (data?.toJSON) {
|
||||||
|
data = data.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
await model.update(
|
||||||
|
{
|
||||||
|
[name]: {
|
||||||
|
collectionName,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transaction,
|
||||||
|
hooks: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
bind() {
|
||||||
|
super.bind();
|
||||||
|
this.on('afterCreateWithAssociations', this.createSnapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
unbind() {
|
||||||
|
super.unbind();
|
||||||
|
this.off('afterCreateWithAssociations', this.createSnapshot);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SnapshotFieldOptions extends BaseColumnFieldOptions {
|
export interface SnapshotFieldOptions extends BaseColumnFieldOptions {
|
||||||
type: 'snapshot';
|
type: 'snapshot';
|
||||||
|
targetField: string;
|
||||||
|
targetCollection?: string;
|
||||||
|
appends?: string[];
|
||||||
}
|
}
|
||||||
|
@ -59,8 +59,21 @@ export class SnapshotFieldPlugin extends Plugin {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.app.db.on('fields.afterCreateWithAssociations', fieldHandler);
|
this.app.db.on('fields.afterCreateWithAssociations', fieldHandler);
|
||||||
|
this.app.db.on('fields.beforeCreate', this.autoFillTargetCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
autoFillTargetCollection = async (model: Model) => {
|
||||||
|
const { collectionName, targetField } = model.get();
|
||||||
|
const collection = this.db.getCollection(collectionName);
|
||||||
|
if (!collection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const field = collection.getField(targetField);
|
||||||
|
if (field?.target) {
|
||||||
|
model.set('targetCollection', field.target);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
// 导入 collection
|
// 导入 collection
|
||||||
await this.db.import({
|
await this.db.import({
|
||||||
|
Loading…
Reference in New Issue
Block a user