From b42e3b4042fa22f152940788c4c446930a63da34 Mon Sep 17 00:00:00 2001 From: katherinehhh Date: Tue, 25 Jul 2023 14:42:30 +0800 Subject: [PATCH] refactor: form data template support data scope config (#2229) * refactor: data template support data scope config * refactor: data template support data scope config * refactor: locale improve * refactor: code improve * refactor: data template config data scope and title field should linkage with collection field * refactor: code improve * refactor: code improve * refactor: code improve * refactor: code improve * refactor: code improve * refactor: locale improve * refactor: locale improve * refactor: code improve --- packages/core/client/src/locale/en_US.ts | 3 +- packages/core/client/src/locale/ja_JP.ts | 3 +- packages/core/client/src/locale/zh_CN.ts | 3 +- .../antd/form-v2/Templates.tsx | 140 ++++++++++-------- .../DataTemplates/FormDataTemplates.tsx | 99 +++++++------ .../DataTemplates/hooks/useCollectionState.ts | 70 ++++++++- 6 files changed, 203 insertions(+), 115 deletions(-) diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts index f4fd5e52c..d7a2d3153 100644 --- a/packages/core/client/src/locale/en_US.ts +++ b/packages/core/client/src/locale/en_US.ts @@ -708,5 +708,6 @@ export default { "Current object":"Current object", "Linkage with form fields":"Linkage with form fields", "Allow add new, update and delete actions":"Allow add new, update and delete actions", - "Date display format":"Date display format" + "Date display format":"Date display format", + "Assign data scope for the template":"Assign data scope for the template", }; diff --git a/packages/core/client/src/locale/ja_JP.ts b/packages/core/client/src/locale/ja_JP.ts index 39e05fb4a..175508b79 100644 --- a/packages/core/client/src/locale/ja_JP.ts +++ b/packages/core/client/src/locale/ja_JP.ts @@ -619,5 +619,6 @@ export default { "Current object":"現在のオブジェクト", "Linkage with form fields":"フォームデータから連動", "Allow add new, update and delete actions":"削除変更操作の許可", - "Date display format":"日付表示形式" + "Date display format":"日付表示形式", + "Assign data scope for the template":"テンプレートのデータ範囲の指定", } diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 1bc7ea021..2e285cc18 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -793,5 +793,6 @@ export default { "Linkage with form fields":"从表单字段联动", "Failed to load plugin": "插件加载失败", "Allow add new, update and delete actions":"允许增删改操作", - "Date display format":"日期显示格式" + "Date display format":"日期显示格式", + "Assign data scope for the template":"为模板指定数据范围", } diff --git a/packages/core/client/src/schema-component/antd/form-v2/Templates.tsx b/packages/core/client/src/schema-component/antd/form-v2/Templates.tsx index 479d63562..f4126253c 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/Templates.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/Templates.tsx @@ -1,14 +1,16 @@ import { useFieldSchema } from '@formily/react'; import { error, forEach } from '@nocobase/utils/client'; -import { Select } from 'antd'; +import { Select, Space } from 'antd'; import _ from 'lodash'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAPIClient } from '../../../api-client'; import { findFormBlock } from '../../../block-provider'; import { useCollectionManager } from '../../../collection-manager'; import { useDuplicatefieldsContext } from '../../../schema-initializer/components'; +import { compatibleDataId } from '../../../schema-settings/DataTemplates/FormDataTemplates'; import { useToken } from '../__builtins__'; +import { RemoteSelect } from '../remote-select'; export interface ITemplate { config?: { @@ -23,9 +25,11 @@ export interface ITemplate { key: string; title: string; collection: string; - dataId: number; + dataId?: number; fields: string[]; default?: boolean; + dataScope?: object; + titleField?: string; }[]; /** 是否在 Form 区块显示模板选择器 */ display: boolean; @@ -63,49 +67,38 @@ const useDataTemplates = () => { key: 'none', title: t('None'), }, - ].concat(items.map((item, i) => ({ key: i, ...item }))); - + ].concat( + items.map((t, i) => ({ + key: i, + ...t, + isLeaf: t.dataId !== null && t.dataId !== undefined, + titleCollectionField: t?.titleField && getCollectionJoinField(`${t.collection}.${t.titleField}`), + })), + ); const defaultTemplate = items.find((item) => item.default); return { templates, display, defaultTemplate, - enabled: items.length > 0 && items.every((item) => item.dataId !== undefined), + enabled: items.length > 0 && items.every((item) => item.dataId || item.dataScope), }; }; -function filterReferences(obj) { - const filteredObj = {}; - for (const key in obj) { - if (typeof obj[key] !== 'object') { - filteredObj[key] = obj[key]; - } - } - return filteredObj; -} export const Templates = ({ style = {}, form }) => { const { token } = useToken(); const { templates, display, enabled, defaultTemplate } = useDataTemplates(); - const [value, setValue] = React.useState(defaultTemplate?.key || 'none'); + const { getCollectionJoinField } = useCollectionManager(); + const templateOptions = compatibleDataId(templates); + const [targetTemplate, setTargetTemplate] = useState(defaultTemplate?.key || 'none'); + const [targetTemplateData, setTemplateData] = useState(null); const api = useAPIClient(); const { t } = useTranslation(); useEffect(() => { if (enabled && defaultTemplate) { form.__template = true; - fetchTemplateData(api, defaultTemplate, t) - .then((data) => { - if (form && data) { - forEach(data, (value, key) => { - if (value) { - form.values[key] = value; - } - }); - } - return data; - }) - .catch((err) => { - console.error(err); - }); + if (defaultTemplate.key === 'duplicate') { + handleTemplateDataChange(defaultTemplate.dataId, defaultTemplate); + } } }, []); @@ -122,46 +115,69 @@ export const Templates = ({ style = {}, form }) => { return { fontSize: token.fontSize, fontWeight: 'bold', whiteSpace: 'nowrap', marginRight: token.marginXS }; }, [token.fontSize, token.marginXS]); - const handleChange = useCallback(async (value, option) => { - setValue(value); - if (option.key !== 'none') { - fetchTemplateData(api, option, t) - .then((data) => { - if (form && data) { - // 切换之前先把之前的数据清空 - form.reset(); - form.__template = true; + const handleTemplateChange = useCallback(async (value, option) => { + setTargetTemplate(value); + setTemplateData(null); + form?.reset(); + }, []); - forEach(data, (value, key) => { - if (value) { - form.values[key] = value; - } - }); - } - return data; - }) - .catch((err) => { - console.error(err); - }); - } else { - form?.reset(); - } + const handleTemplateDataChange: any = useCallback(async (value, option) => { + const template = { ...option, dataId: value }; + setTemplateData(option); + fetchTemplateData(api, template, t) + .then((data) => { + if (form && data) { + // 切换之前先把之前的数据清空 + form.reset(); + form.__template = true; + + forEach(data, (value, key) => { + if (value) { + form.values[key] = value; + } + }); + } + return data; + }) + .catch((err) => { + console.error(err); + }); }, []); if (!enabled || !display) { return null; } + const template = templateOptions?.find((v) => v.key === targetTemplate); return (
- - + {targetTemplate !== 'none' && ( + handleTemplateDataChange(value.id, { ...value, ...template })} + targetField={getCollectionJoinField(`${template?.collection}.${template.titleField}`)} + /> + )} +
); }; @@ -175,7 +191,7 @@ function findDataTemplates(fieldSchema): ITemplate { } export async function fetchTemplateData(api, template: { collection: string; dataId: number; fields: string[] }, t) { - if (template.fields.length === 0) { + if (template.fields.length === 0 || !template.dataId) { return; } return api diff --git a/packages/core/client/src/schema-settings/DataTemplates/FormDataTemplates.tsx b/packages/core/client/src/schema-settings/DataTemplates/FormDataTemplates.tsx index 335b92706..4337b88c8 100644 --- a/packages/core/client/src/schema-settings/DataTemplates/FormDataTemplates.tsx +++ b/packages/core/client/src/schema-settings/DataTemplates/FormDataTemplates.tsx @@ -7,16 +7,11 @@ import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { mergeFilter } from '../../block-provider'; import { useCollectionManager } from '../../collection-manager'; -import { - AssociationSelect, - SchemaComponent, - SchemaComponentContext, - removeNullCondition, -} from '../../schema-component'; +import { SchemaComponent, SchemaComponentContext, removeNullCondition } from '../../schema-component'; import { ITemplate } from '../../schema-component/antd/form-v2/Templates'; import { AsDefaultTemplate } from './components/AsDefaultTemplate'; import { ArrayCollapse } from './components/DataTemplateTitle'; -import { Designer, getSelectedIdFilter } from './components/Designer'; +import { getSelectedIdFilter } from './components/Designer'; import { useCollectionState } from './hooks/useCollectionState'; const Tree = connect( @@ -27,11 +22,30 @@ const Tree = connect( }), ); +export const compatibleDataId = (data, config?) => { + return data?.map((v) => { + const { dataId, ...others } = v; + const obj = { ...others }; + if (dataId) { + obj.dataScope = { $and: [{ id: { $eq: dataId } }] }; + obj.titleField = obj?.titleField || config?.[v.collection]?.['titleField'] || 'id'; + } + return obj; + }); +}; + export const FormDataTemplates = observer( (props: any) => { const { useProps, formSchema, designerCtx } = props; const { defaultValues, collectionName } = useProps(); - const { collectionList, getEnableFieldTree, getOnLoadData, getOnCheck } = useCollectionState(collectionName); + const { + collectionList, + getEnableFieldTree, + getOnLoadData, + getOnCheck, + getScopeDataSource, + useTitleFieldDataSource, + } = useCollectionState(collectionName); const { getCollection, getCollectionField } = useCollectionManager(); const { t } = useTranslation(); @@ -39,11 +53,15 @@ export const FormDataTemplates = observer( const activeData = useMemo( () => observable( - defaultValues || { items: [], display: true, config: { [collectionName]: { titleField: '', filter: {} } } }, + { ...defaultValues, items: compatibleDataId(defaultValues?.items || [], defaultValues?.config) } || { + items: [], + display: true, + config: { [collectionName]: { titleField: '', filter: {} } }, + }, ), [], ); - + console.log(activeData); const getTargetField = (collectionName: string) => { const collection = getCollection(collectionName); return getCollectionField( @@ -63,8 +81,8 @@ export const FormDataTemplates = observer( const filter = activeData.config?.[collectionName]?.filter; return _.isEmpty(filter) ? {} : removeNullCondition(mergeFilter([filter, getSelectedIdFilter(value)], '$or')); }; - const components = useMemo(() => ({ ArrayCollapse }), []); + const scope = useMemo( () => ({ getEnableFieldTree, @@ -75,6 +93,8 @@ export const FormDataTemplates = observer( getOnLoadData, getOnCheck, collectionName, + getScopeDataSource, + useTitleFieldDataSource, }), [], ); @@ -117,49 +137,39 @@ export const FormDataTemplates = observer( options: collectionList, }, }, - dataId: { - type: 'number', - title: '{{ t("Template Data") }}', - required: true, - description: t('Select an existing piece of data as the initialization data for the form'), - 'x-designer': Designer, - 'x-designer-props': { - formSchema, - data: activeData, - }, + dataScope: { + type: 'object', + title: '{{ t("Assign data scope for the template") }}', 'x-decorator': 'FormItem', - 'x-component': AssociationSelect, - 'x-component-props': { - service: { - resource: '{{ $record.collection || collectionName }}', - params: { - filter: '{{ getFilter($self.componentProps.service.resource, $self.value) }}', - }, + 'x-component': 'Filter', + 'x-decorator-props': { + style: { + marginBottom: '0px', }, - action: 'list', - multiple: false, - objectValue: false, - manual: false, - targetField: '{{ getTargetField($self.componentProps.service.resource) }}', - mapOptions: getMapOptions(), - fieldNames: '{{ getFieldNames($self.componentProps.service.resource) }}', }, + required: true, 'x-reactions': [ { dependencies: ['.collection'], fulfill: { state: { disabled: '{{ !$deps[0] }}', - componentProps: { - service: { - resource: '{{ getResource($deps[0], $self) }}', - }, - }, + }, + schema: { + enum: '{{ getScopeDataSource($deps[0]) }}', }, }, }, ], }, + titleField: { + type: 'string', + 'x-decorator': 'FormItem', + title: '{{ t("Title field") }}', + 'x-component': 'Select', + required: true, + 'x-reactions': '{{useTitleFieldDataSource}}', + }, fields: { type: 'array', title: '{{ t("Data fields") }}', @@ -246,15 +256,6 @@ export function getLabel(titleField) { return titleField || 'label'; } -function getMapOptions() { - return (option) => { - if (option?.id === undefined) { - return null; - } - return option; - }; -} - function getResource(resource: string, field: Field) { if (resource !== field.componentProps.service.resource) { // 切换 collection 后,之前选中的其它 collection 的数据就没有意义了,需要清空 diff --git a/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts b/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts index f9e1ec95f..abd65c4c4 100644 --- a/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts +++ b/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts @@ -1,11 +1,12 @@ import { ArrayField } from '@formily/core'; import React, { useCallback, useState } from 'react'; import { useCollectionManager } from '../../../collection-manager'; +import { isTitleField } from '../../../collection-manager/Configuration/CollectionFields'; import { useCompile } from '../../../schema-component'; import { TreeNode } from '../TreeLabel'; export const useCollectionState = (currentCollectionName: string) => { - const { getCollectionFields, getAllCollectionsInheritChain, getCollection } = useCollectionManager(); + const { getCollectionFields, getAllCollectionsInheritChain, getCollection, getInterface } = useCollectionManager(); const [collectionList] = useState(getCollectionList); const compile = useCompile(); @@ -150,11 +151,78 @@ export const useCollectionState = (currentCollectionName: string) => { }; }, []); + const getScopeDataSource = (resource: string) => { + const fields = getCollectionFields(resource); + const field2option = (field, depth) => { + if (!field.interface) { + return; + } + const fieldInterface = getInterface(field.interface); + if (!fieldInterface?.filterable) { + return; + } + const { nested, children, operators } = fieldInterface.filterable; + const option = { + name: field.name, + title: field?.uiSchema?.title || field.name, + schema: field?.uiSchema, + operators: + operators?.filter?.((operator) => { + return !operator?.visible || operator.visible(field); + }) || [], + interface: field.interface, + }; + if (field.target && depth > 2) { + return; + } + if (depth > 2) { + return option; + } + if (children?.length) { + option['children'] = children; + } + if (nested) { + const targetFields = getCollectionFields(field.target); + const options = getOptions(targetFields, depth + 1).filter(Boolean); + option['children'] = option['children'] || []; + option['children'].push(...options); + } + return option; + }; + const getOptions = (fields, depth) => { + const options = []; + fields.forEach((field) => { + const option = field2option(field, depth); + if (option) { + options.push(option); + } + }); + return options; + }; + const options = getOptions(fields, 1); + return options; + }; + const useTitleFieldDataSource = (field) => { + const fieldPath = field.path.entire.replace('titleField', 'collection'); + const collectionName = field.query(fieldPath).get('value'); + const targetFields = getCollectionFields(collectionName); + const options = targetFields + .filter((field) => { + return !field.isForeignKey && getInterface(field.interface)?.titleUsable; + }) + .map((field) => ({ + value: field?.name, + label: compile(field?.uiSchema?.title) || field?.name, + })); + field.dataSource = options; + }; return { collectionList, getEnableFieldTree, getOnLoadData, getOnCheck, + getScopeDataSource, + useTitleFieldDataSource, }; };