feat(collection-manager): inverse fields can be configured (#883)

* feat: inverse field

* feat: improve code

* feat: translations

* fix: required

(cherry picked from commit 306dca8e5ee1b74562d835a816b72f933465bfb6)

# Conflicts:
#	packages/core/client/src/locale/zh_CN.ts
This commit is contained in:
chenos 2022-10-11 08:44:16 +08:00
parent 933c3f4463
commit f6add23da5
9 changed files with 175 additions and 88 deletions

View File

@ -8,10 +8,11 @@ import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRequest } from '../../api-client'; import { useRequest } from '../../api-client';
import { useRecord } from '../../record-provider'; import { useRecord } from '../../record-provider';
import { ActionContext, SchemaComponent, useCompile } from '../../schema-component'; import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useCreateAction } from '../action-hooks'; import { useCreateAction } from '../action-hooks';
import { useCollectionManager } from '../hooks'; import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types'; import { IField } from '../interfaces/types';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import * as components from './components'; import * as components from './components';
import { options } from './interfaces'; import { options } from './interfaces';
@ -27,12 +28,15 @@ const getSchema = (schema: IField, record: any, compile): ISchema => {
properties['defaultValue']['title'] = compile('{{ t("Default value") }}'); properties['defaultValue']['title'] = compile('{{ t("Default value") }}');
properties['defaultValue']['x-decorator'] = 'FormItem'; properties['defaultValue']['x-decorator'] = 'FormItem';
} }
const initialValue = { const initialValue: any = {
name: `f_${uid()}`, name: `f_${uid()}`,
...cloneDeep(schema.default), ...cloneDeep(schema.default),
interface: schema.name, interface: schema.name,
}; };
if (initialValue.reverseField) {
initialValue.reverseField.name = `f_${uid()}`;
}
// initialValue.uiSchema.title = schema.title; // initialValue.uiSchema.title = schema.title;
return { return {
type: 'object', type: 'object',
@ -95,44 +99,22 @@ const useCreateCollectionField = () => {
const { run } = useCreateAction(); const { run } = useCreateAction();
const { refreshCM } = useCollectionManager(); const { refreshCM } = useCollectionManager();
const { title } = useRecord(); const { title } = useRecord();
const ctx = useActionContext();
const { refresh } = useResourceActionContext();
const { resource } = useResourceContext();
return { return {
async run() { async run() {
await form.submit(); await form.submit();
// const options = form?.values?.uiSchema?.enum?.slice() || []; const values = cloneDeep(form.values);
// if (options?.length) { if (values.autoCreateReverseField) {
// form.setValuesIn( } else {
// 'uiSchema.enum', delete values.reverseField;
// options.map((option) => {
// return {
// value: uid(),
// ...option,
// };
// }),
// );
// }
// function recursiveChildren(children = [], prefix = 'children') {
// children.forEach((item, index) => {
// const itemOptions = item.uiSchema?.enum?.slice() || [];
// form.setValuesIn(
// `${prefix}[${index}].uiSchema.enum`,
// itemOptions.map((option) => {
// return {
// value: uid(),
// ...option,
// };
// }),
// );
// recursiveChildren(item.children, `${prefix}[${index}].children`);
// });
// }
// recursiveChildren(form?.values?.children);
if (['obo', 'oho', 'o2o', 'o2m', 'm2o', 'm2m', 'linkTo'].includes(form?.values?.interface) && title) {
form.setValuesIn('reverseField.uiSchema.title', title);
} }
delete values.autoCreateReverseField;
await run(); await resource.create({ values });
ctx.setVisible(false);
await form.reset();
refresh();
await refreshCM(); await refreshCM();
}, },
}; };
@ -183,7 +165,7 @@ export const AddFieldAction = () => {
<SchemaComponent <SchemaComponent
schema={schema} schema={schema}
components={{ ...components, ArrayTable }} components={{ ...components, ArrayTable }}
scope={{ createOnly: true, useCreateCollectionField }} scope={{ createOnly: true, useCreateCollectionField, record, showReverseFieldConfig: true }}
/> />
</ActionContext.Provider> </ActionContext.Provider>
); );

View File

@ -2,16 +2,17 @@ import { ArrayTable } from '@formily/antd';
import { ISchema, useForm } from '@formily/react'; import { ISchema, useForm } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import set from 'lodash/set';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAPIClient, useRequest } from '../../api-client'; import { useAPIClient, useRequest } from '../../api-client';
import { useRecord } from '../../record-provider'; import { useRecord } from '../../record-provider';
import { ActionContext, SchemaComponent, useCompile } from '../../schema-component'; import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useUpdateAction } from '../action-hooks'; import { useUpdateAction } from '../action-hooks';
import { useCollectionManager } from '../hooks'; import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types'; import { IField } from '../interfaces/types';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import * as components from './components'; import * as components from './components';
import { Typography } from 'antd';
const getSchema = (schema: IField, record: any, compile): ISchema => { const getSchema = (schema: IField, record: any, compile): ISchema => {
if (!schema) { if (!schema) {
@ -50,8 +51,8 @@ const getSchema = (schema: IField, record: any, compile): ISchema => {
type: 'void', type: 'void',
'x-component': 'FieldSummary', 'x-component': 'FieldSummary',
'x-component-props': { 'x-component-props': {
schemaKey: schema.name schemaKey: schema.name,
} },
}, },
// @ts-ignore // @ts-ignore
...properties, ...properties,
@ -86,39 +87,23 @@ const useUpdateCollectionField = () => {
const form = useForm(); const form = useForm();
const { run } = useUpdateAction(); const { run } = useUpdateAction();
const { refreshCM } = useCollectionManager(); const { refreshCM } = useCollectionManager();
const ctx = useActionContext();
const { refresh } = useResourceActionContext();
const { resource, targetKey } = useResourceContext();
const { [targetKey]: filterByTk } = useRecord();
return { return {
async run() { async run() {
await form.submit(); await form.submit();
const options = form?.values?.uiSchema?.enum?.slice() || []; const values = cloneDeep(form.values);
form.setValuesIn( if (values.autoCreateReverseField) {
'uiSchema.enum', } else {
options.map((option) => { delete values.reverseField;
return {
value: uid(),
...option,
};
}),
);
function recursiveChildren(children = [], prefix = 'children') {
children.forEach((item, index) => {
const itemOptions = item.uiSchema?.enum?.slice() || [];
form.setValuesIn(
`${prefix}[${index}].uiSchema.enum`,
itemOptions.map((option) => {
return {
value: uid(),
...option,
};
}),
);
recursiveChildren(item.children, `${prefix}[${index}].children`);
});
} }
delete values.autoCreateReverseField;
recursiveChildren(form?.values?.children); await resource.update({ filterByTk, values });
ctx.setVisible(false);
await run(); await form.reset();
refresh();
await refreshCM(); await refreshCM();
}, },
}; };
@ -132,18 +117,32 @@ export const EditFieldAction = (props) => {
const api = useAPIClient(); const api = useAPIClient();
const { t } = useTranslation(); const { t } = useTranslation();
const compile = useCompile(); const compile = useCompile();
const [data, setData] = useState<any>({});
return ( return (
<ActionContext.Provider value={{ visible, setVisible }}> <ActionContext.Provider value={{ visible, setVisible }}>
<a <a
onClick={async () => { onClick={async () => {
const { data } = await api.resource('collections.fields', record.collectionName).get({ const { data } = await api.resource('collections.fields', record.collectionName).get({
filterByTk: record.name, filterByTk: record.name,
appends: record.interface === 'subTable' ? ['uiSchema', 'children'] : ['uiSchema'], appends: ['uiSchema', 'reverseField'],
}); });
const schema = getSchema({ setData(data?.data);
...getInterface(record.interface), const interfaceConf = getInterface(record.interface);
default: data?.data, const defaultValues: any = cloneDeep(data?.data) || {};
}, record, compile); if (!defaultValues?.reverseField) {
defaultValues.autoCreateReverseField = false;
defaultValues.reverseField = interfaceConf.default?.reverseField;
set(defaultValues.reverseField, 'name', `f_${uid()}`);
set(defaultValues.reverseField, 'uiSchema.title', record.__parent.title);
}
const schema = getSchema(
{
...interfaceConf,
default: defaultValues,
},
record,
compile,
);
setSchema(schema); setSchema(schema);
setVisible(true); setVisible(true);
}} }}
@ -153,7 +152,7 @@ export const EditFieldAction = (props) => {
<SchemaComponent <SchemaComponent
schema={schema} schema={schema}
components={{ ...components, ArrayTable }} components={{ ...components, ArrayTable }}
scope={{ useUpdateCollectionField }} scope={{ useUpdateCollectionField, showReverseFieldConfig: !data?.reverseField }}
/> />
</ActionContext.Provider> </ActionContext.Provider>
); );

View File

@ -1,7 +1,7 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { defaultProps, recordPickerSelector, recordPickerViewer, relationshipType } from './properties'; import { defaultProps, recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties';
import { IField } from './types'; import { IField } from './types';
export const m2m: IField = { export const m2m: IField = {
@ -231,6 +231,7 @@ export const m2m: IField = {
}, },
}, },
}, },
...reverseFieldProperties,
}, },
filterable: { filterable: {
nested: true, nested: true,

View File

@ -1,6 +1,6 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { recordPickerSelector, recordPickerViewer, relationshipType } from './properties'; import { recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties';
import { IField } from './types'; import { IField } from './types';
export const m2o: IField = { export const m2o: IField = {
@ -203,6 +203,7 @@ export const m2o: IField = {
}, },
}, },
}, },
...reverseFieldProperties,
}, },
filterable: { filterable: {
nested: true, nested: true,

View File

@ -1,6 +1,6 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { recordPickerSelector, recordPickerViewer, relationshipType } from './properties'; import { recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties';
import { IField } from './types'; import { IField } from './types';
export const o2m: IField = { export const o2m: IField = {
@ -240,6 +240,7 @@ export const o2m: IField = {
}, },
}, },
}, },
...reverseFieldProperties,
}, },
filterable: { filterable: {
nested: true, nested: true,

View File

@ -1,6 +1,6 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { recordPickerSelector, recordPickerViewer, relationshipType } from './properties'; import { recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties';
import { IField } from './types'; import { IField } from './types';
const internalSchameInitialize = (schema: ISchema, { field, block, readPretty, action }) => { const internalSchameInitialize = (schema: ISchema, { field, block, readPretty, action }) => {
@ -37,11 +37,11 @@ const internalSchameInitialize = (schema: ISchema, { field, block, readPretty, a
'x-initializer': 'FormItemInitializers', 'x-initializer': 'FormItemInitializers',
properties: {}, properties: {},
}, },
} },
}, },
}, },
}, },
} };
} else { } else {
schema.type = 'string'; schema.type = 'string';
if (block === 'Form') { if (block === 'Form') {
@ -57,11 +57,11 @@ const internalSchameInitialize = (schema: ISchema, { field, block, readPretty, a
} else { } else {
schema['properties'] = { schema['properties'] = {
selector: cloneDeep(recordPickerSelector), selector: cloneDeep(recordPickerSelector),
} };
} }
} }
} }
} };
export const o2o: IField = { export const o2o: IField = {
name: 'o2o', name: 'o2o',
@ -207,6 +207,16 @@ export const o2o: IField = {
}, },
}, },
}, },
'reverseField.name': {
type: 'string',
title: '{{t("Inverse field name")}}',
// required: true,
'x-hidden': '{{ !createOnly }}',
'x-decorator': 'FormItem',
'x-component': 'Input',
description:
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
},
}, },
filterable: { filterable: {
nested: true, nested: true,
@ -372,6 +382,7 @@ export const oho: IField = {
}, },
}, },
}, },
...reverseFieldProperties,
}, },
filterable: { filterable: {
nested: true, nested: true,
@ -537,6 +548,7 @@ export const obo: IField = {
}, },
}, },
}, },
...reverseFieldProperties,
}, },
filterable: { filterable: {
nested: true, nested: true,

View File

@ -53,6 +53,89 @@ export const relationshipType: ISchema = {
], ],
}; };
export const reverseFieldProperties: Record<string, ISchema> = {
reverse: {
type: 'void',
'x-component': 'div',
'x-hidden': '{{ !showReverseFieldConfig }}',
properties: {
autoCreateReverseField: {
type: 'boolean',
default: true,
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Create inverse field in the target collection")}}',
'x-reactions': [
{
target: 'reverseField.type',
when: '{{!!$self.value}}',
fulfill: {
state: {
hidden: false,
},
},
otherwise: {
state: {
hidden: true,
},
},
},
{
target: 'reverseField.uiSchema.title',
when: '{{!!$self.value}}',
fulfill: {
state: {
hidden: false,
},
},
otherwise: {
state: {
hidden: true,
},
},
},
{
target: 'reverseField.name',
when: '{{!!$self.value}}',
fulfill: {
state: {
hidden: false,
},
},
otherwise: {
state: {
hidden: true,
},
},
},
],
},
'reverseField.type': {
...relationshipType,
title: '{{t("Inverse relationship type")}}',
},
'reverseField.uiSchema.title': {
type: 'string',
title: '{{t("Inverse field display name")}}',
default: '{{record.title}}',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
'reverseField.name': {
type: 'string',
title: '{{t("Inverse field name")}}',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': 'uid',
description:
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
},
},
},
};
export const dateTimeProps: { [key: string]: ISchema } = { export const dateTimeProps: { [key: string]: ISchema } = {
'uiSchema.x-component-props.dateFormat': { 'uiSchema.x-component-props.dateFormat': {
type: 'string', type: 'string',

View File

@ -175,7 +175,8 @@ export default {
"Time format": "时间格式", "Time format": "时间格式",
"12 hour": "12 小时制", "12 hour": "12 小时制",
"24 hour": "24 小时制", "24 hour": "24 小时制",
"Relationship type": "关联类型", "Relationship type": "关系类型",
"Inverse relationship type": "反向关系类型",
"Source collection": "源数据表", "Source collection": "源数据表",
"Source key": "源数据表字段标识", "Source key": "源数据表字段标识",
"Target collection": "目标数据表", "Target collection": "目标数据表",
@ -683,7 +684,9 @@ export default {
'Marketplace': '插件市场', 'Marketplace': '插件市场',
'Coming soon...': '敬请期待...', 'Coming soon...': '敬请期待...',
'Settings center': '配置中心', 'Settings center': '配置中心',
'Bookmark': '书签', 'Bookmark': '书签',
'Manage all settings': '管理所有配置', 'Manage all settings': '管理所有配置',
'Create inverse field in the target collection': '在目标数据表里创建反向关系字段',
'Inverse field name': '反向关系字段标识',
'Inverse field display name': '反向关系字段名称',
} }

View File

@ -41,7 +41,12 @@ export class CollectionManagerPlugin extends Plugin {
lodash.get(newValue, 'reverseField') && lodash.get(newValue, 'reverseField') &&
!lodash.get(newValue, 'reverseField.key') !lodash.get(newValue, 'reverseField.key')
) { ) {
throw new Error('cant update field without a reverseField key'); const field = await this.app.db
.getModel('fields')
.findByPk(model.get('reverseKey'), { transaction: options.transaction });
if (field) {
throw new Error('cant update field without a reverseField key');
}
} }
}); });