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 { createForm } from '@formily/core';
|
||||||
import { useField } from '@formily/react';
|
import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
|
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useCollection } from '../collection-manager';
|
import { useCollection } from '../collection-manager';
|
||||||
import { RecordProvider, useRecord } from '../record-provider';
|
import { RecordProvider, useRecord } from '../record-provider';
|
||||||
import { useActionContext, useDesignable } from '../schema-component';
|
import { useActionContext, useDesignable } from '../schema-component';
|
||||||
|
import { Templates as DataTemplateSelect } from '../schema-component/antd/form-v2/Templates';
|
||||||
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
||||||
|
|
||||||
export const FormBlockContext = createContext<any>({});
|
export const FormBlockContext = createContext<any>({});
|
||||||
@ -40,10 +41,14 @@ const InternalFormBlockProvider = (props) => {
|
|||||||
>
|
>
|
||||||
{readPretty ? (
|
{readPretty ? (
|
||||||
<RecordProvider parent={isEmpty(record?.__parent) ? record : record?.__parent} record={service?.data?.data}>
|
<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>
|
</RecordProvider>
|
||||||
) : (
|
) : (
|
||||||
<div ref={formBlockRef}>{props.children}</div>
|
<div ref={formBlockRef}>
|
||||||
|
<RenderChildrenWithDataTemplates form={form} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</FormBlockContext.Provider>
|
</FormBlockContext.Provider>
|
||||||
);
|
);
|
||||||
@ -108,3 +113,27 @@ export const useFormBlockProps = () => {
|
|||||||
form: ctx.form,
|
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: {
|
delete: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{ t("Delete") }}',
|
title: '{{ t("Delete") }}',
|
||||||
'x-visible': '{{cm.useDeleteButtonVisible()}}',
|
'x-disabled': '{{cm.useDeleteButtonDisabled()}}',
|
||||||
'x-component': 'Action.Link',
|
'x-component': 'Action.Link',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
confirm: {
|
confirm: {
|
||||||
|
@ -3,9 +3,9 @@ import { message } from 'antd';
|
|||||||
import omit from 'lodash/omit';
|
import omit from 'lodash/omit';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useCollection, useCollectionManager } from '.';
|
import { useCollection, useCollectionManager } from '.';
|
||||||
|
import { useCompile } from '..';
|
||||||
import { useRequest } from '../api-client';
|
import { useRequest } from '../api-client';
|
||||||
import { useRecord } from '../record-provider';
|
import { useRecord } from '../record-provider';
|
||||||
import { useCompile } from '..';
|
|
||||||
import { useActionContext } from '../schema-component';
|
import { useActionContext } from '../schema-component';
|
||||||
import { useFilterFieldOptions, useFilterFieldProps } from '../schema-component/antd/filter/useFilterActionProps';
|
import { useFilterFieldOptions, useFilterFieldProps } from '../schema-component/antd/filter/useFilterActionProps';
|
||||||
import { useResourceActionContext, useResourceContext } from './ResourceActionProvider';
|
import { useResourceActionContext, useResourceContext } from './ResourceActionProvider';
|
||||||
@ -377,10 +377,10 @@ export const useDestroyActionAndRefreshCM = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDeleteButtonVisible = () => {
|
export const useDeleteButtonDisabled = () => {
|
||||||
const { interface: i, deletable = true } = useRecord();
|
const { interface: i, deletable = true } = useRecord();
|
||||||
|
|
||||||
return deletable && i !== 'id';
|
return !deletable || i === 'id';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useBulkDestroyActionAndRefreshCM = () => {
|
export const useBulkDestroyActionAndRefreshCM = () => {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { clone } from '@formily/shared';
|
import { clone } from '@formily/shared';
|
||||||
import { CascaderProps } from 'antd';
|
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 { useContext } from 'react';
|
||||||
import { useCompile } from '../../schema-component';
|
import { useCompile } from '../../schema-component';
|
||||||
import { CollectionManagerContext } from '../context';
|
import { CollectionManagerContext } from '../context';
|
||||||
@ -108,9 +107,31 @@ export const useCollectionManager = () => {
|
|||||||
* Max depth of recursion
|
* Max depth of recursion
|
||||||
*/
|
*/
|
||||||
maxDepth?: number;
|
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) {
|
if (collectionNames.length - 1 > maxDepth) {
|
||||||
return;
|
return;
|
||||||
@ -129,14 +150,16 @@ export const useCollectionManager = () => {
|
|||||||
?.filter(
|
?.filter(
|
||||||
(field) =>
|
(field) =>
|
||||||
field.interface &&
|
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 && field.target && field.target !== collectionName && Array.isArray(association)
|
||||||
? association.includes(field.interface)
|
? association.includes(field.interface)
|
||||||
: false)),
|
: false)),
|
||||||
)
|
)
|
||||||
?.map((field) => {
|
?.map((field) => {
|
||||||
const result: CascaderProps<any>['options'][0] = {
|
const result: CascaderProps<any>['options'][0] = {
|
||||||
value: field.name,
|
value: usePrefix && prefixFieldValue ? `${prefixFieldValue}.${field.name}` : field.name,
|
||||||
label: compile(field?.uiSchema?.title) || field.name,
|
label: compile(field?.uiSchema?.title) || field.name,
|
||||||
...field,
|
...field,
|
||||||
};
|
};
|
||||||
@ -147,6 +170,12 @@ export const useCollectionManager = () => {
|
|||||||
...opts,
|
...opts,
|
||||||
cached,
|
cached,
|
||||||
collectionNames: [...collectionNames, field.target],
|
collectionNames: [...collectionNames, field.target],
|
||||||
|
prefixFieldValue: usePrefix
|
||||||
|
? prefixFieldValue
|
||||||
|
? `${prefixFieldValue}.${field.name}`
|
||||||
|
: field.name
|
||||||
|
: '',
|
||||||
|
usePrefix,
|
||||||
});
|
});
|
||||||
if (!result.children?.length) {
|
if (!result.children?.length) {
|
||||||
return null;
|
return null;
|
||||||
@ -161,6 +190,50 @@ export const useCollectionManager = () => {
|
|||||||
return options;
|
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 {
|
return {
|
||||||
service,
|
service,
|
||||||
interfaces,
|
interfaces,
|
||||||
@ -176,12 +249,7 @@ export const useCollectionManager = () => {
|
|||||||
getCollectionFields,
|
getCollectionFields,
|
||||||
getCollectionFieldsOptions,
|
getCollectionFieldsOptions,
|
||||||
getCurrentCollectionFields,
|
getCurrentCollectionFields,
|
||||||
getCollection(name: any) {
|
getCollection,
|
||||||
if (typeof name !== 'string') {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
return collections?.find((collection) => collection.name === name);
|
|
||||||
},
|
|
||||||
getCollectionJoinField(name: string) {
|
getCollectionJoinField(name: string) {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return;
|
return;
|
||||||
@ -227,5 +295,6 @@ export const useCollectionManager = () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
getAllCollectionsInheritChain,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -454,6 +454,7 @@ export default {
|
|||||||
"Turn pages": "Turn pages",
|
"Turn pages": "Turn pages",
|
||||||
"Others": "Others",
|
"Others": "Others",
|
||||||
"Save as template": "Save as template",
|
"Save as template": "Save as template",
|
||||||
|
"Save as block template": "Save as block template",
|
||||||
"Block templates": "Block templates",
|
"Block templates": "Block templates",
|
||||||
"Convert reference to duplicate": "Convert reference to duplicate",
|
"Convert reference to duplicate": "Convert reference to duplicate",
|
||||||
"Template name": "Template name",
|
"Template name": "Template name",
|
||||||
|
@ -383,6 +383,7 @@ export default {
|
|||||||
"Turn pages": "ページをめくる",
|
"Turn pages": "ページをめくる",
|
||||||
"Others": "Others",
|
"Others": "Others",
|
||||||
"Save as template": "テンプレートとして保存",
|
"Save as template": "テンプレートとして保存",
|
||||||
|
"Save as block template": "ブロックテンプレートとして保存",
|
||||||
"Block templates": "ブロックテンプレート",
|
"Block templates": "ブロックテンプレート",
|
||||||
"Convert reference to duplicate": "参照を複製に変換",
|
"Convert reference to duplicate": "参照を複製に変換",
|
||||||
"Template name": "テンプレート名",
|
"Template name": "テンプレート名",
|
||||||
|
@ -416,6 +416,7 @@ export default {
|
|||||||
"Turn pages": "Virar páginas",
|
"Turn pages": "Virar páginas",
|
||||||
"Others": "Outros",
|
"Others": "Outros",
|
||||||
"Save as template": "Salvar como modelo",
|
"Save as template": "Salvar como modelo",
|
||||||
|
"Save as block template": "Salvar como modelo de bloco",
|
||||||
"Block templates": "Modelos de bloco",
|
"Block templates": "Modelos de bloco",
|
||||||
"Convert reference to duplicate": "Converter referência em duplicado",
|
"Convert reference to duplicate": "Converter referência em duplicado",
|
||||||
"Template name": "Nome do modelo",
|
"Template name": "Nome do modelo",
|
||||||
|
@ -327,6 +327,7 @@ export default {
|
|||||||
"Turn pages": "Перелистывать страницы",
|
"Turn pages": "Перелистывать страницы",
|
||||||
"Others": "Другие",
|
"Others": "Другие",
|
||||||
"Save as template": "Сохранить как шаблон",
|
"Save as template": "Сохранить как шаблон",
|
||||||
|
"Save as block template": "Сохранить как шаблон Блока",
|
||||||
"Block templates": "Шаблоны Блока",
|
"Block templates": "Шаблоны Блока",
|
||||||
"Convert reference to duplicate": "Преобразовать ссылку в дубликат",
|
"Convert reference to duplicate": "Преобразовать ссылку в дубликат",
|
||||||
"Template name": "Имя Шаблона",
|
"Template name": "Имя Шаблона",
|
||||||
|
@ -326,6 +326,7 @@ export default {
|
|||||||
"Turn pages": "Sayfaları çevir",
|
"Turn pages": "Sayfaları çevir",
|
||||||
"Others": "Diğerleri",
|
"Others": "Diğerleri",
|
||||||
"Save as template": "Şablon olarak kaydet",
|
"Save as template": "Şablon olarak kaydet",
|
||||||
|
"Save as block template": "Blok şablonu olarak kaydet",
|
||||||
"Block templates": "Blok şablonları",
|
"Block templates": "Blok şablonları",
|
||||||
"Convert reference to duplicate": "Referansı kopyaya dönüştür",
|
"Convert reference to duplicate": "Referansı kopyaya dönüştür",
|
||||||
"Template name": "Şablon adı",
|
"Template name": "Şablon adı",
|
||||||
|
@ -490,6 +490,7 @@ export default {
|
|||||||
"Turn pages": "翻页",
|
"Turn pages": "翻页",
|
||||||
"Others": "其他",
|
"Others": "其他",
|
||||||
"Save as template": "保存为模板",
|
"Save as template": "保存为模板",
|
||||||
|
"Save as block template": "保存为区块模板",
|
||||||
"Block templates": "区块模板",
|
"Block templates": "区块模板",
|
||||||
"Convert reference to duplicate": "模板引用转为复制",
|
"Convert reference to duplicate": "模板引用转为复制",
|
||||||
"Template name": "模板名称",
|
"Template name": "模板名称",
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { useFieldSchema } from '@formily/react';
|
import { useFieldSchema } from '@formily/react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useFormBlockContext } from '../../../block-provider';
|
|
||||||
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
|
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
|
||||||
import { useCollection, useCollectionManager } from '../../../collection-manager';
|
import { useCollection, useCollectionManager } from '../../../collection-manager';
|
||||||
import { useRecord } from '../../../record-provider';
|
import { useRecord } from '../../../record-provider';
|
||||||
|
@ -81,12 +81,12 @@ FormItem.Designer = () => {
|
|||||||
const { dn, refresh, insertAdjacent } = useDesignable();
|
const { dn, refresh, insertAdjacent } = useDesignable();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
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 interfaceConfig = getInterface(collectionField?.interface);
|
||||||
const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema);
|
const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema);
|
||||||
const originalTitle = collectionField?.uiSchema?.title;
|
const originalTitle = collectionField?.uiSchema?.title;
|
||||||
const targetFields = collectionField?.target
|
const targetFields = collectionField?.target
|
||||||
? getCollectionFields(collectionField.target)
|
? getCollectionFields(collectionField?.target)
|
||||||
: getCollectionFields(collectionField?.targetCollection) ?? [];
|
: getCollectionFields(collectionField?.targetCollection) ?? [];
|
||||||
const fieldComponentOptions = useFieldComponentOptions();
|
const fieldComponentOptions = useFieldComponentOptions();
|
||||||
const isSubFormAssociationField = field.address.segments.includes('__form_grid');
|
const isSubFormAssociationField = field.address.segments.includes('__form_grid');
|
||||||
@ -402,11 +402,11 @@ FormItem.Designer = () => {
|
|||||||
title: t('Set default value'),
|
title: t('Set default value'),
|
||||||
properties: {
|
properties: {
|
||||||
default: {
|
default: {
|
||||||
...collectionField.uiSchema,
|
...collectionField?.uiSchema,
|
||||||
name: 'default',
|
name: 'default',
|
||||||
title: t('Default value'),
|
title: t('Default value'),
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
default: fieldSchema.default || collectionField.defaultValue,
|
default: fieldSchema.default || collectionField?.defaultValue,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as ISchema
|
} as ISchema
|
||||||
@ -434,7 +434,7 @@ FormItem.Designer = () => {
|
|||||||
value={fieldSchema['x-component']}
|
value={fieldSchema['x-component']}
|
||||||
onChange={(type) => {
|
onChange={(type) => {
|
||||||
const schema: ISchema = {
|
const schema: ISchema = {
|
||||||
name: collectionField.name,
|
name: collectionField?.name,
|
||||||
type: 'void',
|
type: 'void',
|
||||||
required: fieldSchema['required'],
|
required: fieldSchema['required'],
|
||||||
description: fieldSchema['description'],
|
description: fieldSchema['description'],
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { ArrayItems } from '@formily/antd';
|
import { ArrayItems } from '@formily/antd';
|
||||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import _ from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDetailsBlockContext } from '../../../block-provider/DetailsBlockProvider';
|
import { useDetailsBlockContext } from '../../../block-provider/DetailsBlockProvider';
|
||||||
import { useCollection } from '../../../collection-manager';
|
import { useCollection } from '../../../collection-manager';
|
||||||
import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks';
|
import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks';
|
||||||
|
import { useRecord } from '../../../record-provider';
|
||||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||||
import { useSchemaTemplate } from '../../../schema-templates';
|
import { useSchemaTemplate } from '../../../schema-templates';
|
||||||
import { useDesignable } from '../../hooks';
|
import { useDesignable } from '../../hooks';
|
||||||
@ -16,17 +18,20 @@ export const FormDesigner = () => {
|
|||||||
const template = useSchemaTemplate();
|
const template = useSchemaTemplate();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||||
|
const record = useRecord();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||||
{/* <SchemaSettings.Template componentName={'FormItem'} collectionName={name} /> */}
|
{/* <SchemaSettings.Template componentName={'FormItem'} collectionName={name} /> */}
|
||||||
<SchemaSettings.BlockTitleItem />
|
<SchemaSettings.BlockTitleItem />
|
||||||
|
<SchemaSettings.LinkageRules collectionName={name} />
|
||||||
|
{_.isEmpty(record) ? <SchemaSettings.DataTemplates collectionName={name} /> : null}
|
||||||
|
<SchemaSettings.Divider />
|
||||||
<SchemaSettings.FormItemTemplate
|
<SchemaSettings.FormItemTemplate
|
||||||
componentName={'FormItem'}
|
componentName={'FormItem'}
|
||||||
collectionName={name}
|
collectionName={name}
|
||||||
resourceName={defaultResource}
|
resourceName={defaultResource}
|
||||||
/>
|
/>
|
||||||
<SchemaSettings.LinkageRules collectionName={name} />
|
|
||||||
<SchemaSettings.Divider />
|
<SchemaSettings.Divider />
|
||||||
<SchemaSettings.Remove
|
<SchemaSettings.Remove
|
||||||
removeParentsIfNoChildren
|
removeParentsIfNoChildren
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { FormLayout } from '@formily/antd';
|
import { FormLayout } from '@formily/antd';
|
||||||
import { createForm, Field, onFormInputChange, onFieldReact, onFieldInit, onFieldChange } from '@formily/core';
|
import { Field, createForm, onFieldChange, onFieldInit, onFieldReact, onFormInputChange } from '@formily/core';
|
||||||
import { FieldContext, FormContext, observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
import { FieldContext, FormContext, RecursionField, observer, useField, useFieldSchema } from '@formily/react';
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { ConfigProvider, Spin } from 'antd';
|
import { ConfigProvider, Spin } from 'antd';
|
||||||
import React, { useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { useActionContext } from '..';
|
import { useActionContext } from '..';
|
||||||
import { useAttach, useComponent } from '../..';
|
import { useAttach, useComponent } from '../..';
|
||||||
import { gridRowColWrap } from '../../../schema-initializer';
|
|
||||||
import { useProps } from '../../hooks/useProps';
|
import { useProps } from '../../hooks/useProps';
|
||||||
import { linkageMergeAction } from './utils';
|
import { linkageMergeAction } from './utils';
|
||||||
|
|
||||||
@ -167,8 +166,12 @@ const WithoutForm = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Form: React.FC<FormProps> & { Designer?: any; FilterDesigner?: any; ReadPrettyDesigner?: any } = observer(
|
export const Form: React.FC<FormProps> & {
|
||||||
(props) => {
|
Designer?: any;
|
||||||
|
FilterDesigner?: any;
|
||||||
|
ReadPrettyDesigner?: any;
|
||||||
|
Templates?: any;
|
||||||
|
} = observer((props) => {
|
||||||
const field = useField<Field>();
|
const field = useField<Field>();
|
||||||
const { form, disabled, ...others } = useProps(props);
|
const { form, disabled, ...others } = useProps(props);
|
||||||
const formDisabled = disabled || field.disabled;
|
const formDisabled = disabled || field.disabled;
|
||||||
@ -185,5 +188,4 @@ export const Form: React.FC<FormProps> & { Designer?: any; FilterDesigner?: any;
|
|||||||
</form>
|
</form>
|
||||||
</ConfigProvider>
|
</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 { Form as FormV2 } from './Form';
|
||||||
import { DetailsDesigner, FormDesigner, ReadPrettyFormDesigner } from './Form.Designer';
|
import { DetailsDesigner, FormDesigner, ReadPrettyFormDesigner } from './Form.Designer';
|
||||||
|
import { FilterDesigner } from './Form.FilterDesigner';
|
||||||
|
|
||||||
FormV2.Designer = FormDesigner;
|
FormV2.Designer = FormDesigner;
|
||||||
FormV2.FilterDesigner = FilterDesigner;
|
FormV2.FilterDesigner = FilterDesigner;
|
||||||
FormV2.ReadPrettyDesigner = ReadPrettyFormDesigner;
|
FormV2.ReadPrettyDesigner = ReadPrettyFormDesigner;
|
||||||
|
|
||||||
export { FormV2, DetailsDesigner };
|
|
||||||
export * from './FormField';
|
export * from './FormField';
|
||||||
|
export { FormV2, DetailsDesigner };
|
||||||
|
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { LoadingOutlined } from '@ant-design/icons';
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
import { connect, mapProps, mapReadPretty } from '@formily/react';
|
import { connect, mapProps, mapReadPretty } from '@formily/react';
|
||||||
import { SelectProps } from 'antd';
|
import { SelectProps } from 'antd';
|
||||||
import Item from 'antd/lib/list/Item';
|
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { ResourceActionOptions, useRequest } from '../../../api-client';
|
import { ResourceActionOptions, useRequest } from '../../../api-client';
|
||||||
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
|
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
|
||||||
import { useCompile } from '../../hooks';
|
import { useCompile } from '../../hooks';
|
||||||
import { defaultFieldNames, Select } from '../select';
|
import { Select, defaultFieldNames } from '../select';
|
||||||
import { ReadPretty } from './ReadPretty';
|
import { ReadPretty } from './ReadPretty';
|
||||||
|
|
||||||
export type RemoteSelectProps<P = any> = SelectProps<P, any> & {
|
export type RemoteSelectProps<P = any> = SelectProps<P, any> & {
|
||||||
@ -15,12 +14,22 @@ export type RemoteSelectProps<P = any> = SelectProps<P, any> & {
|
|||||||
target: string;
|
target: string;
|
||||||
wait?: number;
|
wait?: number;
|
||||||
manual?: boolean;
|
manual?: boolean;
|
||||||
|
mapOptions?: (data: any) => RemoteSelectProps['fieldNames'];
|
||||||
service: ResourceActionOptions<P>;
|
service: ResourceActionOptions<P>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const InternalRemoteSelect = connect(
|
const InternalRemoteSelect = connect(
|
||||||
(props: RemoteSelectProps) => {
|
(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 compile = useCompile();
|
||||||
const firstRun = useRef(false);
|
const firstRun = useRef(false);
|
||||||
|
|
||||||
@ -73,6 +82,9 @@ const InternalRemoteSelect = connect(
|
|||||||
|
|
||||||
const getOptionsByFieldNames = useCallback(
|
const getOptionsByFieldNames = useCallback(
|
||||||
(item) => {
|
(item) => {
|
||||||
|
if (mapOptions) {
|
||||||
|
return mapOptions(item);
|
||||||
|
}
|
||||||
return Object.keys(fieldNames).reduce((obj, key) => {
|
return Object.keys(fieldNames).reduce((obj, key) => {
|
||||||
const value = item[fieldNames[key]];
|
const value = item[fieldNames[key]];
|
||||||
if (value) {
|
if (value) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useCompile } from '../../schema-component';
|
||||||
import { SchemaInitializer } from '../SchemaInitializer';
|
import { SchemaInitializer } from '../SchemaInitializer';
|
||||||
import {
|
import {
|
||||||
gridRowColWrap,
|
gridRowColWrap,
|
||||||
@ -10,7 +11,6 @@ import {
|
|||||||
useFormItemInitializerFields,
|
useFormItemInitializerFields,
|
||||||
useInheritsFormItemInitializerFields,
|
useInheritsFormItemInitializerFields,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { useCompile } from '../../schema-component';
|
|
||||||
|
|
||||||
// 表单里配置字段
|
// 表单里配置字段
|
||||||
export const FormItemInitializers = (props: any) => {
|
export const FormItemInitializers = (props: any) => {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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) => {
|
export const CollectionFieldInitializer = (props) => {
|
||||||
const schema: ISchema = {};
|
const schema: ISchema = {};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
|
||||||
import { merge } from '@formily/shared';
|
import { merge } from '@formily/shared';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { SchemaInitializer } from '..';
|
import { SchemaInitializer } from '..';
|
||||||
import { useCurrentSchema } from '../utils';
|
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 { css } from '@emotion/css';
|
||||||
import { ArrayItems } from '@formily/antd';
|
import { ArrayCollapse, ArrayItems, FormDialog, FormItem, FormLayout, Input } from '@formily/antd';
|
||||||
import { FormDialog, FormItem, FormLayout, Input, ArrayCollapse } from '@formily/antd';
|
import { Field, GeneralField, createForm } from '@formily/core';
|
||||||
import { createForm, Field, GeneralField } from '@formily/core';
|
|
||||||
import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema, useForm } from '@formily/react';
|
import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
import _ from 'lodash';
|
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
@ -20,35 +18,37 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { cloneDeep } from 'lodash';
|
import _, { cloneDeep } from 'lodash';
|
||||||
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
|
APIClientProvider,
|
||||||
ActionContext,
|
ActionContext,
|
||||||
CollectionManagerContext,
|
CollectionManagerContext,
|
||||||
createDesignable,
|
|
||||||
Designable,
|
Designable,
|
||||||
FormProvider,
|
FormProvider,
|
||||||
RemoteSchemaComponent,
|
RemoteSchemaComponent,
|
||||||
SchemaComponent,
|
SchemaComponent,
|
||||||
SchemaComponentOptions,
|
SchemaComponentOptions,
|
||||||
useActionContext,
|
createDesignable,
|
||||||
|
findFormBlock,
|
||||||
useAPIClient,
|
useAPIClient,
|
||||||
useCollection,
|
useCollection,
|
||||||
|
useCollectionFilterOptions,
|
||||||
useCollectionManager,
|
useCollectionManager,
|
||||||
useCompile,
|
useCompile,
|
||||||
useDesignable,
|
useDesignable,
|
||||||
useCollectionFilterOptions,
|
|
||||||
} from '..';
|
} 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 { useSchemaTemplateManager } from '../schema-templates';
|
||||||
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate';
|
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate';
|
||||||
|
import { FormDataTemplates } from './DataTemplates';
|
||||||
|
import { EnableChildCollections } from './EnableChildCollections';
|
||||||
import { FormLinkageRules } from './LinkageRules';
|
import { FormLinkageRules } from './LinkageRules';
|
||||||
import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks';
|
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 {
|
interface SchemaSettingsProps {
|
||||||
title?: any;
|
title?: any;
|
||||||
@ -376,7 +376,7 @@ SchemaSettings.FormItemTemplate = (props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('Save as template')}
|
{t('Save as block template')}
|
||||||
</SchemaSettings.Item>
|
</SchemaSettings.Item>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -780,6 +780,7 @@ SchemaSettings.ModalItem = (props) => {
|
|||||||
} = props;
|
} = props;
|
||||||
const options = useContext(SchemaOptionsContext);
|
const options = useContext(SchemaOptionsContext);
|
||||||
const cm = useContext(CollectionManagerContext);
|
const cm = useContext(CollectionManagerContext);
|
||||||
|
const apiClient = useAPIClient();
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -793,7 +794,9 @@ SchemaSettings.ModalItem = (props) => {
|
|||||||
<CollectionManagerContext.Provider value={cm}>
|
<CollectionManagerContext.Provider value={cm}>
|
||||||
<SchemaComponentOptions scope={options.scope} components={options.components}>
|
<SchemaComponentOptions scope={options.scope} components={options.components}>
|
||||||
<FormLayout layout={'vertical'} style={{ minWidth: 520 }}>
|
<FormLayout layout={'vertical'} style={{ minWidth: 520 }}>
|
||||||
|
<APIClientProvider apiClient={apiClient}>
|
||||||
<SchemaComponent components={components} scope={scope} schema={schema} />
|
<SchemaComponent components={components} scope={scope} schema={schema} />
|
||||||
|
</APIClientProvider>
|
||||||
</FormLayout>
|
</FormLayout>
|
||||||
</SchemaComponentOptions>
|
</SchemaComponentOptions>
|
||||||
</CollectionManagerContext.Provider>
|
</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) => {
|
SchemaSettings.EnableChildCollections = (props) => {
|
||||||
const { collectionName } = props;
|
const { collectionName } = props;
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
|
@ -5,6 +5,7 @@ import i18next from 'i18next';
|
|||||||
import bodyParser from 'koa-bodyparser';
|
import bodyParser from 'koa-bodyparser';
|
||||||
import Application, { ApplicationOptions } from './application';
|
import Application, { ApplicationOptions } from './application';
|
||||||
import { parseVariables } from './middlewares';
|
import { parseVariables } from './middlewares';
|
||||||
|
import { dateTemplate } from './middlewares/data-template';
|
||||||
import { dataWrapping } from './middlewares/data-wrapping';
|
import { dataWrapping } from './middlewares/data-wrapping';
|
||||||
import { db2resource } from './middlewares/db2resource';
|
import { db2resource } from './middlewares/db2resource';
|
||||||
import { i18n } from './middlewares/i18n';
|
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(parseVariables, { tag: 'parseVariables', after: 'acl' });
|
||||||
|
app.resourcer.use(dateTemplate, { tag: 'dateTemplate', after: 'acl' });
|
||||||
|
|
||||||
app.use(db2resource, { tag: 'db2resource', after: 'dataWrapping' });
|
app.use(db2resource, { tag: 'db2resource', after: 'dataWrapping' });
|
||||||
app.use(app.resourcer.restApiMiddleware(), { tag: 'restApi', after: 'db2resource' });
|
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",
|
"Turn pages": "Turn pages",
|
||||||
"Others": "Others",
|
"Others": "Others",
|
||||||
"Save as template": "Save as template",
|
"Save as template": "Save as template",
|
||||||
|
"Save as block template": "Save as block template",
|
||||||
"Block templates": "Block templates",
|
"Block templates": "Block templates",
|
||||||
"Convert reference to duplicate": "Convert reference to duplicate",
|
"Convert reference to duplicate": "Convert reference to duplicate",
|
||||||
"Template name": "Template name",
|
"Template name": "Template name",
|
||||||
|
@ -384,6 +384,7 @@
|
|||||||
"Turn pages": "Virar páginas",
|
"Turn pages": "Virar páginas",
|
||||||
"Others": "Outros",
|
"Others": "Outros",
|
||||||
"Save as template": "Salvar como modelo",
|
"Save as template": "Salvar como modelo",
|
||||||
|
"Save as block template": "Salvar como modelo de bloco",
|
||||||
"Block templates": "Modelos de bloco",
|
"Block templates": "Modelos de bloco",
|
||||||
"Convert reference to duplicate": "Converter referência em duplicado",
|
"Convert reference to duplicate": "Converter referência em duplicado",
|
||||||
"Template name": "Nome do modelo",
|
"Template name": "Nome do modelo",
|
||||||
|
@ -400,6 +400,7 @@
|
|||||||
"Turn pages": "翻页",
|
"Turn pages": "翻页",
|
||||||
"Others": "其他",
|
"Others": "其他",
|
||||||
"Save as template": "保存为模板",
|
"Save as template": "保存为模板",
|
||||||
|
"Save as block template": "保存为区块模板",
|
||||||
"Block templates": "区块模板",
|
"Block templates": "区块模板",
|
||||||
"Convert reference to duplicate": "模板引用转为复制",
|
"Convert reference to duplicate": "模板引用转为复制",
|
||||||
"Template name": "模板名称",
|
"Template name": "模板名称",
|
||||||
|
Loading…
Reference in New Issue
Block a user