1850 lines
56 KiB
TypeScript

/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Field, Form } from '@formily/core';
import { ISchema, Schema, useFieldSchema, useForm } from '@formily/react';
import { uid } from '@formily/shared';
import _ from 'lodash';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
DataBlockInitializer,
DataSource,
SchemaInitializerItemType,
useAssociationName,
useCollection,
useCollectionManager,
useDataSourceKey,
} from '../';
import { useFormBlockContext } from '../block-provider/FormBlockProvider';
import { useFormActiveFields } from '../block-provider/hooks/useFormActiveFields';
import {
CollectionFieldOptions_deprecated,
FieldOptions,
useCollectionManager_deprecated,
useCollection_deprecated,
} from '../collection-manager';
import { Collection, CollectionFieldOptions, CollectionOptions } from '../data-source/collection/Collection';
import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider';
import { isAssocField } from '../filter-provider/utils';
import { useActionContext, useCompile, useDesignable } from '../schema-component';
import { useSchemaTemplateManager } from '../schema-templates';
import { useBlockTemplateContext } from '../schema-templates/BlockTemplateProvider';
export const itemsMerge = (items1) => {
return items1;
};
export const gridRowColWrap = (schema: ISchema) => {
return {
type: 'void',
'x-component': 'Grid.Row',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
[schema?.name || uid()]: schema,
},
},
},
};
};
export const removeTableColumn = (schema, cb) => {
cb(schema.parent);
};
export const removeGridFormItem = (schema, cb) => {
cb(schema, {
removeParentsIfNoChildren: true,
breakRemoveOn: {
'x-component': 'Grid',
},
});
};
export const useRemoveGridFormItem = () => {
const form = useForm();
return (schema, cb) => {
cb(schema, {
removeParentsIfNoChildren: true,
breakRemoveOn: {
'x-component': 'Grid',
},
});
delete form.values?.[schema.name];
};
};
export const findTableColumn = (schema: Schema, key: string, action: string, deepth = 0) => {
return schema.reduceProperties((buf, s) => {
if (s[key] === action) {
return s;
}
const c = s.reduceProperties((buf, s) => {
if (s[key] === action) {
return s;
}
return buf;
});
if (c) {
return c;
}
return buf;
});
};
const quickEditField = [
'attachment',
'textarea',
'markdown',
'json',
'richText',
'polygon',
'circle',
'point',
'lineString',
];
export function useTableColumnInitializerFields() {
const { name, currentFields = [] } = useCollection_deprecated();
const { getInterface, getCollection } = useCollectionManager_deprecated();
const fieldSchema = useFieldSchema();
const isSubTable = fieldSchema['x-component'] === 'AssociationField.SubTable';
const form = useForm();
const isReadPretty = isSubTable ? form.readPretty : true;
return currentFields
.filter((field) => field?.interface && field?.interface !== 'subTable' && !field?.treeChildren)
.map((field) => {
const interfaceConfig = getInterface(field.interface);
const isFileCollection = field?.target && getCollection(field?.target)?.template === 'file';
const isPreviewComponent = field?.uiSchema?.['x-component'] === 'Preview';
const schema = {
name: field.name,
'x-collection-field': `${name}.${field.name}`,
'x-component': 'CollectionField',
'x-component-props': isFileCollection
? {
fieldNames: {
label: 'preview',
value: 'id',
},
}
: isPreviewComponent
? { size: 'small' }
: {},
'x-read-pretty': isReadPretty || field.uiSchema?.['x-read-pretty'],
'x-decorator': isSubTable
? quickEditField.includes(field.interface) || isFileCollection
? 'QuickEdit'
: 'FormItem'
: null,
'x-decorator-props': {
labelStyle: {
display: 'none',
},
},
};
// interfaceConfig?.schemaInitialize?.(schema, { field, readPretty: true, block: 'Table' });
return {
type: 'item',
name: field.name,
title: field?.uiSchema?.title || field.name,
Component: 'TableCollectionFieldInitializer',
find: findTableColumn,
remove: removeTableColumn,
schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, {
field,
readPretty: isReadPretty,
block: 'Table',
targetCollection: getCollection(field.target),
});
},
field,
schema,
} as SchemaInitializerItemType;
});
}
export function useAssociatedTableColumnInitializerFields() {
const { name, fields } = useCollection_deprecated();
const { t } = useTranslation();
const { getInterface, getCollectionFields, getCollection } = useCollectionManager_deprecated();
const groups = fields
?.filter((field) => {
return ['o2o', 'oho', 'obo', 'm2o'].includes(field.interface);
})
?.map((field) => {
return getGroupItemForTable({
getCollectionFields,
field,
getInterface,
getCollection,
schemaName: field.name,
maxDepth: 2,
depth: 1,
t,
});
});
return groups;
}
function getGroupItemForTable({
getCollectionFields,
field,
getInterface,
getCollection,
schemaName,
maxDepth,
depth,
t,
}: {
getCollectionFields: (name: any, customDataSource?: string) => CollectionFieldOptions_deprecated[];
field: CollectionFieldOptions;
getInterface: (name: string) => any;
getCollection: (name: any, customDataSource?: string) => CollectionOptions;
schemaName: string;
maxDepth: number;
depth: number;
t: any;
}) {
const subFields = getCollectionFields(field.target);
const items = subFields
?.filter(
(subField) => subField?.interface && !['subTable'].includes(subField?.interface) && !subField?.treeChildren,
)
?.map((subField) => {
const interfaceConfig = getInterface(subField.interface);
const newSchemaName = `${schemaName}.${subField.name}`;
const schema = {
// type: 'string',
name: newSchemaName,
// title: subField?.uiSchema?.title || subField.name,
'x-component': 'CollectionField',
'x-read-pretty': true,
'x-collection-field': `${field.target}.${subField.name}`,
'x-component-props': {},
};
return {
type: 'item',
name: newSchemaName,
title: subField?.uiSchema?.title || subField.name,
Component: 'TableCollectionFieldInitializer',
find: findTableColumn,
remove: removeTableColumn,
schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, {
field: subField,
readPretty: true,
block: 'Table',
targetCollection: getCollection(field.target),
});
},
field: subField,
schema,
} as SchemaInitializerItemType;
});
const displayCollectionFields = {
type: 'itemGroup',
name: `${schemaName}-displayCollectionFields`,
title: t('Display fields'),
children: items,
};
const children = [displayCollectionFields];
if (depth < maxDepth) {
const subChildren = subFields
?.filter((subField) => {
return ['o2o', 'oho', 'obo', 'm2o'].includes(subField.interface);
})
.map((subField) => {
return getGroupItemForTable({
getCollectionFields,
field: subField,
getInterface,
getCollection,
schemaName: `${schemaName}.${subField.name}`,
maxDepth,
depth: depth + 1,
t,
});
});
if (subChildren.length) {
const group: any = {
type: 'itemGroup',
name: `${schemaName}-associationFields`,
title: t('Display association fields'),
children: subChildren,
};
children.push(group);
}
}
return {
type: 'subMenu',
name: `${schemaName}`,
title: field.uiSchema?.title,
children,
} as SchemaInitializerItemType;
}
export function useInheritsTableColumnInitializerFields() {
const { name } = useCollection_deprecated();
const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } =
useCollectionManager_deprecated();
const fieldSchema = useFieldSchema();
const isSubTable = fieldSchema['x-component'] === 'AssociationField.SubTable';
const form = useForm();
const isReadPretty = isSubTable ? form.readPretty : true;
const inherits = getInheritCollections(name);
return inherits?.map((v) => {
const fields = getParentCollectionFields(v, name);
const targetCollection = getCollection(v);
return {
[targetCollection?.title]: fields
?.filter((field) => {
return field?.interface;
})
.map((k) => {
const interfaceConfig = getInterface(k.interface);
const isFileCollection = k?.target && getCollection(k?.target)?.template === 'file';
const schema = {
name: `${k.name}`,
'x-component': 'CollectionField',
'x-read-pretty': isReadPretty || k.uiSchema?.['x-read-pretty'],
'x-collection-field': `${name}.${k.name}`,
'x-component-props': isFileCollection
? {
fieldNames: {
label: 'preview',
value: 'id',
},
}
: {},
'x-decorator': isSubTable
? quickEditField.includes(k.interface) || isFileCollection
? 'QuickEdit'
: 'FormItem'
: null,
'x-decorator-props': {
labelStyle: {
display: 'none',
},
},
};
return {
name: k?.uiSchema?.title || k.name,
type: 'item',
title: k?.uiSchema?.title || k.name,
Component: 'TableCollectionFieldInitializer',
find: findTableColumn,
remove: removeTableColumn,
schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, {
field: k,
readPretty: true,
block: 'Table',
targetCollection: getCollection(k?.target),
});
},
field: k,
schema,
} as SchemaInitializerItemType;
}),
};
});
}
export const useFormItemInitializerFields = (options?: any) => {
const { name, currentFields } = useCollection_deprecated();
const { getInterface, getCollection } = useCollectionManager_deprecated();
const form = useForm();
const { readPretty = form.readPretty, block = 'Form' } = options || {};
const { fieldSchema } = useActionContext();
const action = fieldSchema?.['x-action'];
return currentFields
?.filter((field) => field?.interface && !field?.treeChildren)
?.map((field) => {
const interfaceConfig = getInterface(field.interface);
const targetCollection = getCollection(field.target);
const isFileCollection = field?.target && getCollection(field?.target)?.template === 'file';
const isAssociationField = targetCollection;
const fieldNames = field?.uiSchema?.['x-component-props']?.['fieldNames'];
const schema = {
type: 'string',
name: field.name,
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': `${name}.${field.name}`,
'x-component-props': isFileCollection
? {
fieldNames: {
label: 'preview',
value: 'id',
},
}
: isAssociationField && fieldNames
? {
fieldNames: { ...fieldNames, label: targetCollection?.titleField || fieldNames.label },
}
: {},
'x-read-pretty': field?.uiSchema?.['x-read-pretty'],
};
const resultItem = {
type: 'item',
name: field.name,
title: field?.uiSchema?.title || field.name,
Component: 'CollectionFieldInitializer',
remove: removeGridFormItem,
schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, {
field,
block,
readPretty,
action,
targetCollection,
});
},
schema,
} as SchemaInitializerItemType;
if (block == 'Kanban') {
resultItem['find'] = (schema: Schema, key: string, action: string) => {
const s = findSchema(schema, 'x-component', block);
return findSchema(s, key, action);
};
}
return resultItem;
});
};
// 筛选表单相关
export const useFilterFormItemInitializerFields = (options?: any) => {
const { name, currentFields } = useCollection_deprecated();
const { getInterface, getCollection } = useCollectionManager_deprecated();
const form = useForm();
const { readPretty = form.readPretty, block = 'FilterForm' } = options || {};
const { snapshot, fieldSchema } = useActionContext();
const action = fieldSchema?.['x-action'];
return currentFields
?.filter((field) => field?.interface && getInterface(field.interface)?.filterable)
?.map((field) => {
const interfaceConfig = getInterface(field.interface);
const targetCollection = getCollection(field.target);
let schema = {
type: 'string',
name: field.name,
required: false,
// 'x-designer': 'FormItem.FilterFormDesigner',
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FilterFormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-use-decorator-props': 'useFormItemProps',
'x-collection-field': `${name}.${field.name}`,
'x-component-props': {
component: interfaceConfig?.filterable?.operators?.[0]?.schema?.['x-component'],
},
};
if (isAssocField(field)) {
schema = {
type: 'string',
name: `${field.name}`,
required: false,
// 'x-designer': 'FormItem.FilterFormDesigner',
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FilterFormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-use-decorator-props': 'useFormItemProps',
'x-collection-field': `${name}.${field.name}`,
'x-component-props': field.uiSchema?.['x-component-props'],
};
}
const resultItem = {
name: field?.uiSchema?.title || field.name,
type: 'item',
title: field?.uiSchema?.title || field.name,
Component: 'CollectionFieldInitializer',
remove: removeGridFormItem,
schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, {
field,
block,
readPretty,
action,
targetCollection,
});
},
schema,
} as SchemaInitializerItemType;
return resultItem;
});
};
export const useAssociatedFormItemInitializerFields = (options?: any) => {
const { name, fields } = useCollection_deprecated();
const { getInterface, getCollectionFields, getCollection } = useCollectionManager_deprecated();
const form = useForm();
const { t } = useTranslation();
const { readPretty = form.readPretty, block = 'Form' } = options || {};
const interfaces = block === 'Form' ? ['m2o'] : ['o2o', 'oho', 'obo', 'm2o'];
const groups = fields
?.filter((field) => {
return interfaces.includes(field.interface);
})
?.map((field) => {
return getGroupItemForForm({
getCollectionFields,
field,
getInterface,
getCollection,
readPretty,
block,
schemaName: field.name,
maxDepth: 2,
depth: 1,
t,
});
});
return groups;
};
const associationFieldToMenu = (
field: FieldOptions,
schemaName: string,
collectionName: string,
getCollectionFields,
processedCollections: string[],
) => {
if (field.target && field.uiSchema) {
if (processedCollections.includes(field.target) || processedCollections.length >= 1) return;
const subFields = getCollectionFields(field.target);
if (!subFields?.length) return;
return {
type: 'subMenu',
name: schemaName,
title: field.uiSchema?.title,
children: subFields
.map((subField) =>
associationFieldToMenu(subField, `${schemaName}.${subField.name}`, collectionName, getCollectionFields, [
...processedCollections,
field.target,
]),
)
.filter(Boolean),
} as SchemaInitializerItemType;
}
if (!field.uiSchema) return;
const schema = {
type: 'string',
name: schemaName,
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FilterFormItem',
'x-designer-props': {
// 在 useOperatorList 中使用,用于获取对应的操作符列表
interface: field.interface,
},
'x-component': 'CollectionField',
'x-read-pretty': false,
'x-decorator': 'FormItem',
'x-collection-field': `${collectionName}.${schemaName}`,
};
return {
name: field.uiSchema?.title || field.name,
type: 'item',
title: field.uiSchema?.title || field.name,
Component: 'CollectionFieldInitializer',
remove: removeGridFormItem,
schema,
} as SchemaInitializerItemType;
};
// 筛选表单相关
export const useFilterAssociatedFormItemInitializerFields = () => {
const { name, fields } = useCollection_deprecated();
const { getCollectionFields } = useCollectionManager_deprecated();
return fields
?.filter((field) => field.target && field.uiSchema)
.map((field) => associationFieldToMenu(field, field.name, name, getCollectionFields, []))
.filter(Boolean);
};
export const useInheritsFormItemInitializerFields = (options?) => {
const { name } = useCollection_deprecated();
const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } =
useCollectionManager_deprecated();
const inherits = getInheritCollections(name);
const { snapshot } = useActionContext();
const form = useForm();
return inherits?.map((v) => {
const fields = getParentCollectionFields(v, name);
const { readPretty = form.readPretty, block = 'Form', component = 'CollectionField' } = options || {};
const targetCollection = getCollection(v);
return {
[targetCollection?.title]: fields
?.filter((field) => field?.interface)
?.map((field) => {
const interfaceConfig = getInterface(field.interface);
const targetCollection = getCollection(field.target);
// const component =
// field.interface === 'o2m' && targetCollection?.template !== 'file' && !snapshot
// ? 'TableField'
// : 'CollectionField';
const schema = {
type: 'string',
name: field.name,
title: field?.uiSchema?.title || field.name,
// 'x-designer': 'FormItem.Designer',
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': component,
'x-decorator': 'FormItem',
'x-collection-field': `${name}.${field.name}`,
'x-component-props': {},
'x-read-pretty': field?.uiSchema?.['x-read-pretty'],
};
return {
name: field?.uiSchema?.title || field.name,
type: 'item',
title: field?.uiSchema?.title || field.name,
Component: 'CollectionFieldInitializer',
remove: removeGridFormItem,
schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, {
field,
block,
readPretty,
targetCollection,
});
},
schema,
} as SchemaInitializerItemType;
}),
};
});
};
// 筛选表单相关
export const useFilterInheritsFormItemInitializerFields = (options?) => {
const { name } = useCollection_deprecated();
const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } =
useCollectionManager_deprecated();
const inherits = getInheritCollections(name);
const { snapshot } = useActionContext();
const form = useForm();
return inherits?.map((v) => {
const fields = getParentCollectionFields(v, name);
const { readPretty = form.readPretty, block = 'Form' } = options || {};
const targetCollection = getCollection(v);
return {
[targetCollection.title]: fields
?.filter((field) => field?.interface && getInterface(field.interface)?.filterable)
?.map((field) => {
const interfaceConfig = getInterface(field.interface);
const targetCollection = getCollection(field.target);
// const component =
// field.interface === 'o2m' && targetCollection?.template !== 'file' && !snapshot
// ? 'TableField'
// : 'CollectionField';
const schema = {
type: 'string',
name: field.name,
title: field?.uiSchema?.title || field.name,
required: false,
// 'x-designer': 'FormItem.FilterFormDesigner',
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FilterFormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': `${name}.${field.name}`,
'x-component-props': {},
'x-read-pretty': field?.uiSchema?.['x-read-pretty'],
};
return {
name: field?.uiSchema?.title || field.name,
type: 'item',
title: field?.uiSchema?.title || field.name,
Component: 'CollectionFieldInitializer',
remove: removeGridFormItem,
schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, {
field,
block,
readPretty,
targetCollection,
});
},
schema,
} as SchemaInitializerItemType;
}),
};
});
};
export const useCustomFormItemInitializerFields = (options?: any) => {
const { name, currentFields } = useCollection_deprecated();
const { getInterface, getCollection } = useCollectionManager_deprecated();
const form = useForm();
const { readPretty = form.readPretty, block = 'Form' } = options || {};
const remove = useRemoveGridFormItem();
return currentFields
?.filter((field) => {
return field?.interface && field.interface !== 'snapshot' && field.type !== 'sequence';
})
?.map((field) => {
const interfaceConfig = getInterface(field.interface);
const schema = {
type: 'string',
name: field.name,
title: field?.uiSchema?.title || field.name,
// 'x-designer': 'FormItem.Designer',
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'AssignedField',
'x-decorator': 'FormItem',
'x-collection-field': `${name}.${field.name}`,
};
return {
name: field?.uiSchema?.title || field.name,
type: 'item',
title: field?.uiSchema?.title || field.name,
Component: 'CollectionFieldInitializer',
remove: remove,
schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, {
field,
block,
readPretty,
targetCollection: getCollection(field.target),
});
},
schema,
} as SchemaInitializerItemType;
});
};
export const findSchema = (schema: Schema, key: string, action: string, name?: string) => {
if (!Schema.isSchemaInstance(schema)) return null;
return schema.reduceProperties((buf, s) => {
if (s[key] === action && (!name || s.name === name)) {
return s;
}
if (s['x-component'] !== 'Action.Container' && !s['x-component'].includes('AssociationField')) {
const c = findSchema(s, key, action, name);
if (c) {
return c;
}
}
return buf;
});
};
const removeSchema = (schema, cb) => {
return cb(schema);
};
const recursiveParent = (schema: Schema) => {
if (!schema.parent) return null;
if (schema.parent['x-initializer']) return schema.parent;
return recursiveParent(schema.parent);
};
export const useCurrentSchema = (action: string, key: string, find = findSchema, rm = removeSchema, name?: string) => {
const { removeActiveFieldName } = useFormActiveFields() || {};
const { form }: { form?: Form } = useFormBlockContext();
let fieldSchema = useFieldSchema();
if (!fieldSchema?.['x-initializer'] && fieldSchema?.['x-decorator'] === 'FormItem') {
const recursiveInitializerSchema = recursiveParent(fieldSchema);
if (recursiveInitializerSchema) {
fieldSchema = recursiveInitializerSchema;
}
}
const { remove } = useDesignable();
const schema = find(fieldSchema, key, action, name);
return {
schema,
exists: !!schema,
remove() {
removeActiveFieldName?.(schema.name);
form?.query(new RegExp(`${schema.parent.name}.${schema.name}$`)).forEach((field: Field) => {
// 如果字段被删掉,那么在提交的时候不应该提交这个字段
field.setValue?.(undefined);
field.setInitialValue?.(undefined);
});
schema && rm(schema, remove);
},
};
};
/**
* @deprecated
* 待统一区块的创建之后,将废弃该方法
*/
export const useRecordCollectionDataSourceItems = (
componentName,
item = null,
collectionName = null,
resourceName = null,
) => {
const { t } = useTranslation();
const collection = useCollection_deprecated();
const { getTemplatesByCollection } = useSchemaTemplateManager();
const templates = getTemplatesByCollection(collection.dataSource, collectionName || collection.name)
.filter((template) => {
return componentName && template.componentName === componentName;
})
.filter((template) => {
return ['FormItem', 'ReadPrettyFormItem'].includes(componentName) || template.resourceName === resourceName;
});
if (!templates.length) {
return [];
}
const index = 0;
return [
{
key: `${collectionName || componentName}_table_blank`,
type: 'item',
name: collection.name,
title: t('Blank block'),
item,
},
{
type: 'divider',
},
{
key: `${collectionName || componentName}_table_subMenu_${index}_copy`,
type: 'subMenu',
name: 'copy',
title: t('Duplicate template'),
children: templates.map((template) => {
const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName)
? `${template?.name} ${t('(Fields only)')}`
: template?.name;
return {
type: 'item',
mode: 'copy',
name: collection.name,
template,
item,
title: templateName || t('Untitled'),
};
}),
},
{
key: `${collectionName || componentName}_table_subMenu_${index}_ref`,
type: 'subMenu',
name: 'ref',
title: t('Reference template'),
children: templates.map((template) => {
const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName)
? `${template?.name} ${t('(Fields only)')}`
: template?.name;
return {
type: 'item',
mode: 'reference',
name: collection.name,
template,
item,
title: templateName || t('Untitled'),
};
}),
},
];
};
export const useCollectionDataSourceItems = ({
name,
componentName,
filter = () => true,
onlyCurrentDataSource = false,
showAssociationFields,
filterDataSource,
dataBlockInitializerProps,
hideOtherRecordsInPopup,
onClick,
filterOtherRecordsCollection,
currentText,
otherText,
}: {
name: string;
componentName: string;
filter?: (options: { collection?: Collection; associationField?: CollectionFieldOptions }) => boolean;
onlyCurrentDataSource?: boolean;
showAssociationFields?: boolean;
filterDataSource?: (dataSource?: DataSource) => boolean;
dataBlockInitializerProps?: any;
/**
* 隐藏弹窗中的 Other records 选项
*/
hideOtherRecordsInPopup?: boolean;
onClick?: (options: any) => void;
/**
* 用来筛选弹窗中的 “Other records” 选项中的数据表
*/
filterOtherRecordsCollection?: (collection: Collection) => boolean;
currentText?: string;
otherText?: string;
}) => {
const { componentNamePrefix } = useBlockTemplateContext();
const { t } = useTranslation();
const dm = useDataSourceManager();
const dataSourceKey = useDataSourceKey();
const collection = useCollection();
const associationFields = useAssociationFields({
componentName: componentNamePrefix + componentName,
filterCollections: filter,
showAssociationFields,
componentNamePrefix,
});
const association = useAssociationName();
let allCollections = dm.getAllCollections({
filterCollection: (collection) => {
if (onlyCurrentDataSource && collection.dataSource !== dataSourceKey) {
return false;
}
return filter({ collection });
},
filterDataSource,
});
if (onlyCurrentDataSource) {
allCollections = allCollections.filter((collection) => collection.key === dataSourceKey);
}
const { getTemplatesByCollection } = useSchemaTemplateManager();
const noAssociationMenu = useMemo(() => {
return allCollections.map(({ key, displayName, collections }) => ({
name: key,
label: displayName,
type: 'subMenu',
children: [
...getChildren({
name,
association,
collections,
componentName: componentNamePrefix + componentName,
searchValue: '',
dataSource: key,
getTemplatesByCollection,
t,
componentNamePrefix,
}).sort((item) => {
// fix https://nocobase.height.app/T-3551
const inherits = _.toArray(collection?.inherits || []);
if (item.name === collection?.name || inherits.some((inheritName) => inheritName === item.name)) return -1;
}),
],
}));
}, [allCollections, collection?.inherits, collection?.name, componentName, getTemplatesByCollection, t]);
// https://nocobase.height.app/T-3821
// showAssociationFields 的值是固定不变的,所以在 if 语句里使用 hooks 是安全的
if (showAssociationFields) {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useMemo(() => {
const currentRecord = {
name: 'currentRecord',
collectionName: collection.name,
dataSource: collection.dataSource,
Component: DataBlockInitializer,
// 目的是使点击无效
onClick() {},
componentProps: {
...dataBlockInitializerProps,
icon: null,
title: currentText || t('Current record'),
name: 'currentRecord',
hideSearch: false,
hideChildrenIfSingleCollection: true,
items: noAssociationMenu,
},
};
const associationRecords = {
name: 'associationRecords',
Component: DataBlockInitializer,
// 目的是使点击无效
onClick() {},
componentProps: {
...dataBlockInitializerProps,
icon: null,
title: t('Associated records'),
name: 'associationRecords',
hideSearch: false,
items: [
{
name: 'associationFields',
label: t('Association fields'),
type: 'subMenu', // 这里套一层 subMenu 是因为 DataBlockInitializer 组件需要这样的数据结构,其实这层 subMenu 最终是不会渲染出来的
children: associationFields,
},
],
},
};
const componentTypeMap = {
ReadPrettyFormItem: 'Details',
};
const otherRecords = {
name: 'otherRecords',
Component: DataBlockInitializer,
// 目的是使点击无效
onClick() {},
componentProps: {
...dataBlockInitializerProps,
icon: null,
title: otherText || t('Other records'),
name: 'otherRecords',
showAssociationFields: false,
onlyCurrentDataSource: false,
hideChildrenIfSingleCollection: false,
fromOthersInPopup: true,
componentType: componentTypeMap[componentName] || componentName,
filter({ collection, associationField }) {
if (filterOtherRecordsCollection) {
return filterOtherRecordsCollection(collection);
}
return true;
},
onClick(options) {
onClick({ ...options, fromOthersInPopup: true });
},
},
};
let children;
const _associationRecords = associationFields.length ? associationRecords : null;
if (noAssociationMenu[0].children.length && associationFields.length) {
if (hideOtherRecordsInPopup) {
children = [currentRecord, _associationRecords];
} else {
children = [currentRecord, _associationRecords, otherRecords];
}
} else if (noAssociationMenu[0].children.length) {
if (hideOtherRecordsInPopup) {
// 当可选数据表只有一个时,实现只点击一次区块 menu 就能创建区块
if (noAssociationMenu[0].children.length <= 1) {
noAssociationMenu[0].children = (noAssociationMenu[0].children[0]?.children as any) || [];
return noAssociationMenu;
}
children = [currentRecord];
} else {
children = [currentRecord, otherRecords];
}
} else {
if (hideOtherRecordsInPopup) {
children = [_associationRecords];
} else {
children = [_associationRecords, otherRecords];
}
}
return [
{
name: 'records',
label: t('Records'),
type: 'subMenu',
children: children.filter(Boolean),
},
];
}, [
associationFields,
collection.dataSource,
collection.name,
componentName,
dataBlockInitializerProps,
filterOtherRecordsCollection,
hideOtherRecordsInPopup,
noAssociationMenu,
onClick,
t,
]);
}
return noAssociationMenu;
};
/**
* @deprecated
* 已弃用,请使用 createDetailsUISchema 和 createDetailsWithPaginationUISchema 替代
* @param options
* @returns
*/
export const createDetailsBlockSchema = (options: {
collection: string;
dataSource: string;
rowKey?: string;
formItemInitializers?: string;
actionInitializers?: string;
association?: string;
template?: any;
settings?: string;
action?: string;
[key: string]: any;
}) => {
const {
formItemInitializers = 'details:configureFields',
actionInitializers = 'detailsWithPaging:configureActions',
collection,
dataSource,
association,
template,
settings,
action = 'list',
...others
} = options;
const resourceName = association || collection;
const schema: ISchema = {
type: 'void',
'x-acl-action': action === 'get' ? `${resourceName}:get` : `${resourceName}:view`,
'x-decorator': 'DetailsBlockProvider',
'x-decorator-props': {
dataSource,
collection,
association,
readPretty: true,
action,
...(action === 'list'
? {
params: {
pageSize: 1,
},
}
: {}),
...others,
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': settings,
'x-component': 'CardItem',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Details',
'x-use-component-props': 'useDetailsBlockProps',
'x-read-pretty': true,
properties: {
[uid()]: {
type: 'void',
'x-initializer': actionInitializers,
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 24,
},
},
properties: {},
},
grid: template || {
type: 'void',
'x-component': 'Grid',
'x-initializer': formItemInitializers,
properties: {},
},
...(action === 'list'
? {
pagination: {
type: 'void',
'x-component': 'Pagination',
'x-use-component-props': 'useDetailsPaginationProps',
},
}
: {}),
},
},
},
};
return schema;
};
/**
* @deprecated
* 已弃用,请使用 createCreateFormBlockUISchema 或者 createEditFormBlockUISchema 替代
* @param options
* @returns
*/
export const createFormBlockSchema = (options: {
formItemInitializers?: string;
actionInitializers?: string;
collection: string;
resource?: string;
dataSource?: string;
association?: string;
action?: string;
actions?: Record<string, any>;
template?: any;
title?: string;
settings?: any;
'x-designer'?: string;
[key: string]: any;
}) => {
const {
formItemInitializers = 'form:configureFields',
actionInitializers = 'createForm:configureActions',
collection,
resource,
dataSource,
association,
action,
actions = {},
'x-designer': designer = 'FormV2.Designer',
template,
title,
settings,
...others
} = options;
const resourceName = resource || association || collection;
const schema: ISchema = {
type: 'void',
'x-acl-action-props': {
skipScopeCheck: !action,
},
'x-acl-action': action ? `${resourceName}:update` : `${resourceName}:create`,
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
...others,
action,
dataSource,
resource: resourceName,
collection,
association,
},
'x-toolbar': 'BlockSchemaToolbar',
...(settings ? { 'x-settings': settings } : { 'x-designer': designer }),
'x-component': 'CardItem',
'x-component-props': {
title,
},
properties: {
[uid()]: {
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': 'useFormBlockProps',
properties: {
grid: template || {
type: 'void',
'x-component': 'Grid',
'x-initializer': formItemInitializers,
properties: {},
},
[uid()]: {
type: 'void',
'x-initializer': actionInitializers,
'x-component': 'ActionBar',
'x-component-props': {
layout: 'one-column',
style: {
marginTop: 24,
},
},
properties: actions,
},
},
},
},
};
return schema;
};
/**
* @deprecated
* 已弃用,可以使用 createDetailsBlockSchema 替换
* @param options
* @returns
*/
export const createReadPrettyFormBlockSchema = (options) => {
const {
formItemInitializers = 'details:configureFields',
actionInitializers = 'details:configureActions',
collection,
association,
dataSource,
resource,
template,
settings,
...others
} = options;
const resourceName = resource || association || collection;
const schema: ISchema = {
type: 'void',
'x-acl-action': `${resourceName}:get`,
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
resource: resourceName,
collection,
association,
dataSource,
readPretty: true,
action: 'get',
useParams: '{{ useParamsFromRecord }}',
...others,
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': settings,
'x-component': 'CardItem',
properties: {
[uid()]: {
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': 'useFormBlockProps',
'x-read-pretty': true,
properties: {
[uid()]: {
type: 'void',
'x-initializer': actionInitializers,
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 24,
},
},
properties: {},
},
grid: template || {
type: 'void',
'x-component': 'Grid',
'x-initializer': formItemInitializers,
properties: {},
},
},
},
},
};
return schema;
};
/**
* @deprecated
* 已弃用,可以使用 createTableBlockUISchema 替换
* @param options
* @returns
*/
export const createTableBlockSchema = (options) => {
const {
collection,
rowKey,
tableActionInitializers,
tableColumnInitializers,
tableActionColumnInitializers,
tableBlockProvider,
disableTemplate,
dataSource,
blockType,
pageSize = 20,
...others
} = options;
const schema: ISchema = {
type: 'void',
'x-decorator': tableBlockProvider ?? 'TableBlockProvider',
'x-acl-action': `${collection}:list`,
'x-decorator-props': {
collection,
dataSource,
action: 'list',
params: {
pageSize,
},
rowKey,
showIndex: true,
dragSort: false,
disableTemplate: disableTemplate ?? false,
blockType,
...others,
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:table',
'x-component': 'CardItem',
'x-filter-targets': [],
properties: {
actions: {
type: 'void',
'x-initializer': tableActionInitializers ?? 'table:configureActions',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
properties: {},
},
[uid()]: {
type: 'array',
'x-initializer': tableColumnInitializers ?? 'table:configureColumns',
'x-component': 'TableV2',
'x-use-component-props': 'useTableBlockProps',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
},
properties: {
actions: {
type: 'void',
title: '{{ t("Actions") }}',
'x-action-column': 'actions',
'x-decorator': 'TableV2.Column.ActionBar',
'x-component': 'TableV2.Column',
'x-designer': 'TableV2.ActionColumnDesigner',
'x-initializer': tableActionColumnInitializers ?? 'table:configureItemActions',
properties: {
[uid()]: {
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': {},
properties: {},
},
},
},
},
},
},
};
// console.log(JSON.stringify(schema, null, 2));
return schema;
};
const getChildren = ({
name,
association,
collections,
dataSource,
componentName,
searchValue,
getTemplatesByCollection,
t,
componentNamePrefix,
}: {
name: string;
association: string;
collections: any[];
componentName: string;
searchValue: string;
dataSource: string;
getTemplatesByCollection: (dataSource: string, collectionName: string, resourceName?: string) => any;
t: any;
componentNamePrefix: string;
}) => {
return collections
?.filter((item) => {
if (item.inherit) {
return false;
}
if (!item.filterTargetKey) {
return false;
} else if (
[componentNamePrefix + 'Kanban', componentNamePrefix + 'FormItem'].includes(componentName) &&
((item.template === 'view' && !item.writableView) || item.template === 'sql')
) {
return false;
} else if (
item.template === 'file' &&
[componentNamePrefix + 'Kanban', componentNamePrefix + 'FormItem', componentNamePrefix + 'Calendar'].includes(
componentName,
)
) {
return false;
} else {
const title = item.title || item.tableName;
if (!title) {
return false;
}
return title.toUpperCase().includes(searchValue.toUpperCase()) && !(item?.isThrough && item?.autoCreate);
}
})
?.map((item, index) => {
const title = item.title || item.tableName || item.label;
const templates = getTemplatesByCollection(dataSource, item.name).filter((template) => {
// 弹窗中的 Current record 选项
const isCurrentRecordOption = name !== 'otherRecords' && association;
if (isCurrentRecordOption) {
if (template.resourceName !== association) {
return false;
}
return componentName && template.componentName === componentName;
}
if (!isCurrentRecordOption && template?.resourceName?.includes('.')) {
return false;
}
return componentName && template.componentName === componentName;
});
if (!templates.length) {
return {
type: 'item',
name: item.name,
title,
dataSource,
};
}
return {
key: `${componentName}_table_subMenu_${index}`,
type: 'subMenu',
name: `${item.name}_${index}`,
title,
dataSource,
children: [
{
type: 'item',
name: item.name,
dataSource,
title: t('Blank block'),
},
{
type: 'divider',
},
{
key: `${componentName}_table_subMenu_${index}_copy`,
type: 'subMenu',
name: 'copy',
dataSource,
title: t('Duplicate template'),
children: templates.map((template) => {
const templateName = [
componentNamePrefix + 'FormItem',
componentNamePrefix + 'ReadPrettyFormItem',
].includes(template?.componentName)
? `${template?.name} ${t('(Fields only)')}`
: template?.name;
return {
type: 'item',
mode: 'copy',
name: item.name,
template,
dataSource,
title: templateName || t('Untitled'),
};
}),
},
{
key: `${componentName}_table_subMenu_${index}_ref`,
type: 'subMenu',
name: 'ref',
dataSource,
title: t('Reference template'),
children: templates.map((template) => {
const templateName = [
componentNamePrefix + 'FormItem',
componentNamePrefix + 'ReadPrettyFormItem',
].includes(template?.componentName)
? `${template?.name} ${t('(Fields only)')}`
: template?.name;
return {
type: 'item',
mode: 'reference',
name: item.name,
template,
dataSource,
title: templateName || t('Untitled'),
};
}),
},
],
};
});
};
function getGroupItemForForm({
getCollectionFields,
field,
getInterface,
getCollection,
readPretty,
block,
maxDepth,
depth,
schemaName,
t,
}: {
getCollectionFields: (name: any, customDataSource?: string) => CollectionFieldOptions_deprecated[];
field: CollectionFieldOptions;
getInterface: (name: string) => any;
getCollection: (name: any, customDataSource?: string) => CollectionOptions;
readPretty: any;
block: any;
maxDepth: number;
depth: number;
schemaName: string;
t: any;
}) {
const subFields = getCollectionFields(field.target);
const items = subFields
?.filter((subField) => subField?.interface && !['subTable'].includes(subField?.interface) && !subField.treeChildren)
?.map((subField) => {
const interfaceConfig = getInterface(subField.interface);
const isFileCollection = field?.target && getCollection(field?.target)?.template === 'file';
const newSchemaName = `${schemaName}.${subField.name}`;
const schema = {
type: 'string',
name: newSchemaName,
// 'x-designer': 'FormItem.Designer',
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-read-pretty': readPretty,
'x-component-props': {
'pattern-disable': block === 'Form' && readPretty,
fieldNames: isFileCollection
? {
label: 'preview',
value: 'id',
}
: undefined,
},
'x-decorator': 'FormItem',
'x-collection-field': `${field.target}.${subField.name}`,
};
return {
name: newSchemaName,
type: 'item',
title: subField?.uiSchema?.title || subField.name,
Component: 'CollectionFieldInitializer',
remove: removeGridFormItem,
schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, {
field: subField,
block,
readPretty,
targetCollection: getCollection(field.target),
});
},
schema,
} as SchemaInitializerItemType;
});
const displayCollectionFields = {
type: 'itemGroup',
name: `${schemaName}-displayCollectionFields`,
title: t('Display fields'),
children: items,
};
const children = [displayCollectionFields];
if (depth < maxDepth) {
const subChildren = subFields
?.filter((subField) => {
return ['o2o', 'oho', 'obo', 'm2o'].includes(subField.interface);
})
.map((subField) => {
return getGroupItemForForm({
getCollectionFields,
field: subField,
getInterface,
getCollection,
schemaName: `${schemaName}.${subField.name}`,
readPretty,
block,
maxDepth,
depth: depth + 1,
t,
});
});
if (subChildren.length) {
const group: any = {
type: 'itemGroup',
name: `${schemaName}-associationFields`,
title: t('Display association fields'),
children: subChildren,
};
children.push(group);
}
}
return {
type: 'subMenu',
name: `${schemaName}.${field.name}`,
title: field.uiSchema?.title,
children,
} as SchemaInitializerItemType;
}
function useAssociationFields({
componentName,
filterCollections,
showAssociationFields,
componentNamePrefix,
}: {
componentName: string;
filterCollections: (options: { collection?: Collection; associationField?: CollectionFieldOptions }) => boolean;
componentNamePrefix: string;
showAssociationFields?: boolean;
}) {
const fieldSchema = useFieldSchema();
const { getCollectionFields } = useCollectionManager_deprecated();
const collection = useCollection_deprecated();
const cm = useCollectionManager();
const dataSource = useDataSourceKey();
const { getTemplatesByCollection } = useSchemaTemplateManager();
const { t } = useTranslation();
const compile = useCompile();
return useMemo(() => {
if (!showAssociationFields) {
return [];
}
let fields: CollectionFieldOptions[] = [];
if (fieldSchema['x-initializer']) {
fields = collection.fields;
} else {
const collection = recursiveParent(fieldSchema.parent);
if (collection) {
fields = getCollectionFields(collection);
}
}
return fields
.filter((field) => ['linkTo', 'subTable', 'o2m', 'm2m', 'obo', 'oho', 'o2o', 'm2o'].includes(field.interface))
.filter((field) => filterCollections({ associationField: field }))
.map((field, index) => {
const title = compile(field.uiSchema.title || field.name);
const templates = getTemplatesByCollection(dataSource, field.target).filter((template) => {
if (template.resourceName !== `${field.collectionName}.${field.name}`) {
return false;
}
// 针对弹窗中的详情区块
if (componentName === componentNamePrefix + 'ReadPrettyFormItem') {
if (['hasOne', 'belongsTo'].includes(field.type)) {
return template.componentName === componentNamePrefix + 'ReadPrettyFormItem';
} else {
return template.componentName === componentNamePrefix + 'Details';
}
}
return template.componentName === componentName;
});
if (!templates.length) {
return {
type: 'item',
name: `${field.collectionName}.${field.name}`,
collectionName: field.target,
title,
dataSource,
associationField: field,
};
}
return {
key: `associationFiled_${componentName}_table_subMenu_${index}`,
type: 'subMenu',
name: `${field.target}_${index}`,
title,
dataSource,
children: [
{
type: 'item',
name: `${field.collectionName}.${field.name}`,
collectionName: field.target,
dataSource,
title: t('Blank block'),
associationField: field,
},
{
type: 'divider',
},
{
key: `associationFiled_${componentName}_table_subMenu_${index}_copy`,
type: 'subMenu',
name: 'copy',
dataSource,
title: t('Duplicate template'),
children: templates.map((template) => {
const templateName = [
componentNamePrefix + 'FormItem',
componentNamePrefix + 'ReadPrettyFormItem',
].includes(template?.componentName)
? `${template?.name} ${t('(Fields only)')}`
: template?.name;
return {
type: 'item',
mode: 'copy',
name: `${field.collectionName}.${field.name}`,
collectionName: field.target,
template,
dataSource,
title: templateName || t('Untitled'),
associationField: field,
};
}),
},
{
key: `associationFiled_${componentName}_table_subMenu_${index}_ref`,
type: 'subMenu',
name: 'ref',
dataSource,
title: t('Reference template'),
children: templates.map((template) => {
const templateName = [
componentNamePrefix + 'FormItem',
componentNamePrefix + 'ReadPrettyFormItem',
].includes(template?.componentName)
? `${template?.name} ${t('(Fields only)')}`
: template?.name;
return {
type: 'item',
mode: 'reference',
name: `${field.collectionName}.${field.name}`,
collectionName: field.target,
template,
dataSource,
title: templateName || t('Untitled'),
associationField: field,
};
}),
},
],
};
});
}, [
collection.fields,
compile,
componentName,
dataSource,
fieldSchema,
filterCollections,
getCollectionFields,
getTemplatesByCollection,
showAssociationFields,
t,
componentNamePrefix,
]);
}