feat(form-block): data templates (#1704)
* feat(Form): support to select existing data as template * refactor: extract useDataTemplates * feat(Form): support to use template * fix: template switch * fix: fix association field * fix: filter fields * fix: fix unselected default value * fix: avoid errors * refactor: remove useless code * refactor: move templateSelect to FormBlockProvider * feat: add a checkbox to toggle template selector * feat: change the options order * feat: hide Collection option when no inherit * fix: optimize the label text * fix: should empty form * fix: should hide configuration when is not added * chore: change text * fix: template selector not displayed * feat: optimize template * feat: data template middleware * fix: template select * fix: default * fix: fields * feat: field delete button changed from hidden to disabled * fix: improve code * fix: prefix error * fix: items * feat: use Tree * fix: maxDepth --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
67dc21baf7
commit
69bc18e166
@ -1,11 +1,12 @@
|
||||
import { createForm } from '@formily/core';
|
||||
import { useField } from '@formily/react';
|
||||
import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import { Spin } from 'antd';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
|
||||
import { useCollection } from '../collection-manager';
|
||||
import { RecordProvider, useRecord } from '../record-provider';
|
||||
import { useActionContext, useDesignable } from '../schema-component';
|
||||
import { Templates as DataTemplateSelect } from '../schema-component/antd/form-v2/Templates';
|
||||
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
||||
|
||||
export const FormBlockContext = createContext<any>({});
|
||||
@ -40,10 +41,14 @@ const InternalFormBlockProvider = (props) => {
|
||||
>
|
||||
{readPretty ? (
|
||||
<RecordProvider parent={isEmpty(record?.__parent) ? record : record?.__parent} record={service?.data?.data}>
|
||||
<div ref={formBlockRef}>{props.children}</div>
|
||||
<div ref={formBlockRef}>
|
||||
<RenderChildrenWithDataTemplates form={form} />
|
||||
</div>
|
||||
</RecordProvider>
|
||||
) : (
|
||||
<div ref={formBlockRef}>{props.children}</div>
|
||||
<div ref={formBlockRef}>
|
||||
<RenderChildrenWithDataTemplates form={form} />
|
||||
</div>
|
||||
)}
|
||||
</FormBlockContext.Provider>
|
||||
);
|
||||
@ -108,3 +113,27 @@ export const useFormBlockProps = () => {
|
||||
form: ctx.form,
|
||||
};
|
||||
};
|
||||
|
||||
const RenderChildrenWithDataTemplates = ({ form }) => {
|
||||
const FieldSchema = useFieldSchema();
|
||||
const { findComponent } = useDesignable();
|
||||
const field = useField();
|
||||
const Component = findComponent(field.component?.[0]) || React.Fragment;
|
||||
|
||||
return (
|
||||
<Component {...field.componentProps}>
|
||||
<DataTemplateSelect style={{ marginBottom: 18 }} form={form} />
|
||||
<RecursionField schema={FieldSchema} onlyRenderProperties />
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
export const findFormBlock = (schema: Schema) => {
|
||||
while (schema) {
|
||||
if (schema['x-decorator'] === 'FormBlockProvider') {
|
||||
return schema;
|
||||
}
|
||||
schema = schema.parent;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
@ -194,7 +194,7 @@ export const collectionFieldSchema: ISchema = {
|
||||
delete: {
|
||||
type: 'void',
|
||||
title: '{{ t("Delete") }}',
|
||||
'x-visible': '{{cm.useDeleteButtonVisible()}}',
|
||||
'x-disabled': '{{cm.useDeleteButtonDisabled()}}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
confirm: {
|
||||
|
@ -3,9 +3,9 @@ import { message } from 'antd';
|
||||
import omit from 'lodash/omit';
|
||||
import { useEffect } from 'react';
|
||||
import { useCollection, useCollectionManager } from '.';
|
||||
import { useCompile } from '..';
|
||||
import { useRequest } from '../api-client';
|
||||
import { useRecord } from '../record-provider';
|
||||
import { useCompile } from '..';
|
||||
import { useActionContext } from '../schema-component';
|
||||
import { useFilterFieldOptions, useFilterFieldProps } from '../schema-component/antd/filter/useFilterActionProps';
|
||||
import { useResourceActionContext, useResourceContext } from './ResourceActionProvider';
|
||||
@ -377,10 +377,10 @@ export const useDestroyActionAndRefreshCM = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const useDeleteButtonVisible = () => {
|
||||
export const useDeleteButtonDisabled = () => {
|
||||
const { interface: i, deletable = true } = useRecord();
|
||||
|
||||
return deletable && i !== 'id';
|
||||
return !deletable || i === 'id';
|
||||
};
|
||||
|
||||
export const useBulkDestroyActionAndRefreshCM = () => {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { clone } from '@formily/shared';
|
||||
import { CascaderProps } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { reduce, unionBy, uniq, uniqBy } from 'lodash';
|
||||
import _, { reduce, unionBy, uniq, uniqBy } from 'lodash';
|
||||
import { useContext } from 'react';
|
||||
import { useCompile } from '../../schema-component';
|
||||
import { CollectionManagerContext } from '../context';
|
||||
@ -108,9 +107,31 @@ export const useCollectionManager = () => {
|
||||
* Max depth of recursion
|
||||
*/
|
||||
maxDepth?: number;
|
||||
allowAllTypes?: boolean;
|
||||
/**
|
||||
* 排除这些接口的字段
|
||||
*/
|
||||
exceptInterfaces?: string[];
|
||||
/**
|
||||
* field value 的前缀,用 . 连接,比如 a.b.c
|
||||
*/
|
||||
prefixFieldValue?: string;
|
||||
/**
|
||||
* 是否使用 prefixFieldValue 作为 field value
|
||||
*/
|
||||
usePrefix?: boolean;
|
||||
},
|
||||
) => {
|
||||
const { association = false, cached = {}, collectionNames = [collectionName], maxDepth = 1 } = opts || {};
|
||||
const {
|
||||
association = false,
|
||||
cached = {},
|
||||
collectionNames = [collectionName],
|
||||
maxDepth = 1,
|
||||
allowAllTypes = false,
|
||||
exceptInterfaces = [],
|
||||
prefixFieldValue = '',
|
||||
usePrefix = false,
|
||||
} = opts || {};
|
||||
|
||||
if (collectionNames.length - 1 > maxDepth) {
|
||||
return;
|
||||
@ -129,14 +150,16 @@ export const useCollectionManager = () => {
|
||||
?.filter(
|
||||
(field) =>
|
||||
field.interface &&
|
||||
(type.includes(field.type) ||
|
||||
!exceptInterfaces.includes(field.interface) &&
|
||||
(allowAllTypes ||
|
||||
type.includes(field.type) ||
|
||||
(association && field.target && field.target !== collectionName && Array.isArray(association)
|
||||
? association.includes(field.interface)
|
||||
: false)),
|
||||
)
|
||||
?.map((field) => {
|
||||
const result: CascaderProps<any>['options'][0] = {
|
||||
value: field.name,
|
||||
value: usePrefix && prefixFieldValue ? `${prefixFieldValue}.${field.name}` : field.name,
|
||||
label: compile(field?.uiSchema?.title) || field.name,
|
||||
...field,
|
||||
};
|
||||
@ -147,6 +170,12 @@ export const useCollectionManager = () => {
|
||||
...opts,
|
||||
cached,
|
||||
collectionNames: [...collectionNames, field.target],
|
||||
prefixFieldValue: usePrefix
|
||||
? prefixFieldValue
|
||||
? `${prefixFieldValue}.${field.name}`
|
||||
: field.name
|
||||
: '',
|
||||
usePrefix,
|
||||
});
|
||||
if (!result.children?.length) {
|
||||
return null;
|
||||
@ -161,6 +190,50 @@ export const useCollectionManager = () => {
|
||||
return options;
|
||||
};
|
||||
|
||||
const getCollection = (name: any) => {
|
||||
if (typeof name !== 'string') {
|
||||
return name;
|
||||
}
|
||||
return collections?.find((collection) => collection.name === name);
|
||||
};
|
||||
|
||||
// 获取当前 collection 继承链路上的所有 collection
|
||||
const getAllCollectionsInheritChain = (collectionName: string) => {
|
||||
const collectionsInheritChain = [collectionName];
|
||||
const getInheritChain = (name: string) => {
|
||||
const collection = getCollection(name);
|
||||
if (collection) {
|
||||
const { inherits } = collection;
|
||||
const children = getChildrenCollections(name);
|
||||
// 搜寻祖先表
|
||||
if (inherits) {
|
||||
for (let index = 0; index < inherits.length; index++) {
|
||||
const collectionKey = inherits[index];
|
||||
if (collectionsInheritChain.includes(collectionKey)) {
|
||||
continue;
|
||||
}
|
||||
collectionsInheritChain.push(collectionKey);
|
||||
getInheritChain(collectionKey);
|
||||
}
|
||||
}
|
||||
// 搜寻后代表
|
||||
if (children) {
|
||||
for (let index = 0; index < children.length; index++) {
|
||||
const collectionKey = children[index].name;
|
||||
if (collectionsInheritChain.includes(collectionKey)) {
|
||||
continue;
|
||||
}
|
||||
collectionsInheritChain.push(collectionKey);
|
||||
getInheritChain(collectionKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
return collectionsInheritChain;
|
||||
};
|
||||
|
||||
return getInheritChain(collectionName);
|
||||
};
|
||||
|
||||
return {
|
||||
service,
|
||||
interfaces,
|
||||
@ -176,12 +249,7 @@ export const useCollectionManager = () => {
|
||||
getCollectionFields,
|
||||
getCollectionFieldsOptions,
|
||||
getCurrentCollectionFields,
|
||||
getCollection(name: any) {
|
||||
if (typeof name !== 'string') {
|
||||
return name;
|
||||
}
|
||||
return collections?.find((collection) => collection.name === name);
|
||||
},
|
||||
getCollection,
|
||||
getCollectionJoinField(name: string) {
|
||||
if (!name) {
|
||||
return;
|
||||
@ -227,5 +295,6 @@ export const useCollectionManager = () => {
|
||||
});
|
||||
});
|
||||
},
|
||||
getAllCollectionsInheritChain,
|
||||
};
|
||||
};
|
||||
|
@ -454,6 +454,7 @@ export default {
|
||||
"Turn pages": "Turn pages",
|
||||
"Others": "Others",
|
||||
"Save as template": "Save as template",
|
||||
"Save as block template": "Save as block template",
|
||||
"Block templates": "Block templates",
|
||||
"Convert reference to duplicate": "Convert reference to duplicate",
|
||||
"Template name": "Template name",
|
||||
|
@ -383,6 +383,7 @@ export default {
|
||||
"Turn pages": "ページをめくる",
|
||||
"Others": "Others",
|
||||
"Save as template": "テンプレートとして保存",
|
||||
"Save as block template": "ブロックテンプレートとして保存",
|
||||
"Block templates": "ブロックテンプレート",
|
||||
"Convert reference to duplicate": "参照を複製に変換",
|
||||
"Template name": "テンプレート名",
|
||||
|
@ -416,6 +416,7 @@ export default {
|
||||
"Turn pages": "Virar páginas",
|
||||
"Others": "Outros",
|
||||
"Save as template": "Salvar como modelo",
|
||||
"Save as block template": "Salvar como modelo de bloco",
|
||||
"Block templates": "Modelos de bloco",
|
||||
"Convert reference to duplicate": "Converter referência em duplicado",
|
||||
"Template name": "Nome do modelo",
|
||||
|
@ -327,6 +327,7 @@ export default {
|
||||
"Turn pages": "Перелистывать страницы",
|
||||
"Others": "Другие",
|
||||
"Save as template": "Сохранить как шаблон",
|
||||
"Save as block template": "Сохранить как шаблон Блока",
|
||||
"Block templates": "Шаблоны Блока",
|
||||
"Convert reference to duplicate": "Преобразовать ссылку в дубликат",
|
||||
"Template name": "Имя Шаблона",
|
||||
|
@ -326,6 +326,7 @@ export default {
|
||||
"Turn pages": "Sayfaları çevir",
|
||||
"Others": "Diğerleri",
|
||||
"Save as template": "Şablon olarak kaydet",
|
||||
"Save as block template": "Blok şablonu olarak kaydet",
|
||||
"Block templates": "Blok şablonları",
|
||||
"Convert reference to duplicate": "Referansı kopyaya dönüştür",
|
||||
"Template name": "Şablon adı",
|
||||
|
@ -490,6 +490,7 @@ export default {
|
||||
"Turn pages": "翻页",
|
||||
"Others": "其他",
|
||||
"Save as template": "保存为模板",
|
||||
"Save as block template": "保存为区块模板",
|
||||
"Block templates": "区块模板",
|
||||
"Convert reference to duplicate": "模板引用转为复制",
|
||||
"Template name": "模板名称",
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useFormBlockContext } from '../../../block-provider';
|
||||
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
|
||||
import { useCollection, useCollectionManager } from '../../../collection-manager';
|
||||
import { useRecord } from '../../../record-provider';
|
||||
|
@ -81,12 +81,12 @@ FormItem.Designer = () => {
|
||||
const { dn, refresh, insertAdjacent } = useDesignable();
|
||||
const compile = useCompile();
|
||||
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||
const targetCollection = getCollection(collectionField.target);
|
||||
const targetCollection = getCollection(collectionField?.target);
|
||||
const interfaceConfig = getInterface(collectionField?.interface);
|
||||
const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema);
|
||||
const originalTitle = collectionField?.uiSchema?.title;
|
||||
const targetFields = collectionField?.target
|
||||
? getCollectionFields(collectionField.target)
|
||||
? getCollectionFields(collectionField?.target)
|
||||
: getCollectionFields(collectionField?.targetCollection) ?? [];
|
||||
const fieldComponentOptions = useFieldComponentOptions();
|
||||
const isSubFormAssociationField = field.address.segments.includes('__form_grid');
|
||||
@ -402,11 +402,11 @@ FormItem.Designer = () => {
|
||||
title: t('Set default value'),
|
||||
properties: {
|
||||
default: {
|
||||
...collectionField.uiSchema,
|
||||
...collectionField?.uiSchema,
|
||||
name: 'default',
|
||||
title: t('Default value'),
|
||||
'x-decorator': 'FormItem',
|
||||
default: fieldSchema.default || collectionField.defaultValue,
|
||||
default: fieldSchema.default || collectionField?.defaultValue,
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
@ -434,7 +434,7 @@ FormItem.Designer = () => {
|
||||
value={fieldSchema['x-component']}
|
||||
onChange={(type) => {
|
||||
const schema: ISchema = {
|
||||
name: collectionField.name,
|
||||
name: collectionField?.name,
|
||||
type: 'void',
|
||||
required: fieldSchema['required'],
|
||||
description: fieldSchema['description'],
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { ArrayItems } from '@formily/antd';
|
||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDetailsBlockContext } from '../../../block-provider/DetailsBlockProvider';
|
||||
import { useCollection } from '../../../collection-manager';
|
||||
import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks';
|
||||
import { useRecord } from '../../../record-provider';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useDesignable } from '../../hooks';
|
||||
@ -16,17 +18,20 @@ export const FormDesigner = () => {
|
||||
const template = useSchemaTemplate();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const record = useRecord();
|
||||
|
||||
return (
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
{/* <SchemaSettings.Template componentName={'FormItem'} collectionName={name} /> */}
|
||||
<SchemaSettings.BlockTitleItem />
|
||||
<SchemaSettings.LinkageRules collectionName={name} />
|
||||
{_.isEmpty(record) ? <SchemaSettings.DataTemplates collectionName={name} /> : null}
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.FormItemTemplate
|
||||
componentName={'FormItem'}
|
||||
collectionName={name}
|
||||
resourceName={defaultResource}
|
||||
/>
|
||||
<SchemaSettings.LinkageRules collectionName={name} />
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.Remove
|
||||
removeParentsIfNoChildren
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { FormLayout } from '@formily/antd';
|
||||
import { createForm, Field, onFormInputChange, onFieldReact, onFieldInit, onFieldChange } from '@formily/core';
|
||||
import { FieldContext, FormContext, observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||
import { Field, createForm, onFieldChange, onFieldInit, onFieldReact, onFormInputChange } from '@formily/core';
|
||||
import { FieldContext, FormContext, RecursionField, observer, useField, useFieldSchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { ConfigProvider, Spin } from 'antd';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useActionContext } from '..';
|
||||
import { useAttach, useComponent } from '../..';
|
||||
import { gridRowColWrap } from '../../../schema-initializer';
|
||||
import { useProps } from '../../hooks/useProps';
|
||||
import { linkageMergeAction } from './utils';
|
||||
|
||||
@ -167,23 +166,26 @@ const WithoutForm = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Form: React.FC<FormProps> & { Designer?: any; FilterDesigner?: any; ReadPrettyDesigner?: any } = observer(
|
||||
(props) => {
|
||||
const field = useField<Field>();
|
||||
const { form, disabled, ...others } = useProps(props);
|
||||
const formDisabled = disabled || field.disabled;
|
||||
return (
|
||||
<ConfigProvider componentDisabled={formDisabled}>
|
||||
<form>
|
||||
<Spin spinning={field.loading || false}>
|
||||
{form ? (
|
||||
<WithForm form={form} {...others} disabled={formDisabled} />
|
||||
) : (
|
||||
<WithoutForm {...others} disabled={formDisabled} />
|
||||
)}
|
||||
</Spin>
|
||||
</form>
|
||||
</ConfigProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
export const Form: React.FC<FormProps> & {
|
||||
Designer?: any;
|
||||
FilterDesigner?: any;
|
||||
ReadPrettyDesigner?: any;
|
||||
Templates?: any;
|
||||
} = observer((props) => {
|
||||
const field = useField<Field>();
|
||||
const { form, disabled, ...others } = useProps(props);
|
||||
const formDisabled = disabled || field.disabled;
|
||||
return (
|
||||
<ConfigProvider componentDisabled={formDisabled}>
|
||||
<form>
|
||||
<Spin spinning={field.loading || false}>
|
||||
{form ? (
|
||||
<WithForm form={form} {...others} disabled={formDisabled} />
|
||||
) : (
|
||||
<WithoutForm {...others} disabled={formDisabled} />
|
||||
)}
|
||||
</Spin>
|
||||
</form>
|
||||
</ConfigProvider>
|
||||
);
|
||||
});
|
||||
|
@ -0,0 +1,107 @@
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { Select } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAPIClient } from '../../../api-client';
|
||||
import { findFormBlock } from '../../../block-provider';
|
||||
|
||||
interface ITemplate {
|
||||
items: {
|
||||
key: string;
|
||||
title: string;
|
||||
collection: string;
|
||||
dataId: number;
|
||||
fields: string[];
|
||||
default?: boolean;
|
||||
}[];
|
||||
/** 是否在 Form 区块显示模板选择器 */
|
||||
display: boolean;
|
||||
}
|
||||
|
||||
const useDataTemplates = () => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { t } = useTranslation();
|
||||
const { items = [], display = true } = findDataTemplates(fieldSchema);
|
||||
const templates: any = [
|
||||
{
|
||||
key: 'none',
|
||||
title: t('None'),
|
||||
},
|
||||
].concat(items.map<any>((t, i) => ({ key: i, ...t })));
|
||||
const defaultTemplate = items.find((item) => item.default);
|
||||
return {
|
||||
templates,
|
||||
display,
|
||||
defaultTemplate,
|
||||
enabled: items.length > 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const Templates = ({ style = {}, form }) => {
|
||||
const { templates, display, enabled, defaultTemplate } = useDataTemplates();
|
||||
const [value, setValue] = React.useState(defaultTemplate?.key || 'none');
|
||||
const api = useAPIClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultTemplate) {
|
||||
fetchTemplateData(api, defaultTemplate).then((data) => {
|
||||
if (form) {
|
||||
form.values = data;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleChange = useCallback(async (value, option) => {
|
||||
setValue(value);
|
||||
if (option.key !== 'none') {
|
||||
if (form) {
|
||||
form.values = await fetchTemplateData(api, option);
|
||||
}
|
||||
} else {
|
||||
form?.reset();
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!enabled || !display) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', backgroundColor: '#f8f8f8', padding: '1em', ...style }}>
|
||||
<label style={{ fontSize: 14, fontWeight: 'bold', whiteSpace: 'nowrap', marginRight: 8 }}>
|
||||
{t('Data template')}:{' '}
|
||||
</label>
|
||||
<Select
|
||||
// style={{ width: '8em' }}
|
||||
options={templates}
|
||||
fieldNames={{ label: 'title', value: 'key' }}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function findDataTemplates(fieldSchema): ITemplate {
|
||||
const formSchema = findFormBlock(fieldSchema);
|
||||
if (formSchema) {
|
||||
return _.cloneDeep(formSchema['x-data-templates']) || {};
|
||||
}
|
||||
return {} as ITemplate;
|
||||
}
|
||||
|
||||
async function fetchTemplateData(api, template: { collection: string; dataId: number; fields: string[] }) {
|
||||
return api
|
||||
.resource(template.collection)
|
||||
.get({
|
||||
filterByTk: template.dataId,
|
||||
fields: template.fields,
|
||||
isTemplate: true,
|
||||
})
|
||||
.then((data) => {
|
||||
return data.data?.data;
|
||||
});
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
import { FilterDesigner } from './Form.FilterDesigner';
|
||||
import { Form as FormV2 } from './Form';
|
||||
import { DetailsDesigner, FormDesigner, ReadPrettyFormDesigner } from './Form.Designer';
|
||||
import { FilterDesigner } from './Form.FilterDesigner';
|
||||
|
||||
FormV2.Designer = FormDesigner;
|
||||
FormV2.FilterDesigner = FilterDesigner;
|
||||
FormV2.ReadPrettyDesigner = ReadPrettyFormDesigner;
|
||||
|
||||
export { FormV2, DetailsDesigner };
|
||||
export * from './FormField';
|
||||
export { FormV2, DetailsDesigner };
|
||||
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { connect, mapProps, mapReadPretty } from '@formily/react';
|
||||
import { SelectProps } from 'antd';
|
||||
import Item from 'antd/lib/list/Item';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { ResourceActionOptions, useRequest } from '../../../api-client';
|
||||
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
|
||||
import { useCompile } from '../../hooks';
|
||||
import { defaultFieldNames, Select } from '../select';
|
||||
import { Select, defaultFieldNames } from '../select';
|
||||
import { ReadPretty } from './ReadPretty';
|
||||
|
||||
export type RemoteSelectProps<P = any> = SelectProps<P, any> & {
|
||||
@ -15,12 +14,22 @@ export type RemoteSelectProps<P = any> = SelectProps<P, any> & {
|
||||
target: string;
|
||||
wait?: number;
|
||||
manual?: boolean;
|
||||
mapOptions?: (data: any) => RemoteSelectProps['fieldNames'];
|
||||
service: ResourceActionOptions<P>;
|
||||
};
|
||||
|
||||
const InternalRemoteSelect = connect(
|
||||
(props: RemoteSelectProps) => {
|
||||
const { fieldNames = {}, service = {}, wait = 300, value, objectValue, manual = true, ...others } = props;
|
||||
const {
|
||||
fieldNames = {},
|
||||
service = {},
|
||||
wait = 300,
|
||||
value,
|
||||
objectValue,
|
||||
manual = true,
|
||||
mapOptions,
|
||||
...others
|
||||
} = props;
|
||||
const compile = useCompile();
|
||||
const firstRun = useRef(false);
|
||||
|
||||
@ -73,6 +82,9 @@ const InternalRemoteSelect = connect(
|
||||
|
||||
const getOptionsByFieldNames = useCallback(
|
||||
(item) => {
|
||||
if (mapOptions) {
|
||||
return mapOptions(item);
|
||||
}
|
||||
return Object.keys(fieldNames).reduce((obj, key) => {
|
||||
const value = item[fieldNames[key]];
|
||||
if (value) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCompile } from '../../schema-component';
|
||||
import { SchemaInitializer } from '../SchemaInitializer';
|
||||
import {
|
||||
gridRowColWrap,
|
||||
@ -10,7 +11,6 @@ import {
|
||||
useFormItemInitializerFields,
|
||||
useInheritsFormItemInitializerFields,
|
||||
} from '../utils';
|
||||
import { useCompile } from '../../schema-component';
|
||||
|
||||
// 表单里配置字段
|
||||
export const FormItemInitializers = (props: any) => {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import { ISchema } from "@formily/react";
|
||||
import { ISchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
|
||||
import { InitializerWithSwitch } from "./InitializerWithSwitch";
|
||||
import { InitializerWithSwitch } from './InitializerWithSwitch';
|
||||
|
||||
export const CollectionFieldInitializer = (props) => {
|
||||
const schema: ISchema = {};
|
||||
return <InitializerWithSwitch {...props} schema={schema} type={'x-collection-field'} />;
|
||||
};
|
||||
const schema: ISchema = {};
|
||||
return <InitializerWithSwitch {...props} schema={schema} type={'x-collection-field'} />;
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { merge } from '@formily/shared';
|
||||
import React from 'react';
|
||||
|
||||
import { SchemaInitializer } from '..';
|
||||
import { useCurrentSchema } from '../utils';
|
||||
|
@ -0,0 +1,178 @@
|
||||
import { connect, mapProps, observer } from '@formily/react';
|
||||
import { Tree as AntdTree } from 'antd';
|
||||
import React from 'react';
|
||||
import { AssociationSelect, SchemaComponent } from '../../schema-component';
|
||||
import { AsDefaultTemplate } from './components/AsDefaultTemplate';
|
||||
import { ArrayCollapse } from './components/DataTemplateTitle';
|
||||
import { useCollectionState } from './hooks/useCollectionState';
|
||||
|
||||
const Tree = connect(
|
||||
AntdTree,
|
||||
mapProps({
|
||||
value: 'checkedKeys',
|
||||
dataSource: 'treeData',
|
||||
onInput: 'onCheck',
|
||||
}),
|
||||
);
|
||||
|
||||
export const FormDataTemplates = observer((props: any) => {
|
||||
const { useProps } = props;
|
||||
const { defaultValues, collectionName } = useProps();
|
||||
const { collectionList, getEnableFieldTree } = useCollectionState(collectionName);
|
||||
|
||||
return (
|
||||
<SchemaComponent
|
||||
components={{ ArrayCollapse }}
|
||||
scope={{ getEnableFieldTree }}
|
||||
schema={{
|
||||
type: 'object',
|
||||
properties: {
|
||||
items: {
|
||||
type: 'array',
|
||||
default: defaultValues?.items,
|
||||
'x-component': 'ArrayCollapse',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': {
|
||||
accordion: true,
|
||||
},
|
||||
items: {
|
||||
type: 'object',
|
||||
'x-component': 'ArrayCollapse.CollapsePanel',
|
||||
'x-component-props': {
|
||||
extra: [<AsDefaultTemplate />],
|
||||
},
|
||||
properties: {
|
||||
layout: {
|
||||
type: 'void',
|
||||
'x-component': 'FormLayout',
|
||||
'x-component-props': {
|
||||
layout: 'vertical',
|
||||
},
|
||||
// TODO: 翻译
|
||||
properties: {
|
||||
collection: {
|
||||
type: 'string',
|
||||
title: '{{ t("Collection") }}',
|
||||
required: true,
|
||||
description: '当前表有继承关系时,可选择继承链路上的表作为模板来源',
|
||||
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: '选择一条已有的数据作为表单的初始化数据',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': AssociationSelect,
|
||||
'x-component-props': {
|
||||
service: {
|
||||
resource: collectionName,
|
||||
},
|
||||
action: 'list',
|
||||
multiple: false,
|
||||
objectValue: false,
|
||||
manual: false,
|
||||
mapOptions: (item) => {
|
||||
// TODO: 应该使用 item.title 字段的值作为 label
|
||||
return {
|
||||
label: item.id,
|
||||
value: item.id,
|
||||
};
|
||||
},
|
||||
fieldNames: {
|
||||
label: 'label',
|
||||
value: 'value',
|
||||
},
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.collection'],
|
||||
fulfill: {
|
||||
state: {
|
||||
disabled: '{{ !$deps[0] }}',
|
||||
componentProps: {
|
||||
service: {
|
||||
resource: '{{ $deps[0] }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: {
|
||||
type: 'array',
|
||||
title: '{{ t("Data fields") }}',
|
||||
required: true,
|
||||
description: '仅选择的字段才会作为表单的初始化数据',
|
||||
'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',
|
||||
},
|
||||
},
|
||||
},
|
||||
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',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
@ -0,0 +1,23 @@
|
||||
import { Tag } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
export const TreeNode = (props) => {
|
||||
const { tag, type } = props;
|
||||
const text = {
|
||||
reference: 'Reference',
|
||||
duplicate: 'Duplicate',
|
||||
preloading: 'Preloading',
|
||||
};
|
||||
const colors = {
|
||||
reference: 'blue',
|
||||
duplicate: 'green',
|
||||
preloading: 'cyan',
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Tag color={colors[type]}>
|
||||
<span>{tag}</span> ({text[type]})
|
||||
</Tag>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
import { ArrayBase } from '@formily/antd';
|
||||
import { Switch } from 'antd';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const AsDefaultTemplate = React.forwardRef((props: any) => {
|
||||
const array = ArrayBase.useArray();
|
||||
const index = ArrayBase.useIndex(props.index);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Switch
|
||||
{...props}
|
||||
checkedChildren={t('Default')}
|
||||
unCheckedChildren={t('Default')}
|
||||
checked={array?.field?.value[index].default}
|
||||
style={{
|
||||
transition: 'all 0.25s ease-in-out',
|
||||
color: 'rgba(0, 0, 0, 0.8)',
|
||||
fontSize: 16,
|
||||
marginLeft: 6,
|
||||
marginBottom: 3,
|
||||
}}
|
||||
onChange={(checked, e) => {
|
||||
e.stopPropagation();
|
||||
array.field.value.splice(index, 1, { ...array?.field?.value[index], default: checked });
|
||||
array.field.value.forEach((item, i) => {
|
||||
if (i !== index) {
|
||||
array.field.value.splice(i, 1, { ...array?.field?.value[i], default: false });
|
||||
}
|
||||
});
|
||||
}}
|
||||
onClick={(checked, e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
@ -0,0 +1,271 @@
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
import { ArrayBase, ArrayBaseMixins } from '@formily/antd';
|
||||
import { ArrayField } from '@formily/core';
|
||||
import { ISchema, RecursionField, observer, useField, useFieldSchema } from '@formily/react';
|
||||
import { toArr, uid } from '@formily/shared';
|
||||
import { Badge, Card, Collapse, CollapsePanelProps, CollapseProps, Empty, Input } from 'antd';
|
||||
import cls from 'classnames';
|
||||
import { clone } from 'lodash';
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const DataTemplateTitle = (props) => {
|
||||
const array = ArrayBase.useArray();
|
||||
const index = ArrayBase.useIndex(props.index);
|
||||
const { t } = useTranslation();
|
||||
const value = array?.field?.value[index];
|
||||
useEffect(() => {
|
||||
if (!value.key) {
|
||||
value.key = uid();
|
||||
}
|
||||
if (!value.title) {
|
||||
value.title = `${t('Template name')} ${array?.field?.value?.length}`;
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<Input.TextArea
|
||||
value={value.title}
|
||||
placeholder={t('Template name')}
|
||||
onChange={(ev) => {
|
||||
ev.stopPropagation();
|
||||
array.field.value.splice(index, 1, { ...value, title: ev.target.value });
|
||||
}}
|
||||
onBlur={(ev) => {
|
||||
ev.stopPropagation();
|
||||
array.field.value.splice(index, 1, { ...value, title: ev.target.value });
|
||||
}}
|
||||
autoSize
|
||||
style={{ width: '70%', border: 'none' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export interface IArrayCollapseProps extends CollapseProps {
|
||||
defaultOpenPanelCount?: number;
|
||||
}
|
||||
type ComposedArrayCollapse = React.FC<React.PropsWithChildren<IArrayCollapseProps>> &
|
||||
ArrayBaseMixins & {
|
||||
CollapsePanel?: React.FC<React.PropsWithChildren<CollapsePanelProps>>;
|
||||
};
|
||||
|
||||
const isAdditionComponent = (schema: ISchema) => {
|
||||
return schema['x-component']?.indexOf?.('Addition') > -1;
|
||||
};
|
||||
|
||||
const isIndexComponent = (schema: ISchema) => {
|
||||
return schema['x-component']?.indexOf?.('Index') > -1;
|
||||
};
|
||||
|
||||
const isRemoveComponent = (schema: ISchema) => {
|
||||
return schema['x-component']?.indexOf?.('Remove') > -1;
|
||||
};
|
||||
|
||||
const isMoveUpComponent = (schema: ISchema) => {
|
||||
return schema['x-component']?.indexOf?.('MoveUp') > -1;
|
||||
};
|
||||
|
||||
const isMoveDownComponent = (schema: ISchema) => {
|
||||
return schema['x-component']?.indexOf?.('MoveDown') > -1;
|
||||
};
|
||||
const isCopyComponent = (schema: ISchema) => {
|
||||
return schema['x-component']?.indexOf?.('Copy') > -1;
|
||||
};
|
||||
const isOperationComponent = (schema: ISchema) => {
|
||||
return (
|
||||
isAdditionComponent(schema) ||
|
||||
isRemoveComponent(schema) ||
|
||||
isMoveDownComponent(schema) ||
|
||||
isMoveUpComponent(schema) ||
|
||||
isCopyComponent(schema)
|
||||
);
|
||||
};
|
||||
|
||||
const range = (count: number) => Array.from({ length: count }).map((_, i) => i);
|
||||
|
||||
const takeDefaultActiveKeys = (dataSourceLength: number, defaultOpenPanelCount: number) => {
|
||||
if (dataSourceLength < defaultOpenPanelCount) return range(dataSourceLength);
|
||||
return range(defaultOpenPanelCount);
|
||||
};
|
||||
|
||||
const insertActiveKeys = (activeKeys: number[], index: number) => {
|
||||
if (activeKeys.length <= index) return activeKeys.concat(index);
|
||||
return activeKeys.reduce((buf, key) => {
|
||||
if (key < index) return buf.concat(key);
|
||||
if (key === index) return buf.concat([key, key + 1]);
|
||||
return buf.concat(key + 1);
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const ArrayCollapse: ComposedArrayCollapse = observer((props: IArrayCollapseProps) => {
|
||||
const field = useField<ArrayField>();
|
||||
const dataSource = Array.isArray(field.value) ? field.value : [];
|
||||
const [activeKeys, setActiveKeys] = useState<number[]>(
|
||||
takeDefaultActiveKeys(dataSource.length, props.defaultOpenPanelCount),
|
||||
);
|
||||
const schema = useFieldSchema();
|
||||
const prefixCls = 'ant-formily-array-collapse';
|
||||
useEffect(() => {
|
||||
if (!field.modified && dataSource.length) {
|
||||
setActiveKeys(takeDefaultActiveKeys(dataSource.length, props.defaultOpenPanelCount));
|
||||
}
|
||||
}, [dataSource.length, field]);
|
||||
if (!schema) throw new Error('can not found schema object');
|
||||
|
||||
const renderAddition = () => {
|
||||
return schema.reduceProperties((addition, schema, key) => {
|
||||
if (isAdditionComponent(schema)) {
|
||||
return <RecursionField schema={schema} name={key} />;
|
||||
}
|
||||
return addition;
|
||||
}, null);
|
||||
};
|
||||
const renderEmpty = () => {
|
||||
if (dataSource.length) return;
|
||||
return (
|
||||
<Card className={cls(`${prefixCls}-item`, props.className)}>
|
||||
<Empty />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const renderItems = () => {
|
||||
return (
|
||||
<Collapse
|
||||
{...props}
|
||||
activeKey={activeKeys}
|
||||
onChange={(keys: string[]) => setActiveKeys(toArr(keys).map(Number))}
|
||||
className={cls(`${prefixCls}-item`, props.className)}
|
||||
>
|
||||
{dataSource.map((item, index) => {
|
||||
const items = Array.isArray(schema.items) ? schema.items[index] || schema.items[0] : schema.items;
|
||||
|
||||
const panelProps = field.query(`${field.address}.${index}`).get('componentProps');
|
||||
const props: CollapsePanelProps = items['x-component-props'];
|
||||
const header = () => {
|
||||
const header = `${panelProps?.header || props.header || field.title}`;
|
||||
const path = field.address.concat(index);
|
||||
const errors = field.form.queryFeedbacks({
|
||||
type: 'error',
|
||||
address: `${path}.**`,
|
||||
});
|
||||
return (
|
||||
<ArrayBase.Item index={index} record={() => field.value?.[index]}>
|
||||
<RecursionField
|
||||
schema={items}
|
||||
name={index}
|
||||
filterProperties={(schema) => {
|
||||
if (!isIndexComponent(schema)) return false;
|
||||
return true;
|
||||
}}
|
||||
onlyRenderProperties
|
||||
/>
|
||||
{errors.length ? (
|
||||
<Badge size="small" className="errors-badge" count={errors.length}>
|
||||
{header}
|
||||
</Badge>
|
||||
) : (
|
||||
<DataTemplateTitle item={item.initialValue || item} index={index} />
|
||||
)}
|
||||
</ArrayBase.Item>
|
||||
);
|
||||
};
|
||||
|
||||
const extra = (
|
||||
<ArrayBase.Item index={index} record={item}>
|
||||
<RecursionField
|
||||
schema={items}
|
||||
name={index}
|
||||
filterProperties={(schema) => {
|
||||
if (!isOperationComponent(schema)) return false;
|
||||
return true;
|
||||
}}
|
||||
onlyRenderProperties
|
||||
/>
|
||||
{panelProps?.extra}
|
||||
</ArrayBase.Item>
|
||||
);
|
||||
|
||||
const content = (
|
||||
<RecursionField
|
||||
schema={items}
|
||||
name={index}
|
||||
filterProperties={(schema) => {
|
||||
if (isIndexComponent(schema)) return false;
|
||||
if (isOperationComponent(schema)) return false;
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Collapse.Panel {...props} {...panelProps} forceRender key={index} header={header()} extra={extra}>
|
||||
<ArrayBase.Item index={index} key={index} record={item}>
|
||||
{content}
|
||||
</ArrayBase.Item>
|
||||
</Collapse.Panel>
|
||||
);
|
||||
})}
|
||||
</Collapse>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<ArrayBase
|
||||
onAdd={(index) => {
|
||||
setActiveKeys(insertActiveKeys(activeKeys, index));
|
||||
}}
|
||||
>
|
||||
{renderEmpty()}
|
||||
{renderItems()}
|
||||
{renderAddition()}
|
||||
</ArrayBase>
|
||||
);
|
||||
});
|
||||
|
||||
const CollapsePanel: React.FC<React.PropsWithChildren<CollapsePanelProps>> = ({ children }) => {
|
||||
return <Fragment>{children}</Fragment>;
|
||||
};
|
||||
|
||||
CollapsePanel.displayName = 'CollapsePanel';
|
||||
|
||||
ArrayCollapse.defaultProps = {
|
||||
defaultOpenPanelCount: 5,
|
||||
};
|
||||
ArrayCollapse.displayName = 'ArrayCollapse';
|
||||
ArrayCollapse.CollapsePanel = CollapsePanel;
|
||||
|
||||
ArrayBase.mixin(ArrayCollapse);
|
||||
|
||||
export default ArrayCollapse;
|
||||
|
||||
//@ts-ignore
|
||||
ArrayCollapse.Copy = React.forwardRef((props: any, ref) => {
|
||||
const self = useField();
|
||||
const array = ArrayBase.useArray();
|
||||
const index = ArrayBase.useIndex(props.index);
|
||||
if (!array) return null;
|
||||
if (array.field?.pattern !== 'editable') return null;
|
||||
return (
|
||||
<CopyOutlined
|
||||
{...props}
|
||||
style={{
|
||||
transition: 'all 0.25s ease-in-out',
|
||||
color: 'rgba(0, 0, 0, 0.8)',
|
||||
fontSize: '16px',
|
||||
marginLeft: 6,
|
||||
}}
|
||||
ref={ref}
|
||||
onClick={(e) => {
|
||||
if (self?.disabled) return;
|
||||
e.stopPropagation();
|
||||
if (array.props?.disabled) return;
|
||||
const value = clone(array?.field?.value[index]);
|
||||
array.field.push(value);
|
||||
if (props.onClick) {
|
||||
props.onClick(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
@ -0,0 +1,116 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useCollectionManager } from '../../../collection-manager';
|
||||
import { useCompile } from '../../../schema-component';
|
||||
import { TreeNode } from '../TreeLabel';
|
||||
|
||||
export const useCollectionState = (currentCollectionName: string) => {
|
||||
const { getCollectionFields, getAllCollectionsInheritChain, getCollection, getCollectionFieldsOptions } =
|
||||
useCollectionManager();
|
||||
const [collectionList] = useState(getCollectionList);
|
||||
const compile = useCompile();
|
||||
|
||||
function getCollectionList() {
|
||||
const collections = getAllCollectionsInheritChain(currentCollectionName);
|
||||
return collections.map((name) => ({ label: getCollection(name).title, value: name }));
|
||||
}
|
||||
|
||||
const getEnableFieldTree = (collectionName: string) => {
|
||||
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;
|
||||
}
|
||||
const node = {
|
||||
type: 'duplicate',
|
||||
tag: compile(field.uiSchema?.title) || field.name,
|
||||
};
|
||||
const option = {
|
||||
title: React.createElement(TreeNode, node),
|
||||
key: prefix ? `${prefix}.${field.name}` : field.name,
|
||||
};
|
||||
// 多对多的只展示关系字段
|
||||
if (['belongsTo', 'belongsToMany'].includes(field.type)) {
|
||||
option['type'] = 'reference';
|
||||
option['label'] = 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 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
collectionList,
|
||||
getEnableFieldTree,
|
||||
};
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './FormDataTemplates';
|
@ -1,9 +1,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { ArrayItems } from '@formily/antd';
|
||||
import { FormDialog, FormItem, FormLayout, Input, ArrayCollapse } from '@formily/antd';
|
||||
import { createForm, Field, GeneralField } from '@formily/core';
|
||||
import { ArrayCollapse, ArrayItems, FormDialog, FormItem, FormLayout, Input } from '@formily/antd';
|
||||
import { Field, GeneralField, createForm } from '@formily/core';
|
||||
import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema, useForm } from '@formily/react';
|
||||
import _ from 'lodash';
|
||||
import { uid } from '@formily/shared';
|
||||
import {
|
||||
Alert,
|
||||
@ -20,35 +18,37 @@ import {
|
||||
Switch,
|
||||
} from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import _, { cloneDeep } from 'lodash';
|
||||
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
APIClientProvider,
|
||||
ActionContext,
|
||||
CollectionManagerContext,
|
||||
createDesignable,
|
||||
Designable,
|
||||
FormProvider,
|
||||
RemoteSchemaComponent,
|
||||
SchemaComponent,
|
||||
SchemaComponentOptions,
|
||||
useActionContext,
|
||||
createDesignable,
|
||||
findFormBlock,
|
||||
useAPIClient,
|
||||
useCollection,
|
||||
useCollectionFilterOptions,
|
||||
useCollectionManager,
|
||||
useCompile,
|
||||
useDesignable,
|
||||
useCollectionFilterOptions,
|
||||
} from '..';
|
||||
import { findFilterTargets, updateFilterTargets } from '../block-provider/hooks';
|
||||
import { FilterBlockType, isSameCollection, useSupportedBlocks } from '../filter-provider/utils';
|
||||
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 { FilterBlockType, isSameCollection, useSupportedBlocks } from '../filter-provider/utils';
|
||||
import { findFilterTargets, updateFilterTargets } from '../block-provider/hooks';
|
||||
import { EnableChildCollections } from './EnableChildCollections';
|
||||
import { getTargetKey } from '../schema-component/antd/association-filter/utilts';
|
||||
|
||||
interface SchemaSettingsProps {
|
||||
title?: any;
|
||||
@ -376,7 +376,7 @@ SchemaSettings.FormItemTemplate = (props) => {
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Save as template')}
|
||||
{t('Save as block template')}
|
||||
</SchemaSettings.Item>
|
||||
);
|
||||
};
|
||||
@ -780,6 +780,7 @@ SchemaSettings.ModalItem = (props) => {
|
||||
} = props;
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
const cm = useContext(CollectionManagerContext);
|
||||
const apiClient = useAPIClient();
|
||||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
@ -793,7 +794,9 @@ SchemaSettings.ModalItem = (props) => {
|
||||
<CollectionManagerContext.Provider value={cm}>
|
||||
<SchemaComponentOptions scope={options.scope} components={options.components}>
|
||||
<FormLayout layout={'vertical'} style={{ minWidth: 520 }}>
|
||||
<SchemaComponent components={components} scope={scope} schema={schema} />
|
||||
<APIClientProvider apiClient={apiClient}>
|
||||
<SchemaComponent components={components} scope={scope} schema={schema} />
|
||||
</APIClientProvider>
|
||||
</FormLayout>
|
||||
</SchemaComponentOptions>
|
||||
</CollectionManagerContext.Provider>
|
||||
@ -998,6 +1001,62 @@ SchemaSettings.LinkageRules = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const useDataTemplates = () => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const formSchema = findFormBlock(fieldSchema) || fieldSchema;
|
||||
return {
|
||||
templateData: _.cloneDeep(formSchema?.['x-data-templates']),
|
||||
};
|
||||
};
|
||||
|
||||
SchemaSettings.DataTemplates = (props) => {
|
||||
const { collectionName } = props;
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
const formSchema = findFormBlock(fieldSchema) || fieldSchema;
|
||||
const { templateData } = useDataTemplates();
|
||||
|
||||
return (
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Form data templates')}
|
||||
components={{ ArrayCollapse, FormLayout }}
|
||||
width={770}
|
||||
schema={
|
||||
{
|
||||
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();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
SchemaSettings.EnableChildCollections = (props) => {
|
||||
const { collectionName } = props;
|
||||
const fieldSchema = useFieldSchema();
|
||||
|
@ -5,6 +5,7 @@ import i18next from 'i18next';
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import Application, { ApplicationOptions } from './application';
|
||||
import { parseVariables } from './middlewares';
|
||||
import { dateTemplate } from './middlewares/data-template';
|
||||
import { dataWrapping } from './middlewares/data-wrapping';
|
||||
import { db2resource } from './middlewares/db2resource';
|
||||
import { i18n } from './middlewares/i18n';
|
||||
@ -72,7 +73,9 @@ export function registerMiddlewares(app: Application, options: ApplicationOption
|
||||
}
|
||||
|
||||
app.resourcer.use(parseVariables, { tag: 'parseVariables', after: 'acl' });
|
||||
app.resourcer.use(dateTemplate, { tag: 'dateTemplate', after: 'acl' });
|
||||
|
||||
app.use(db2resource, { tag: 'db2resource', after: 'dataWrapping' });
|
||||
app.use(app.resourcer.restApiMiddleware(), { tag: 'restApi', after: 'db2resource' });
|
||||
|
||||
}
|
||||
|
76
packages/core/server/src/middlewares/data-template.ts
Normal file
76
packages/core/server/src/middlewares/data-template.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { Context } from '@nocobase/actions';
|
||||
import { Collection } from '@nocobase/database';
|
||||
|
||||
export const dateTemplate = async (ctx: Context, next) => {
|
||||
const { resourceName, actionName } = ctx.action;
|
||||
const { isTemplate, fields } = ctx.action.params;
|
||||
|
||||
await next();
|
||||
|
||||
if (isTemplate && actionName === 'get' && fields.length > 0) {
|
||||
ctx.body = traverseJSON(ctx.body?.toJSON(), ctx.db.getCollection(resourceName));
|
||||
}
|
||||
};
|
||||
|
||||
const traverseHasMany = (arr: any[], collection: Collection, exclude = []) => {
|
||||
if (!arr) {
|
||||
return arr;
|
||||
}
|
||||
return arr.map((item) => traverseJSON(item, collection, exclude));
|
||||
};
|
||||
|
||||
const traverseBelongsToMany = (arr: any[], collection: Collection, exclude = [], through) => {
|
||||
if (!arr) {
|
||||
return arr;
|
||||
}
|
||||
const throughCollection = collection.db.getCollection(through);
|
||||
return arr.map((item) => {
|
||||
const data = traverseJSON(item[through], throughCollection, exclude);
|
||||
if (Object.keys(data).length) {
|
||||
item[through] = data;
|
||||
} else {
|
||||
delete item[through];
|
||||
}
|
||||
return item;
|
||||
});
|
||||
};
|
||||
|
||||
const traverseJSON = (data, collection: Collection, exclude = []) => {
|
||||
const result = {};
|
||||
for (const key of Object.keys(data)) {
|
||||
if (exclude.includes(key)) {
|
||||
continue;
|
||||
}
|
||||
if (['createdAt', 'updatedAt', 'createdBy', 'createdById', 'updatedById', 'updatedBy'].includes(key)) {
|
||||
continue;
|
||||
}
|
||||
const field = collection.getField(key);
|
||||
if (!field) {
|
||||
result[key] = data[key];
|
||||
continue;
|
||||
}
|
||||
if (field.options.primaryKey) {
|
||||
continue;
|
||||
}
|
||||
if (field.type === 'sort') {
|
||||
continue;
|
||||
}
|
||||
if (field.type === 'hasOne') {
|
||||
result[key] = traverseJSON(data[key], collection.db.getCollection(field.target), [field.foreignKey]);
|
||||
} else if (field.type === 'hasMany') {
|
||||
result[key] = traverseHasMany(data[key], collection.db.getCollection(field.target), [field.foreignKey]);
|
||||
} else if (field.type === 'belongsTo') {
|
||||
result[key] = data[key];
|
||||
} else if (field.type === 'belongsToMany') {
|
||||
result[key] = traverseBelongsToMany(
|
||||
data[key],
|
||||
collection.db.getCollection(field.target),
|
||||
[field.foreignKey, field.otherKey],
|
||||
field.through,
|
||||
);
|
||||
} else {
|
||||
result[key] = data[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
@ -400,6 +400,7 @@
|
||||
"Turn pages": "Turn pages",
|
||||
"Others": "Others",
|
||||
"Save as template": "Save as template",
|
||||
"Save as block template": "Save as block template",
|
||||
"Block templates": "Block templates",
|
||||
"Convert reference to duplicate": "Convert reference to duplicate",
|
||||
"Template name": "Template name",
|
||||
|
@ -384,6 +384,7 @@
|
||||
"Turn pages": "Virar páginas",
|
||||
"Others": "Outros",
|
||||
"Save as template": "Salvar como modelo",
|
||||
"Save as block template": "Salvar como modelo de bloco",
|
||||
"Block templates": "Modelos de bloco",
|
||||
"Convert reference to duplicate": "Converter referência em duplicado",
|
||||
"Template name": "Nome do modelo",
|
||||
@ -1377,4 +1378,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -400,6 +400,7 @@
|
||||
"Turn pages": "翻页",
|
||||
"Others": "其他",
|
||||
"Save as template": "保存为模板",
|
||||
"Save as block template": "保存为区块模板",
|
||||
"Block templates": "区块模板",
|
||||
"Convert reference to duplicate": "模板引用转为复制",
|
||||
"Template name": "模板名称",
|
||||
|
Loading…
Reference in New Issue
Block a user