mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 08:55:33 +00:00
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
This commit is contained in:
parent
2c8e7b163e
commit
b42e3b4042
@ -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",
|
||||
};
|
||||
|
@ -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":"テンプレートのデータ範囲の指定",
|
||||
}
|
||||
|
@ -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":"为模板指定数据范围",
|
||||
}
|
||||
|
@ -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<any>((item, i) => ({ key: i, ...item })));
|
||||
|
||||
].concat(
|
||||
items.map<any>((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 (
|
||||
<div style={wrapperStyle}>
|
||||
<label style={labelStyle}>{t('Data template')}: </label>
|
||||
<Select
|
||||
popupMatchSelectWidth={false}
|
||||
options={templates}
|
||||
fieldNames={{ label: 'title', value: 'key' }}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Space wrap>
|
||||
<label style={labelStyle}>{t('Data template')}: </label>
|
||||
<Select
|
||||
popupMatchSelectWidth={false}
|
||||
options={templateOptions}
|
||||
fieldNames={{ label: 'title', value: 'key' }}
|
||||
value={targetTemplate}
|
||||
onChange={handleTemplateChange}
|
||||
/>
|
||||
{targetTemplate !== 'none' && (
|
||||
<RemoteSelect
|
||||
style={{ width: 220 }}
|
||||
fieldNames={{ label: template.titleField, value: 'id' }}
|
||||
target={template?.collection}
|
||||
value={targetTemplateData}
|
||||
objectValue
|
||||
service={{
|
||||
resource: template?.collection,
|
||||
params: {
|
||||
filter: template?.dataScope,
|
||||
},
|
||||
}}
|
||||
onChange={(value) => handleTemplateDataChange(value.id, { ...value, ...template })}
|
||||
targetField={getCollectionJoinField(`${template?.collection}.${template.titleField}`)}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -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
|
||||
|
@ -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<ITemplate>(
|
||||
() =>
|
||||
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 的数据就没有意义了,需要清空
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user