diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts index f19866e20..8c6a8eedf 100644 --- a/packages/core/client/src/locale/en_US.ts +++ b/packages/core/client/src/locale/en_US.ts @@ -709,5 +709,4 @@ export default { "Feedback": "Feedback", "Try again": "Try again", "Data template": "Data template", - "Template fields have been removed and need to be reconfigured": "Template fields have been removed and need to be reconfigured", }; diff --git a/packages/core/client/src/locale/es_ES.ts b/packages/core/client/src/locale/es_ES.ts index 25901eb6e..7221d646b 100644 --- a/packages/core/client/src/locale/es_ES.ts +++ b/packages/core/client/src/locale/es_ES.ts @@ -688,5 +688,4 @@ export default { "Sortable": "Clasificable", "Enable link": "Activar enlace", "Data template": "Plantilla de datos", - "Template fields have been removed and need to be reconfigured": "Los campos de la plantilla se han eliminado y deben reconfigurarse", }; diff --git a/packages/core/client/src/locale/ja_JP.ts b/packages/core/client/src/locale/ja_JP.ts index 5cd578357..4dfa5a6a2 100644 --- a/packages/core/client/src/locale/ja_JP.ts +++ b/packages/core/client/src/locale/ja_JP.ts @@ -618,5 +618,4 @@ export default { 'Display data template selector': 'データテンプレートセレクターを表示', 'Form data templates': 'フォームデータテンプレート', "Data template": "データテンプレート", - "Template fields have been removed and need to be reconfigured": "テンプレートフィールドが削除されました。再設定する必要があります", } diff --git a/packages/core/client/src/locale/pt_BR.ts b/packages/core/client/src/locale/pt_BR.ts index 3f6f3f667..c7e2bf5da 100644 --- a/packages/core/client/src/locale/pt_BR.ts +++ b/packages/core/client/src/locale/pt_BR.ts @@ -668,5 +668,4 @@ export default { 'Display data template selector': 'Exibir seletor de modelo de dados', 'Form data templates': 'Modelos de dados do formulário', "Data template": "Modelo de dados", - "Template fields have been removed and need to be reconfigured": "Os campos do modelo foram removidos e precisam ser reconfigurados", }; diff --git a/packages/core/client/src/locale/ru_RU.ts b/packages/core/client/src/locale/ru_RU.ts index 4f767930c..ffdacaabd 100644 --- a/packages/core/client/src/locale/ru_RU.ts +++ b/packages/core/client/src/locale/ru_RU.ts @@ -522,5 +522,4 @@ export default { 'Display data template selector': "Отображать селектор шаблона данных", 'Form data templates': "Шаблоны данных формы", "Data template": "Шаблон данных", - "Template fields have been removed and need to be reconfigured": "Поля шаблона были удалены и требуют повторной настройки", } diff --git a/packages/core/client/src/locale/tr_TR.ts b/packages/core/client/src/locale/tr_TR.ts index 05ad1cb3f..4acf11b56 100644 --- a/packages/core/client/src/locale/tr_TR.ts +++ b/packages/core/client/src/locale/tr_TR.ts @@ -521,5 +521,4 @@ export default { 'Display data template selector': 'Veri şablonu seçicisini görüntüle', 'Form data templates': 'Form veri şablonları', "Data template": "Veri şablonu", - "Template fields have been removed and need to be reconfigured": "Şablon alanları kaldırıldı ve yeniden yapılandırılması gerekiyor", } diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index d2ccad1f9..3744380e0 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -786,6 +786,5 @@ export default { "Allows to clear cache, reboot application": "允许清除缓存,重启应用", 'The will interrupt service, it may take a few seconds to restart. Are you sure to continue?': '重启将会中断当前服务,这个过程可能需要一点时间,确定要继续吗?', 'Reboot': '重启', - "Template fields have been removed and need to be reconfigured": "模板字段已被删除,需要重新配置", 'Clear cache': '清除缓存', } 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 329901189..e612ac55c 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,5 +1,5 @@ import { useFieldSchema } from '@formily/react'; -import { error, forEach, showToast } from '@nocobase/utils/client'; +import { error, forEach } from '@nocobase/utils/client'; import { Select } from 'antd'; import _ from 'lodash'; import React, { useCallback, useEffect } from 'react'; @@ -71,7 +71,7 @@ export const Templates = ({ style = {}, form }) => { if (defaultTemplate) { fetchTemplateData(api, defaultTemplate, t) .then((data) => { - if (form) { + if (form && data) { forEach(data, (value, key) => { if (value) { form.values[key] = value; @@ -91,7 +91,7 @@ export const Templates = ({ style = {}, form }) => { if (option.key !== 'none') { fetchTemplateData(api, option, t) .then((data) => { - if (form) { + if (form && data) { forEach(data, (value, key) => { if (value) { form.values[key] = value; @@ -138,7 +138,7 @@ function findDataTemplates(fieldSchema): ITemplate { async function fetchTemplateData(api, template: { collection: string; dataId: number; fields: string[] }, t) { if (template.fields.length === 0) { - return showToast(t('Template fields have been removed and need to be reconfigured')); + return; } return api .resource(template.collection) diff --git a/packages/core/client/src/schema-settings/DataTemplates/FormDataTemplates.tsx b/packages/core/client/src/schema-settings/DataTemplates/FormDataTemplates.tsx index 1065922db..46574e48d 100644 --- a/packages/core/client/src/schema-settings/DataTemplates/FormDataTemplates.tsx +++ b/packages/core/client/src/schema-settings/DataTemplates/FormDataTemplates.tsx @@ -1,6 +1,6 @@ import { connect, mapProps, observer } from '@formily/react'; import { Tree as AntdTree } from 'antd'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useCollectionManager } from '../../collection-manager'; import { AssociationSelect, SchemaComponent } from '../../schema-component'; @@ -13,183 +13,185 @@ const Tree = connect( mapProps({ value: 'checkedKeys', dataSource: 'treeData', - onInput: 'onCheck', + // onInput: 'onCheck', }), ); export const FormDataTemplates = observer((props: any) => { const { useProps } = props; const { defaultValues, collectionName } = useProps(); - const { collectionList, getEnableFieldTree } = useCollectionState(collectionName); + 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'}`); - - return ( - ({ ArrayCollapse }), []); + const scope = useMemo(() => ({ getEnableFieldTree }), []); + const schema = useMemo( + () => ({ + type: 'object', + properties: { + items: { + type: 'array', + default: defaultValues?.items, + 'x-component': 'ArrayCollapse', + 'x-decorator': 'FormItem', + 'x-component-props': { + accordion: true, + }, items: { - type: 'array', - default: defaultValues?.items, - 'x-component': 'ArrayCollapse', - 'x-decorator': 'FormItem', + type: 'object', + 'x-component': 'ArrayCollapse.CollapsePanel', 'x-component-props': { - accordion: true, - }, - items: { - type: 'object', - 'x-component': 'ArrayCollapse.CollapsePanel', - 'x-component-props': { - extra: [], - }, - properties: { - layout: { - type: 'void', - 'x-component': 'FormLayout', - 'x-component-props': { - layout: 'vertical', - }, - // TODO: 翻译 - properties: { - collection: { - type: 'string', - title: '{{ t("Collection") }}', - required: true, - description: t('If collection inherits, choose inherited collections as templates'), - default: collectionName, - 'x-display': collectionList.length > 1 ? 'visible' : 'hidden', - 'x-decorator': 'FormItem', - 'x-component': 'Select', - 'x-component-props': { - 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-decorator': 'FormItem', - 'x-component': AssociationSelect, - 'x-component-props': { - service: { - resource: collectionName, - }, - 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', - }, - }, - 'x-reactions': [ - { - dependencies: ['.collection'], - fulfill: { - state: { - disabled: '{{ !$deps[0] }}', - componentProps: { - service: { - resource: '{{ $deps[0] }}', - }, - }, - }, - }, - }, - ], - }, - fields: { - type: 'array', - title: '{{ t("Data fields") }}', - required: true, - description: t('Only the selected fields will be used as the initialization data for the form'), - 'x-decorator': 'FormItem', - 'x-component': Tree, - 'x-component-props': { - treeData: [], - checkable: true, - selectable: false, - rootStyle: { - padding: '8px 0', - border: '1px solid #d9d9d9', - borderRadius: '2px', - maxHeight: '30vh', - overflow: 'auto', - margin: '2px 0', - }, - }, - 'x-reactions': [ - { - dependencies: ['.collection'], - fulfill: { - state: { - disabled: '{{ !$deps[0] }}', - componentProps: { - treeData: '{{ getEnableFieldTree($deps[0]) }}', - }, - }, - }, - }, - ], - }, - }, - }, - remove: { - type: 'void', - 'x-component': 'ArrayCollapse.Remove', - }, - moveUp: { - type: 'void', - 'x-component': 'ArrayCollapse.MoveUp', - }, - moveDown: { - type: 'void', - 'x-component': 'ArrayCollapse.MoveDown', - }, - }, + extra: [], }, properties: { - add: { + layout: { type: 'void', - title: '{{ t("Add template") }}', - 'x-component': 'ArrayCollapse.Addition', + 'x-component': 'FormLayout', + 'x-component-props': { + layout: 'vertical', + }, + properties: { + collection: { + type: 'string', + title: '{{ t("Collection") }}', + required: true, + description: t('If collection inherits, choose inherited collections as templates'), + default: collectionName, + 'x-display': collectionList.length > 1 ? 'visible' : 'hidden', + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-component-props': { + 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-decorator': 'FormItem', + 'x-component': AssociationSelect, + 'x-component-props': { + service: { + resource: collectionName, + }, + 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', + }, + }, + 'x-reactions': [ + { + dependencies: ['.collection'], + fulfill: { + state: { + disabled: '{{ !$deps[0] }}', + componentProps: { + service: { + resource: '{{ $deps[0] }}', + }, + }, + }, + }, + }, + ], + }, + fields: { + type: 'array', + title: '{{ t("Data fields") }}', + required: true, + description: t('Only the selected fields will be used as the initialization data for the form'), + 'x-decorator': 'FormItem', + 'x-component': Tree, + 'x-component-props': { + treeData: [], + checkable: true, + checkStrictly: true, + selectable: false, + loadData: onLoadData, + onCheck, + rootStyle: { + padding: '8px 0', + border: '1px solid #d9d9d9', + borderRadius: '2px', + maxHeight: '30vh', + overflow: 'auto', + margin: '2px 0', + }, + }, + 'x-reactions': [ + { + dependencies: ['.collection'], + fulfill: { + state: { + disabled: '{{ !$deps[0] }}', + componentProps: { + treeData: '{{ getEnableFieldTree($deps[0], $self) }}', + }, + }, + }, + }, + ], + }, + }, + }, + remove: { + type: 'void', + 'x-component': 'ArrayCollapse.Remove', + }, + moveUp: { + type: 'void', + 'x-component': 'ArrayCollapse.MoveUp', + }, + moveDown: { + type: 'void', + 'x-component': 'ArrayCollapse.MoveDown', }, }, }, - display: { - type: 'boolean', - 'x-content': '{{ t("Display data template selector") }}', - default: defaultValues?.display !== false, - 'x-decorator': 'FormItem', - 'x-component': 'Checkbox', + properties: { + add: { + type: 'void', + title: '{{ t("Add template") }}', + 'x-component': 'ArrayCollapse.Addition', + }, }, }, - }} - /> + display: { + type: 'boolean', + 'x-content': '{{ t("Display data template selector") }}', + default: defaultValues?.display !== false, + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + }, + }, + }), + [], ); + + return ; }); function getLabel(collection: any) { 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 6c4be3307..eecd7c811 100644 --- a/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts +++ b/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts @@ -1,4 +1,7 @@ -import React, { useState } from 'react'; +import { ArrayField } from '@formily/core'; +import { error } from '@nocobase/utils/client'; +import _ from 'lodash'; +import React, { useCallback, useState } from 'react'; import { useCollectionManager } from '../../../collection-manager'; import { useCompile } from '../../../schema-component'; import { TreeNode } from '../TreeLabel'; @@ -9,113 +12,213 @@ export const useCollectionState = (currentCollectionName: string) => { const [collectionList] = useState(getCollectionList); const compile = useCompile(); + let dataFields: ArrayField = {} as any; + function getCollectionList() { const collections = getAllCollectionsInheritChain(currentCollectionName); return collections.map((name) => ({ label: getCollection(name)?.title, value: name })); } - const getEnableFieldTree = (collectionName: string) => { + // 过滤掉系统字段 + const systemKeys = [ + // 'id', + 'sort', + 'createdById', + 'createdBy', + 'createdAt', + 'updatedById', + 'updatedBy', + 'updatedAt', + ]; + + /** + * maxDepth: 从 0 开始,0 表示一层,1 表示两层,以此类推 + */ + const traverseFields = (collectionName, { exclude = [], depth = 0, maxDepth, prefix = '' }) => { + if (depth > maxDepth) { + return []; + } + + return getCollectionFields(collectionName) + .map((field) => { + if (exclude.includes(field.name)) { + return; + } + if (!field.interface) { + return; + } + if (['sort', 'password', 'sequence'].includes(field.type)) { + return; + } + const node = { + type: 'duplicate', + tag: compile(field.uiSchema?.title) || field.name, + }; + const option = { + ...node, + title: React.createElement(TreeNode, node), + key: prefix ? `${prefix}.${field.name}` : field.name, + isLeaf: true, + field, + }; + // 多对多和多对一只展示关系字段 + if (['belongsTo', 'belongsToMany'].includes(field.type)) { + node['type'] = 'reference'; + option['type'] = 'reference'; + option['title'] = React.createElement(TreeNode, { ...node, type: 'reference' }); + option.isLeaf = false; + option['children'] = traverseAssociations(field.target, { + depth: depth + 1, + maxDepth, + prefix: option.key, + exclude: systemKeys, + }); + } else if (['hasOne', 'hasMany'].includes(field.type)) { + option.isLeaf = false; + option['children'] = traverseFields(field.target, { + depth: depth + 1, + maxDepth, + prefix: option.key, + exclude: ['id', ...systemKeys], + }); + } + return option; + }) + .filter(Boolean); + }; + + const traverseAssociations = (collectionName, { prefix, maxDepth, depth = 0, exclude = [] }) => { + if (depth > maxDepth) { + return []; + } + return getCollectionFields(collectionName) + .map((field) => { + if (!field.target || !field.interface) { + return; + } + if (exclude.includes(field.name)) { + return; + } + const option = { + type: 'preloading', + tag: compile(field.uiSchema?.title) || field.name, + }; + const value = prefix ? `${prefix}.${field.name}` : field.name; + return { + title: React.createElement(TreeNode, option), + key: value, + isLeaf: false, + field, + children: traverseAssociations(field.target, { + prefix: value, + depth: depth + 1, + maxDepth, + exclude, + }), + }; + }) + .filter(Boolean); + }; + + /** + * fields: 通过在 x-reactions 字段中传递 $self 得到的,相当于 useField 的返回值,目的是修改其中的状态页面会更新 + */ + const getEnableFieldTree = useCallback((collectionName: string, fields: ArrayField) => { + dataFields = fields; + if (!collectionName) { return []; } - // 过滤掉系统字段 - const systemKeys = [ - // 'id', - 'sort', - 'createdById', - 'createdBy', - 'createdAt', - 'updatedById', - 'updatedBy', - 'updatedAt', - ]; - const traverseAssociations = (collectionName, { prefix, maxDepth, depth, exclude = [] }) => { - if (depth > maxDepth) { - return []; - } - return getCollectionFields(collectionName) - .map((field) => { - if (!field.target || !field.interface) { - return; - } - if (exclude.includes(field.name)) { - return; - } - const option = { - type: 'preloading', - tag: compile(field.uiSchema?.title) || field.name, - }; - const value = prefix ? `${prefix}.${field.name}` : field.name; - return { - title: React.createElement(TreeNode, option), - key: value, - children: traverseAssociations(getCollectionFields(field.target), { - prefix: value, - depth: depth + 1, - maxDepth, - exclude, - }), - }; - }) - .filter(Boolean); - }; - const traverseFields = (collectionName, { exclude = [], depth = 0, maxDepth, prefix = '' }) => { - if (depth > maxDepth) { - return []; - } - return getCollectionFields(collectionName) - .map((field) => { - if (exclude.includes(field.name)) { - return; - } - if (!field.interface) { - return; - } - if (['sort', 'password', 'sequence'].includes(field.type)) { - return; - } - const node = { - type: 'duplicate', - tag: compile(field.uiSchema?.title) || field.name, - }; - const option = { - ...node, - title: React.createElement(TreeNode, node), - key: prefix ? `${prefix}.${field.name}` : field.name, - }; - // 多对多的只展示关系字段 - if (['belongsTo', 'belongsToMany'].includes(field.type)) { - node['type'] = 'reference'; - option['type'] = 'reference'; - option['title'] = React.createElement(TreeNode, { ...node, type: 'reference' }); - option['children'] = traverseAssociations(field.target, { - depth: depth + 1, - maxDepth, - prefix: option.key, - exclude: systemKeys, - }); - } else if (['hasOne', 'hasMany'].includes(field.type)) { - option['children'] = traverseFields(field.target, { - depth: depth + 1, - maxDepth, - prefix: option.key, - exclude: ['id', ...systemKeys], - }); - } - return option; - }) - .filter(Boolean); - }; + try { - return traverseFields(collectionName, { exclude: ['id', ...systemKeys], maxDepth: 3 }); + return traverseFields(collectionName, { exclude: ['id', ...systemKeys], maxDepth: 1 }); } catch (error) { console.error(error); return []; } - }; + }, []); + + const onLoadData = useCallback((node) => { + return new Promise((resolve) => { + if (node.children.length) { + node.children.forEach((child) => { + loadChildren({ node: child, traverseAssociations, traverseFields, systemKeys, dataFields }); + }); + return resolve(void 0); + } + + loadChildren({ node, traverseAssociations, traverseFields, systemKeys, dataFields }); + resolve(void 0); + }); + }, []); + + const onCheck = useCallback((checkedKeys, { node, checked }) => { + if (checked) { + let parentKey = node.key.split('.').slice(0, -1).join('.'); + + try { + // 当子节点被选中时,也选中所有祖先节点,提高用户辨识度 + while (parentKey) { + if (parentKey) { + checkedKeys.checked = _.uniq([...checkedKeys.checked, parentKey]); + } + + parentKey = parentKey.split('.').slice(0, -1).join('.'); + } + } catch (err) { + error(err); + } + } + + dataFields.value = checkedKeys; + }, []); return { collectionList, getEnableFieldTree, + onLoadData, + onCheck, }; }; + +// 广度优先遍历查找一个 key 相等的节点并返回 +function findNode(treeData, item) { + const queue = [...treeData]; + while (queue.length) { + const node = queue.shift(); + if (node.key === item.key) { + return node; + } + if (node.children) { + queue.push(...node.children); + } + } +} + +function loadChildren({ node, traverseAssociations, traverseFields, systemKeys, dataFields }) { + const activeNode = findNode(dataFields.componentProps.treeData, node); + let children = []; + + // 多对多和多对一只展示关系字段 + if (['belongsTo', 'belongsToMany'].includes(node.field.type)) { + children = traverseAssociations(node.field.target, { + exclude: systemKeys, + prefix: node.key, + maxDepth: 1, + }); + } else if (['hasOne', 'hasMany'].includes(node.field.type)) { + children = traverseFields(node.field.target, { + exclude: ['id', ...systemKeys], + prefix: node.key, + maxDepth: 1, + }); + } + + if (children.length) { + activeNode.children = children; + } else { + activeNode.isLeaf = true; + } + + return children; +} diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx index 96f80c50c..6086519cd 100644 --- a/packages/core/client/src/schema-settings/SchemaSettings.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx @@ -47,9 +47,9 @@ import { getTargetKey } from '../schema-component/antd/association-filter/utilts import { useSchemaTemplateManager } from '../schema-templates'; import { useBlockTemplateContext } from '../schema-templates/BlockTemplate'; import { FormDataTemplates } from './DataTemplates'; +import { EnableChildCollections } from './EnableChildCollections'; import { FormLinkageRules } from './LinkageRules'; import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks'; -import { EnableChildCollections } from './EnableChildCollections'; interface SchemaSettingsProps { title?: any; @@ -815,6 +815,10 @@ SchemaSettings.ModalItem = function ModalItem(props) { }) .then((values) => { onSubmit(values); + return values; + }) + .catch((err) => { + console.error(err); }); }} > @@ -897,7 +901,7 @@ SchemaSettings.DefaultSortingRules = function DefaultSortingRules(props) { field: { type: 'string', enum: sortFields, - required:true, + required: true, 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-component-props': { @@ -1023,43 +1027,49 @@ SchemaSettings.DataTemplates = function DataTemplates(props) { const formSchema = findFormBlock(fieldSchema) || fieldSchema; const { templateData } = useDataTemplates(); - return ( - { - return { - defaultValues: templateData, - collectionName, - }; - }, - }, + const schema = useMemo( + () => ({ + type: 'object', + title: t('Form data templates'), + properties: { + fieldReaction: { + 'x-component': FormDataTemplates, + 'x-component-props': { + useProps: () => { + return { + defaultValues: templateData, + collectionName, + }; }, }, - } as ISchema - } - onSubmit={(v) => { - const data = v.fieldReaction || {}; - const schema = { - ['x-uid']: formSchema['x-uid'], - ['x-data-templates']: data, - }; - formSchema['x-data-templates'] = data; - dn.emit('patch', { - schema, - }); - dn.refresh(); - }} - /> + }, + }, + }), + [templateData], + ); + const onSubmit = useCallback((v) => { + const data = v.fieldReaction || {}; + + // 当 Tree 组件开启 checkStrictly 属性时,会导致 checkedKeys 的值是一个对象,而不是数组,所以这里需要转换一下以支持旧版本 + data.items.forEach((item) => { + item.fields = Array.isArray(item.fields) ? item.fields : item.fields.checked; + }); + + const schema = { + ['x-uid']: formSchema['x-uid'], + ['x-data-templates']: data, + }; + formSchema['x-data-templates'] = data; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }, []); + const title = useMemo(() => t('Form data templates'), []); + const components = useMemo(() => ({ ArrayCollapse, FormLayout }), []); + + return ( + ); };