From e61568ac57a1d95c827be84e490e27bbe9df58c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=AB=E9=9B=A8=E6=B0=B4=E8=BF=87=E6=BB=A4=E7=9A=84?= =?UTF-8?q?=E7=A9=BA=E6=B0=94-Rairn?= <958414905@qq.com> Date: Thu, 25 May 2023 17:09:29 +0800 Subject: [PATCH] feat(data-template): support to set data scope and title field (#1918) * refactor: optimize * feat(Data-template): support to set data scope and title field * fix: fix error when no template data * fix: fix error * fix: operator * chore: remove id * fix: filter * fix: should be global * fix: submit * fix: global sync * fix: title field * fix: fix filter * fix: fix first created * fix: collection * fix: collection * fix: collection * chore: should show N/A * Revert "chore: should show N/A" This reverts commit 00c91d77612aea0801fec88b5f3f91e4be999439. * chore: should show N/A * chore: remove useless code --- .../Configuration/CollectionFields.tsx | 6 +- .../association-select/AssociationSelect.tsx | 2 +- .../antd/form-v2/Templates.tsx | 16 +- .../antd/remote-select/RemoteSelect.tsx | 80 ++++---- .../core/SchemaComponentProvider.tsx | 2 +- .../src/schema-items/GeneralSchemaItems.tsx | 8 +- .../DataTemplates/FormDataTemplates.tsx | 127 ++++++++---- .../DataTemplates/components/Designer.tsx | 182 ++++++++++++++++++ .../src/schema-settings/SchemaSettings.tsx | 15 +- .../hooks/useIsShowMultipleSwitch.ts | 4 +- .../schema-templates/BlockTemplateDetails.tsx | 2 +- 11 files changed, 354 insertions(+), 90 deletions(-) create mode 100644 packages/core/client/src/schema-settings/DataTemplates/components/Designer.tsx diff --git a/packages/core/client/src/collection-manager/Configuration/CollectionFields.tsx b/packages/core/client/src/collection-manager/Configuration/CollectionFields.tsx index ae67d1eb9..49b86e04c 100644 --- a/packages/core/client/src/collection-manager/Configuration/CollectionFields.tsx +++ b/packages/core/client/src/collection-manager/Configuration/CollectionFields.tsx @@ -74,7 +74,7 @@ const CurrentFields = (props) => { const { resource, targetKey } = props.collectionResource || {}; const { [targetKey]: filterByTk, titleField } = useRecord(); const [loadingRecord, setLoadingRecord] = React.useState(null); - const { updateCollection, refreshCM } = useCollectionManager(); + const { refreshCM } = useCollectionManager(); const columns: TableColumnProps[] = [ { @@ -180,7 +180,7 @@ const InheritFields = (props) => { const { [targetKey]: filterByTk, titleField, name } = useRecord(); const [loadingRecord, setLoadingRecord] = React.useState(null); const { t } = useTranslation(); - const { updateCollection, refreshCM } = useCollectionManager(); + const { refreshCM } = useCollectionManager(); const columns: TableColumnProps[] = [ { @@ -265,7 +265,7 @@ const InheritFields = (props) => { ); }; -export const CollectionFields = (props) => { +export const CollectionFields = () => { const compile = useCompile(); const field = useField(); const { name } = useRecord(); diff --git a/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx b/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx index 85e117e35..ccba7e0f0 100644 --- a/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx +++ b/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx @@ -160,7 +160,7 @@ AssociationSelect.Designer = function Designer() { readOnlyMode = 'read-pretty'; } - const fieldSchemaWithoutRequired = _.omit(fieldSchema,'required') + const fieldSchemaWithoutRequired = _.omit(fieldSchema, 'required'); return ( 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 e612ac55c..590596cc1 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 @@ -8,7 +8,15 @@ import { useAPIClient } from '../../../api-client'; import { findFormBlock } from '../../../block-provider'; import { useCollectionManager } from '../../../collection-manager'; -interface ITemplate { +export interface ITemplate { + config?: { + [key: string]: { + /** 设置的数据范围 */ + filter?: any; + /** 设置的标题字段 */ + titleField?: string; + }; + }; items: { key: string; title: string; @@ -31,7 +39,7 @@ const useDataTemplates = () => { items.forEach((item) => { try { item.fields = item.fields - .map((field) => { + ?.map((field) => { const joinField = getCollectionJoinField(`${item.collection}.${field}`); if (joinField) { return field; @@ -57,7 +65,7 @@ const useDataTemplates = () => { templates, display, defaultTemplate, - enabled: items.length > 0, + enabled: items.length > 0 && items.every((item) => item.dataId !== undefined), }; }; @@ -68,7 +76,7 @@ export const Templates = ({ style = {}, form }) => { const { t } = useTranslation(); useEffect(() => { - if (defaultTemplate) { + if (enabled && defaultTemplate) { fetchTemplateData(api, defaultTemplate, t) .then((data) => { if (form && data) { diff --git a/packages/core/client/src/schema-component/antd/remote-select/RemoteSelect.tsx b/packages/core/client/src/schema-component/antd/remote-select/RemoteSelect.tsx index 069b0ee16..3fcc0dbb9 100644 --- a/packages/core/client/src/schema-component/antd/remote-select/RemoteSelect.tsx +++ b/packages/core/client/src/schema-component/antd/remote-select/RemoteSelect.tsx @@ -7,10 +7,11 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { ResourceActionOptions, useRequest } from '../../../api-client'; import { mergeFilter } from '../../../block-provider/SharedFilterProvider'; import { useCollection, useCollectionManager } from '../../../collection-manager'; -import { useCompile } from '../../hooks'; import { Select, defaultFieldNames } from '../select'; import { ReadPretty } from './ReadPretty'; +const EMPTY = 'N/A'; + export type RemoteSelectProps

= SelectProps & { objectValue?: boolean; onChange?: (v: any) => void; @@ -35,7 +36,6 @@ const InternalRemoteSelect = connect( targetField: _targetField, ...others } = props; - const compile = useCompile(); const firstRun = useRef(false); const fieldSchema = useFieldSchema(); const field = useField(); @@ -58,49 +58,51 @@ const InternalRemoteSelect = connect( const mapOptionsToTags = useCallback( (options) => { try { - return options.map((option) => { - let label = option[fieldNames.label]; + return options + .map((option) => { + let label = option[fieldNames.label]; - if (targetField?.uiSchema?.enum) { - if (Array.isArray(label)) { - label = label - .map((item, index) => { - const option = targetField.uiSchema.enum.find((i) => i.value === item); - if (option) { - return ( - - {option?.label || item} - - ); - } else { - return {item}; - } - }) - .reverse(); - } else { - const item = targetField.uiSchema.enum.find((i) => i.value === label); - if (item) { - label = {item.label}; + if (targetField?.uiSchema?.enum) { + if (Array.isArray(label)) { + label = label + .map((item, index) => { + const option = targetField.uiSchema.enum.find((i) => i.value === item); + if (option) { + return ( + + {option?.label || item} + + ); + } else { + return {item}; + } + }) + .reverse(); + } else { + const item = targetField.uiSchema.enum.find((i) => i.value === label); + if (item) { + label = {item.label}; + } } } - } - if (targetField?.type === 'date') { - label = moment(label).format('YYYY-MM-DD'); - } + if (targetField?.type === 'date') { + label = moment(label).format('YYYY-MM-DD'); + } - if (mapOptions) { - return mapOptions({ - [fieldNames.label]: label, + if (mapOptions) { + return mapOptions({ + [fieldNames.label]: label || EMPTY, + [fieldNames.value]: option[fieldNames.value], + }); + } + return { + ...option, + [fieldNames.label]: label || EMPTY, [fieldNames.value]: option[fieldNames.value], - }); - } - return { - ...option, - [fieldNames.label]: label || option[fieldNames.value], - [fieldNames.value]: option[fieldNames.value], - }; - }); + }; + }) + .filter(Boolean); } catch (err) { console.error(err); return options; diff --git a/packages/core/client/src/schema-component/core/SchemaComponentProvider.tsx b/packages/core/client/src/schema-component/core/SchemaComponentProvider.tsx index 76c09f6f9..36b470a6b 100644 --- a/packages/core/client/src/schema-component/core/SchemaComponentProvider.tsx +++ b/packages/core/client/src/schema-component/core/SchemaComponentProvider.tsx @@ -35,7 +35,7 @@ export const SchemaComponentProvider: React.FC = (prop const { designable, components, children } = props; const [, setUid] = useState(uid()); const [formId, setFormId] = useState(uid()); - const form = props.form || useMemo(() => createForm(), [formId]); + const form = useMemo(() => props.form || createForm(), [formId]); const { t } = useTranslation(); const scope = { ...props.scope, t, randomString }; const [active, setActive] = useCookieState('useCookieDesignable', { diff --git a/packages/core/client/src/schema-items/GeneralSchemaItems.tsx b/packages/core/client/src/schema-items/GeneralSchemaItems.tsx index 6cd7d60f6..3770b9a28 100644 --- a/packages/core/client/src/schema-items/GeneralSchemaItems.tsx +++ b/packages/core/client/src/schema-items/GeneralSchemaItems.tsx @@ -1,9 +1,8 @@ import { Field } from '@formily/core'; -import { ISchema, useField, useFieldSchema,observer } from '@formily/react'; -import _ from 'lodash'; -import { useTranslation } from 'react-i18next'; +import { ISchema, observer, useField, useFieldSchema } from '@formily/react'; import React from 'react'; -import { useCollectionManager, useCollection } from '../collection-manager'; +import { useTranslation } from 'react-i18next'; +import { useCollection, useCollectionManager } from '../collection-manager'; import { useDesignable } from '../schema-component'; import { SchemaSettings } from '../schema-settings'; @@ -18,7 +17,6 @@ export const GeneralSchemaItems: React.FC<{ const { t } = useTranslation(); const { dn, refresh } = useDesignable(); const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); -console.log(field,field.decoratorProps ) return ( <> {collectionField && ( diff --git a/packages/core/client/src/schema-settings/DataTemplates/FormDataTemplates.tsx b/packages/core/client/src/schema-settings/DataTemplates/FormDataTemplates.tsx index 46574e48d..c7886670e 100644 --- a/packages/core/client/src/schema-settings/DataTemplates/FormDataTemplates.tsx +++ b/packages/core/client/src/schema-settings/DataTemplates/FormDataTemplates.tsx @@ -1,11 +1,22 @@ +import { Field } from '@formily/core'; import { connect, mapProps, observer } from '@formily/react'; +import { observable } from '@formily/reactive'; import { Tree as AntdTree } from 'antd'; +import _ from 'lodash'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { mergeFilter } from '../../block-provider'; import { useCollectionManager } from '../../collection-manager'; -import { AssociationSelect, SchemaComponent } from '../../schema-component'; +import { + AssociationSelect, + 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 { useCollectionState } from './hooks/useCollectionState'; const Tree = connect( @@ -18,22 +29,60 @@ const Tree = connect( ); export const FormDataTemplates = observer((props: any) => { - const { useProps } = props; + const { useProps, formSchema, designerCtx } = props; const { defaultValues, collectionName } = useProps(); const { collectionList, getEnableFieldTree, onLoadData, onCheck } = useCollectionState(collectionName); const { getCollection, getCollectionField } = useCollectionManager(); - const collection = getCollection(collectionName); const { t } = useTranslation(); - const field = getCollectionField(`${collectionName}.${collection?.titleField || 'id'}`); + + // 不要在后面的数组中依赖 defaultValues,否则会因为 defaultValues 的变化导致 activeData 响应性丢失 + const activeData = useMemo( + () => + observable( + defaultValues || { items: [], display: true, config: { [collectionName]: { titleField: '', filter: {} } } }, + ), + [], + ); + + const getTargetField = (collectionName: string) => { + const collection = getCollection(collectionName); + return getCollectionField( + `${collectionName}.${activeData?.config[collectionName]?.titleField || collection?.titleField || 'id'}`, + ); + }; + + const getFieldNames = (collectionName: string) => { + const collection = getCollection(collectionName); + return { + label: getLabel(activeData.config?.[collectionName]?.titleField || collection?.titleField || 'id'), + value: 'id', + }; + }; + + const getFilter = (collectionName: string, value: any) => { + const filter = activeData.config?.[collectionName]?.filter; + return _.isEmpty(filter) ? {} : removeNullCondition(mergeFilter([filter, getSelectedIdFilter(value)], '$or')); + }; + const components = useMemo(() => ({ ArrayCollapse }), []); - const scope = useMemo(() => ({ getEnableFieldTree }), []); + const scope = useMemo( + () => ({ + getEnableFieldTree, + getTargetField, + getFieldNames, + getFilter, + getResource, + collectionName, + }), + [], + ); const schema = useMemo( () => ({ type: 'object', properties: { items: { type: 'array', - default: defaultValues?.items, + default: activeData?.items, 'x-component': 'ArrayCollapse', 'x-decorator': 'FormItem', 'x-component-props': { @@ -58,7 +107,7 @@ export const FormDataTemplates = observer((props: any) => { title: '{{ t("Collection") }}', required: true, description: t('If collection inherits, choose inherited collections as templates'), - default: collectionName, + default: '{{ collectionName }}', 'x-display': collectionList.length > 1 ? 'visible' : 'hidden', 'x-decorator': 'FormItem', 'x-component': 'Select', @@ -71,36 +120,27 @@ export const FormDataTemplates = observer((props: any) => { 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, + }, 'x-decorator': 'FormItem', 'x-component': AssociationSelect, 'x-component-props': { service: { - resource: collectionName, + resource: '{{ $record.collection || collectionName }}', + params: { + filter: '{{ getFilter($self.componentProps.service.resource, $self.value) }}', + }, }, action: 'list', multiple: false, objectValue: false, manual: false, - targetField: field, - mapOptions(option) { - try { - const label = getLabel(collection); - option[label] = ( - <> - #{option.id} {option[label]} - - ); - - return option; - } catch (error) { - console.error(error); - return option; - } - }, - fieldNames: { - label: getLabel(collection), - value: 'id', - }, + targetField: '{{ getTargetField($self.componentProps.service.resource) }}', + mapOptions: getMapOptions(), + fieldNames: '{{ getFieldNames($self.componentProps.service.resource) }}', }, 'x-reactions': [ { @@ -110,7 +150,7 @@ export const FormDataTemplates = observer((props: any) => { disabled: '{{ !$deps[0] }}', componentProps: { service: { - resource: '{{ $deps[0] }}', + resource: '{{ getResource($deps[0], $self) }}', }, }, }, @@ -182,7 +222,7 @@ export const FormDataTemplates = observer((props: any) => { display: { type: 'boolean', 'x-content': '{{ t("Display data template selector") }}', - default: defaultValues?.display !== false, + default: activeData?.display !== false, 'x-decorator': 'FormItem', 'x-component': 'Checkbox', }, @@ -191,9 +231,30 @@ export const FormDataTemplates = observer((props: any) => { [], ); - return ; + return ( + + + + ); }); -function getLabel(collection: any) { - return !collection?.titleField || collection.titleField === 'id' ? 'label' : collection?.titleField; +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 的数据就没有意义了,需要清空 + field.value = undefined; + } + return resource; } diff --git a/packages/core/client/src/schema-settings/DataTemplates/components/Designer.tsx b/packages/core/client/src/schema-settings/DataTemplates/components/Designer.tsx new file mode 100644 index 000000000..8412e4542 --- /dev/null +++ b/packages/core/client/src/schema-settings/DataTemplates/components/Designer.tsx @@ -0,0 +1,182 @@ +import { Field } from '@formily/core'; +import { ISchema, observer, useField, useFieldSchema } from '@formily/react'; +import { error } from '@nocobase/utils/client'; +import { Select } from 'antd'; +import _ from 'lodash'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { GeneralSchemaDesigner, SchemaSettings } from '../..'; +import { mergeFilter } from '../../../block-provider'; +import { useCollectionFilterOptions, useCollectionManager } from '../../../collection-manager'; +import { isTitleField } from '../../../collection-manager/Configuration/CollectionFields'; +import { removeNullCondition, useCompile, useDesignable } from '../../../schema-component'; +import { ITemplate } from '../../../schema-component/antd/form-v2/Templates'; +import { FilterDynamicComponent } from '../../../schema-component/antd/table-v2/FilterDynamicComponent'; + +export const Designer = observer(() => { + const { getCollectionFields, getCollectionField, getCollection } = useCollectionManager(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + const compile = useCompile(); + const { formSchema, data } = fieldSchema['x-designer-props'] as { + formSchema: ISchema; + data?: ITemplate; + }; + + // 在这里读取 resource 的值,当 resource 变化时,会触发该组件的更新 + const collectionName = field.componentProps.service.resource; + + const collection = getCollection(collectionName); + const collectionFields = getCollectionFields(collectionName); + const dataSource = useCollectionFilterOptions(collectionName); + + if (!data) { + error('data is required'); + return null; + } + + const getFilter = () => data.config?.[collectionName]?.filter || {}; + const setFilter = (filter) => { + try { + _.set(data, `config.${collectionName}.filter`, removeNullCondition(filter)); + } catch (err) { + error(err); + } + }; + const getTitleFIeld = () => data.config?.[collectionName]?.titleField || collection?.titleField || 'id'; + const setTitleField = (titleField) => { + try { + _.set(data, `config.${collectionName}.titleField`, titleField); + } catch (err) { + error(err); + } + }; + + const options = collectionFields + .filter((field) => isTitleField(field)) + .map((field) => ({ + value: field?.name, + label: compile(field?.uiSchema?.title) || field?.name, + })); + + return ( + + FilterDynamicComponent({ ...props }), + }, + }, + }, + } as ISchema + } + onSubmit={({ filter }) => { + setFilter(filter); + + try { + // 不仅更新当前模板,也更新同级的其它模板 + field.query('fieldReaction.items.*.layout.dataId').forEach((item) => { + if (item.componentProps.service.resource !== collectionName) { + return; + } + + item.componentProps.service.params = { + filter: _.isEmpty(filter) + ? {} + : removeNullCondition(mergeFilter([filter, getSelectedIdFilter(field.value)], '$or')), + }; + }); + } catch (err) { + error(err); + } + formSchema['x-data-templates'] = data; + dn.emit('patch', { + schema: { + ['x-uid']: formSchema['x-uid'], + ['x-data-templates']: data, + }, + }); + dn.refresh(); + }} + /> + { + setTitleField(label); + + try { + // 不仅更新当前模板,也更新同级的其它模板 + field.query('fieldReaction.items.*.layout.dataId').forEach((item) => { + if (item.componentProps.service.resource !== collectionName) { + return; + } + + item.componentProps.fieldNames.label = label; + item.componentProps.targetField = getCollectionField( + `${collectionName}.${label || collection?.titleField || 'id'}`, + ); + }); + } catch (err) { + console.error(err); + } + formSchema['x-data-templates'] = data; + + const schema = { + ['x-uid']: formSchema['x-uid'], + ['x-data-templates']: data, + }; + + dn.emit('patch', { + schema, + }); + dn.refresh(); + }} + /> + + ); +}); + +export function getSelectedIdFilter(selectedId) { + return selectedId + ? { + id: { + $eq: selectedId, + }, + } + : null; +} + +function SelectItem(props) { + const { title, options, value, onChange, ...others } = props; + + return ( + +

+ {title} +