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
This commit is contained in:
被雨水过滤的空气-Rairn 2023-05-25 17:09:29 +08:00 committed by GitHub
parent c722ca41d9
commit e61568ac57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 354 additions and 90 deletions

View File

@ -74,7 +74,7 @@ const CurrentFields = (props) => {
const { resource, targetKey } = props.collectionResource || {};
const { [targetKey]: filterByTk, titleField } = useRecord();
const [loadingRecord, setLoadingRecord] = React.useState<any>(null);
const { updateCollection, refreshCM } = useCollectionManager();
const { refreshCM } = useCollectionManager();
const columns: TableColumnProps<any>[] = [
{
@ -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<any>[] = [
{
@ -265,7 +265,7 @@ const InheritFields = (props) => {
);
};
export const CollectionFields = (props) => {
export const CollectionFields = () => {
const compile = useCompile();
const field = useField<Field>();
const { name } = useRecord();

View File

@ -160,7 +160,7 @@ AssociationSelect.Designer = function Designer() {
readOnlyMode = 'read-pretty';
}
const fieldSchemaWithoutRequired = _.omit(fieldSchema,'required')
const fieldSchemaWithoutRequired = _.omit(fieldSchema, 'required');
return (
<GeneralSchemaDesigner>

View File

@ -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) {

View File

@ -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<P = any> = SelectProps<P, any> & {
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 (
<Tag key={index} color={option.color} style={{ marginRight: 3 }}>
{option?.label || item}
</Tag>
);
} else {
return <Tag>{item}</Tag>;
}
})
.reverse();
} else {
const item = targetField.uiSchema.enum.find((i) => i.value === label);
if (item) {
label = <Tag color={item.color}>{item.label}</Tag>;
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 (
<Tag key={index} color={option.color} style={{ marginRight: 3 }}>
{option?.label || item}
</Tag>
);
} else {
return <Tag key={item}>{item}</Tag>;
}
})
.reverse();
} else {
const item = targetField.uiSchema.enum.find((i) => i.value === label);
if (item) {
label = <Tag color={item.color}>{item.label}</Tag>;
}
}
}
}
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;

View File

@ -35,7 +35,7 @@ export const SchemaComponentProvider: React.FC<ISchemaComponentProvider> = (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', {

View File

@ -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 && (

View File

@ -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<ITemplate>(
() =>
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 <SchemaComponent components={components} scope={scope} schema={schema} />;
return (
<SchemaComponentContext.Provider value={{ ...designerCtx, designable: true }}>
<SchemaComponent components={components} scope={scope} schema={schema} />
</SchemaComponentContext.Provider>
);
});
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;
}

View File

@ -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<Field>();
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 (
<GeneralSchemaDesigner draggable={false}>
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: getFilter(),
// title: '数据范围',
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: (props) => 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();
}}
/>
<SelectItem
key="title-field"
title={t('Title field')}
options={options}
value={getTitleFIeld()}
onChange={(label) => {
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();
}}
/>
</GeneralSchemaDesigner>
);
});
export function getSelectedIdFilter(selectedId) {
return selectedId
? {
id: {
$eq: selectedId,
},
}
: null;
}
function SelectItem(props) {
const { title, options, value, onChange, ...others } = props;
return (
<SchemaSettings.Item {...others}>
<div style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}>
{title}
<Select
bordered={false}
value={value}
onChange={onChange}
options={options}
style={{ textAlign: 'right', minWidth: 100 }}
{...others}
/>
</div>
</SchemaSettings.Item>
);
}

View File

@ -31,6 +31,7 @@ import {
FormProvider,
RemoteSchemaComponent,
SchemaComponent,
SchemaComponentContext,
SchemaComponentOptions,
createDesignable,
findFormBlock,
@ -1011,8 +1012,15 @@ SchemaSettings.LinkageRules = function LinkageRules(props) {
);
};
export const useDataTemplates = () => {
export const useDataTemplates = (schema?: Schema) => {
const fieldSchema = useFieldSchema();
if (schema) {
return {
templateData: _.cloneDeep(schema['x-data-templates']),
};
}
const formSchema = findFormBlock(fieldSchema) || fieldSchema;
return {
templateData: _.cloneDeep(formSchema?.['x-data-templates']),
@ -1020,6 +1028,7 @@ export const useDataTemplates = () => {
};
SchemaSettings.DataTemplates = function DataTemplates(props) {
const designerCtx = useContext(SchemaComponentContext);
const { collectionName } = props;
const fieldSchema = useFieldSchema();
const { dn } = useDesignable();
@ -1035,6 +1044,8 @@ SchemaSettings.DataTemplates = function DataTemplates(props) {
fieldReaction: {
'x-component': FormDataTemplates,
'x-component-props': {
designerCtx,
formSchema,
useProps: () => {
return {
defaultValues: templateData,
@ -1048,7 +1059,7 @@ SchemaSettings.DataTemplates = function DataTemplates(props) {
[templateData],
);
const onSubmit = useCallback((v) => {
const data = v.fieldReaction || {};
const data = { ...(formSchema['x-data-templates'] || {}), ...v.fieldReaction };
// 当 Tree 组件开启 checkStrictly 属性时,会导致 checkedKeys 的值是一个对象,而不是数组,所以这里需要转换一下以支持旧版本
data.items.forEach((item) => {

View File

@ -9,7 +9,9 @@ export function useIsShowMultipleSwitch() {
const fieldSchema = useFieldSchema();
const { getCollectionField } = useCollectionManager();
const collectionField = getCollectionField(fieldSchema['x-collection-field']);
const collectionField = fieldSchema['x-collection-field']
? getCollectionField(fieldSchema['x-collection-field'])
: null;
const uiSchema = collectionField?.uiSchema || fieldSchema;
const hasMultiple = uiSchema['x-component-props']?.multiple === true;

View File

@ -1,4 +1,4 @@
import { Input, PageHeader as AntdPageHeader, Spin } from 'antd';
import { PageHeader as AntdPageHeader, Input, Spin } from 'antd';
import React, { useContext, useState } from 'react';
import { useHistory, useRouteMatch } from 'react-router-dom';
import { useAPIClient, useRequest, useSchemaTemplateManager } from '..';