diff --git a/packages/core/client/.dumirc.ts b/packages/core/client/.dumirc.ts index a4c092e84e..e7e7632580 100644 --- a/packages/core/client/.dumirc.ts +++ b/packages/core/client/.dumirc.ts @@ -186,6 +186,10 @@ export default defineConfig({ title: 'ExtendCollectionsProvider', link: '/core/data-source/extend-collections-provider', }, + { + title: 'Collection Fields To Initializer Items', + link: '/core/data-source/collection-fields-to-initializer-items', + }, ] }, { diff --git a/packages/core/client/docs/en-US/core/data-source/collection-fields-to-initializer-items.md b/packages/core/client/docs/en-US/core/data-source/collection-fields-to-initializer-items.md new file mode 100644 index 0000000000..3e5df8fe82 --- /dev/null +++ b/packages/core/client/docs/en-US/core/data-source/collection-fields-to-initializer-items.md @@ -0,0 +1,339 @@ +# Collection Fields To Initializer Items + +## 介绍 + +![20240718145531](https://static-docs.nocobase.com/20240718145531.png) + +页面上有 `Configure columns` 和 `Configure fields` 两个按钮,鼠标悬浮后显示当前表的字段列表,当点击某个字段后,会插入表格列或者表单项到界面中,这个过程就是从 `Collection Fields` 到 `Initializer Items` 的过程。 + +## Configure fields 分类 + +`Configure fields` 分为三类: + +- `self collection fields`:当前表的字段 +- `parent collection fields`:父表的字段 +- `associated collection fields`:关联表的字段 + +![20240718151313](https://static-docs.nocobase.com/20240718151313.png) + +![20240718151040](https://static-docs.nocobase.com/20240718151040.png) + +## CollectionFieldsToInitializerItems + +我们将单个 `Collection Field` 转为 `Initializer Item` 的过程抽象为以下三个步骤: + +- `filter`:过滤字段 +- `getSchema`:获取字段对应的 schema +- `getInitializerItem`:获取字段对应的 initializer item + +> 关于 Schema 请查看 [UI Schema](https://docs.nocobase.com/development/client/ui-schema/what-is-ui-schema)。
+> 关于 Initializer item,可以参考 [SchemaInitializer](/core/ui-schema/schema-initializer)。
+> 两者的关系是 Initializer item 通过类似 `onClick` 的事件触发,将 Schema 插入到 Schema 树中,并渲染到界面上。 + +`CollectionFieldsToInitializerItems` 是一个组件,用于将 `Collection Fields` 转换为 `Initializer Items`。 + + +```ts +const someInitializer = new SchemaInitializer({ + // ... + items: [ + { + name: 'collectionFields', + Component: CollectionFieldsToInitializerItems, + }, + // ... + ] +}) +``` + +### Types + +```ts +interface CollectionFieldContext { + fieldSchema: ISchema; + collection?: InheritanceCollectionMixin & Collection; + dataSource: DataSource; + form: Form; + actionContext: ReturnType; + t: TFunction<"translation", undefined>; + collectionManager: CollectionManager; + dataSourceManager: DataSourceManager; + compile: (source: any, ext?: any) => any + targetCollection?: Collection; +} + +interface CommonCollectionFieldsProps { + block: string; + isReadPretty?: (context: CollectionFieldContext) => boolean; + filter?: (collectionField: CollectionFieldOptions, context: CollectionFieldContext) => boolean; + getSchema: (collectionField: CollectionFieldOptions, context: CollectionFieldContext) => CollectionFieldGetSchemaResult; + getInitializerItem?: (collectionField: CollectionFieldOptions, context: CollectionFieldContext) => CollectionFieldGetInitializerItemResult; +} + +interface SelfCollectionFieldsProps extends CommonCollectionFieldsProps {} +interface ParentCollectionFieldsProps extends CommonCollectionFieldsProps {} +interface AssociationCollectionFieldsProps extends Omit { + filterSelfField?: CommonCollectionFieldsProps['filter']; + filterAssociationField?: CommonCollectionFieldsProps['filter']; +} + +interface CollectionFieldsProps { + /** + * Block name. + */ + block: string; + selfField: Omit; + parentField?: Omit; + associationField?: Omit; +} +``` + +#### CollectionFieldsProps + +- `block`:区块名称 +- `selfField`:当前表字段配置 +- `parentField`:父表字段配置 +- `associationField`:关联表字段配置 + +#### CommonCollectionFieldsProps + +- `block`:区块名称 +- `isReadPretty`:是否为只读模式 +- `filter`:过滤字段 +- `getSchema`:获取字段对应的 schema +- `getInitializerItem`:获取字段对应的 initializer item + +##### 公共 Schema + +其中 `getSchema` 内部包含了公共的部分,所以并不要求返回整个 Schema,只需要返回差异部分即可。公共部分如下: + +```ts +const defaultSchema: CollectionFieldDefaultSchema = { + type: 'string', + title: collectionField?.uiSchema?.title || collectionField.name, + name: collectionField.name, + 'x-component': 'CollectionField', + 'x-collection-field': `${collection.name}.${collectionField.name}`, + 'x-read-pretty': collectionField?.uiSchema?.['x-read-pretty'], +}; +``` + +其中 [CollectionField](/core/data-source/collection-field) 用于动态渲染字段。 + +##### 公共 Initializer Item + +同样 `getInitializerItem` 内部包含了公共的部分,所以并不要求返回整个 Initializer Item,只需要返回 `CollectionFieldInitializer`(文档 TODO)组件对应的 `find` 和 `remove`。 + +#### AssociationCollectionFieldsProps + +- `filterSelfField`:过滤当前表字段 +- `filterAssociationField`:过滤关联表字段 + +其他属性同 `CommonCollectionFieldsProps`。 + +#### CollectionFieldContext + +- [fieldSchema](/core/ui-schema/designable#usefieldschema):当前 schema +- [collection](/core/data-source/collection):当前表 +- [dataSource](/core/data-source/data-source-provider#usedatasource):数据源 +- `form`:表单 +- [actionContext](/components/action#actioncontext):操作上下文 +- `t`:国际化 +- [collectionManager](/core/data-source/collection-manager-provider#usecollectionmanager):表管理器 +- [dataSourceManager](/core/data-source/data-source-manager-provider#usedatasourcemanager):数据源管理器 +- `compile`:编译函数 +- `targetCollection`:如果是关联表字段,表示关联表 + +### Example + +我们以 `Collection Field` 转为 `FormItem` 为例: + +#### 定义 + +```tsx | pure + +export const CollectionFieldsToFormInitializerItems: FC<{ block?: string }> = (props) => { + const block = props?.block || 'Form'; + const fieldItemSchema = { + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-decorator': 'FormItem', + }; + + const initializerItem = { + remove: removeGridFormItem, + } + return !field.treeChildren, + getSchema: (field, { targetCollection }) => { + const isFileCollection = targetCollection?.template === 'file'; + const isAssociationField = targetCollection; + const fieldNames = field?.uiSchema?.['x-component-props']?.['fieldNames']; + + return { + ...fieldItemSchema, + 'x-component-props': isFileCollection + ? { fieldNames: { label: 'preview', value: 'id' } } + : isAssociationField && fieldNames? { fieldNames: { ...fieldNames, label: targetCollection?.titleField || fieldNames.label }} + : {}, + } + }, + getInitializerItem: () => { + return { + ...initializerItem, + find: props?.block === 'Kanban' ? findKanbanFormItem : undefined + } + } + }} + parentField={{ + getSchema: () => fieldItemSchema, + getInitializerItem: () => initializerItem, + }} + associationField={{ + filterSelfField: (field) => { + if (block !== 'Form') return true; + return field?.interface === 'm2o' + }, + filterAssociationField(collectionField) { + return collectionField?.interface && !['subTable'].includes(collectionField?.interface) && !collectionField.treeChildren + }, + getSchema: () => fieldItemSchema, + getInitializerItem: () => initializerItem, + }} + /> +``` + +##### selfField + +- `filter`:过滤字段 + +`filter: (field) => !field.treeChildren` 表示过滤掉树形结构的字段。 + +因为 ? + +- `getSchema`:获取字段对应的 schema + +参考 [FormItem](/components/form-item) 以及 [Field](/components/checkbox) 文档,希望最终获得的 Schema 如下: + +```json +{ + "type": "string", + "name": "nickname", + "x-toolbar": "FormItemSchemaToolbar", + "x-settings": "fieldSettings:FormItem", + "x-component": "CollectionField", + "x-decorator": "FormItem", + "x-collection-field": "users.nickname", + "x-component-props": { + // ... + }, +} +``` + +其中 [公共部分](/core/data-source/collection-fields-initializer-items#commoncollectionfieldsprops) 如下: + +```json +{ + "type": "string", + "name": "nickname", + "x-component": "CollectionField", + "x-collection-field": "users.nickname", + "x-read-pretty": true +} +``` + +所以我们只需要返回: + +```json +{ + "x-toolbar": "FormItemSchemaToolbar", + "x-settings": "fieldSettings:FormItem", + "x-decorator": "FormItem", + "x-component-props": { + // ... + }, +} +``` + +- `getInitializerItem`:获取字段对应的 initializer item + +因为其对应的 [SchemaInitializer](/core/ui-schema/schema-initializer) 有 wrap 属性,将每个字段包裹在 `Grid` 中,方便布局和拖拽。我们在删除时则不仅需要删除自身的 Schema 还需要删除对应的 `Grid`。所以我们返回: + +```ts +{ + "remove": removeGridFormItem +} +``` + +##### parentField + +略。 + +##### associationField + +- `filterSelfField`:过滤当前表字段 + +表单这里仅需要展示多对一的关联字段,所以我们过滤掉非多对一关联字段。? + +- `filterAssociationField`:过滤关联表字段 + +同样过滤掉树形结构的字段。 + + +#### 使用 + +```diff +const formItemInitializers = new CompatibleSchemaInitializer({ + name: 'form:configureFields', + wrap: gridRowColWrap, + icon: 'SettingOutlined', + title: '{{t("Configure fields")}}', + items: [ ++ { ++ name: 'collectionFields', ++ Component: CollectionFieldsToFormInitializerItems, ++ }, + // ... + ] +}) +``` + +## CollectionFieldsToFormInitializerItems + +`CollectionFieldsToFormInitializerItems` 是 `CollectionFieldsToInitializerItems` 的一个封装,用于表单场景。 + +目前使用在了 `Form`、`List`、`Kanban`、`Grid Card` 和 `Details` 区块中。 + +```ts +const someInitializer = new SchemaInitializer({ + // ... + items: [ + { + name: 'collectionFields', + Component: CollectionFieldsToFormInitializerItems, + }, + // ... + ] +}) +``` + +## CollectionFieldsToTableInitializerItems + +`CollectionFieldsToTableInitializerItems` 是 `CollectionFieldsToInitializerItems` 的一个封装,用于表格场景。 + +目前使用在了 `Table` 和 `Gantt` 区块中。 + +```ts +const someInitializer = new SchemaInitializer({ + // ... + items: [ + { + name: 'collectionFields', + Component: CollectionFieldsToTableInitializerItems, + }, + // ... + ] +}) +``` diff --git a/packages/core/client/docs/zh-CN/core/data-source/collection-fields-to-initializer-items.md b/packages/core/client/docs/zh-CN/core/data-source/collection-fields-to-initializer-items.md new file mode 100644 index 0000000000..3108839315 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/data-source/collection-fields-to-initializer-items.md @@ -0,0 +1,309 @@ +# Collection Fields To Initializer Items + +## 介绍 + +![20240718145531](https://static-docs.nocobase.com/20240718145531.png) + +页面上有 `Configure columns` 和 `Configure fields` 两个按钮,点击后显示当前表的字段列表,当点击某个字段后,会插入表单项或者表格列到界面中,这个过程就是从 `Collection Fields` 到 `Initializer Items` 的过程。 + +## Configure fields 分类 + +`Configure fields` 分为三类: + +- `self collection fields`:当前表的字段 +- `parent collection fields`:父表的字段 +- `associated collection fields`:关联表的字段 + +![20240718151313](https://static-docs.nocobase.com/20240718151313.png) + +![20240718151040](https://static-docs.nocobase.com/20240718151040.png) + +## CollectionFieldsToInitializerItems + +我们将单个 `Collection Field` 转为 `Initializer Item` 的过程抽象以下三个步骤: + +- `filter`:过滤字段 +- `getSchema`:获取字段对应的 schema +- `getInitializerItem`:获取字段对应的 initializer item + +> 关于 Schema 请查看 [UI Schema](https://docs.nocobase.com/development/client/ui-schema/what-is-ui-schema)。
+> 关于 Initializer item,可以参考 [SchemaInitializer](/core/ui-schema/schema-initializer)。
+> 两者的关系是 Initializer item 通过类似 `onClick` 的事件触发,将 Schema 插入到 Schema 树中,并渲染到界面上。 + +`CollectionFieldsToInitializerItems` 是一个组件,用于将 `Collection Fields` 转换为 `Initializer Items`。 + +### Types + +```ts +interface CollectionFieldContext { + fieldSchema: ISchema; + collection?: InheritanceCollectionMixin & Collection; + dataSource: DataSource; + form: Form; + actionContext: ReturnType; + t: TFunction<"translation", undefined>; + collectionManager: CollectionManager; + dataSourceManager: DataSourceManager; + compile: (source: any, ext?: any) => any + targetCollection?: Collection; +} + +interface CommonCollectionFieldsProps { + block: string; + isReadPretty?: (context: CollectionFieldContext) => boolean; + filter?: (collectionField: CollectionFieldOptions, context: CollectionFieldContext) => boolean; + getSchema: (collectionField: CollectionFieldOptions, context: CollectionFieldContext & { + defaultSchema: CollectionFieldDefaultSchema + targetCollection?: Collection + collectionFieldInterface?: CollectionFieldInterface + }) => CollectionFieldGetSchemaResult; + getInitializerItem?: (collectionField: CollectionFieldOptions, context: CollectionFieldContext & { + schema: ISchema; + defaultInitializerItem: CollectionFieldDefaultInitializerItem; + targetCollection?: Collection + collectionFieldInterface?: CollectionFieldInterface + }) => CollectionFieldGetInitializerItemResult; +} + +interface SelfCollectionFieldsProps extends CommonCollectionFieldsProps {} +interface ParentCollectionFieldsProps extends CommonCollectionFieldsProps {} +interface AssociationCollectionFieldsProps extends Omit { + filterSelfField?: CommonCollectionFieldsProps['filter']; + filterAssociationField?: CommonCollectionFieldsProps['filter']; +} + +interface CollectionFieldsProps { + /** + * Block name. + */ + block: string; + selfField: Omit; + parentField?: Omit; + associationField?: Omit; +} +``` + +#### CollectionFieldsProps + +- `block`:区块名称 +- `selfField`:当前表字段配置 +- `parentField`:父表字段配置 +- `associationField`:关联表字段配置 + +#### CommonCollectionFieldsProps + +- `block`:区块名称 +- `isReadPretty`:是否为只读模式 +- `filter`:过滤字段 +- `getSchema`:获取字段对应的 schema +- `getInitializerItem`:获取字段对应的 initializer item + +##### 公共 Schema + +其中 `getSchema` 内部包含了公共的部分,所以并不要求返回整个 Schema,只需要返回差异部分即可。公共部分如下: + +```ts +const defaultSchema: CollectionFieldDefaultSchema = { + type: 'string', + title: collectionField?.uiSchema?.title || collectionField.name, + name: collectionField.name, + 'x-component': 'CollectionField', + 'x-collection-field': `${collection.name}.${collectionField.name}`, + 'x-read-pretty': collectionField?.uiSchema?.['x-read-pretty'], +}; +``` + +其中 [CollectionField](/core/data-source/collection-field) 用于动态渲染字段。 + +##### 公共 Initializer Item + +同样 `getInitializerItem` 内部包含了公共的部分,所以并不要求返回整个 Initializer Item,只需要返回 `CollectionFieldInitializer`(文档 TODO)组件对应的 `find` 和 `remove`。 + +#### AssociationCollectionFieldsProps + +- `filterSelfField`:过滤当前表字段 +- `filterAssociationField`:过滤关联表字段 + +其他属性同 `CommonCollectionFieldsProps`。 + +#### CollectionFieldContext + +- [fieldSchema](/core/ui-schema/designable#usefieldschema):当前 schema +- [collection](/core/data-source/collection):当前表 +- [dataSource](/core/data-source/data-source-provider#usedatasource):数据源 +- `form`:表单 +- [actionContext](/components/action#actioncontext):操作上下文 +- `t`:国际化 +- [collectionManager](/core/data-source/collection-manager-provider#usecollectionmanager):表管理器 +- [dataSourceManager](/core/data-source/data-source-manager-provider#usedatasourcemanager):数据源管理器 +- `compile`:编译函数 +- `targetCollection`:如果是关联表字段,表示关联表 + +### Example + +我们以 `Collection Field` 转为 `FormItem` 为例: + +#### 定义 + +```tsx | pure + +export const CollectionFieldsToFormInitializerItems: FC<{ block?: string }> = (props) => { + const block = props?.block || 'Form'; + const fieldItemSchema = { + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-decorator': 'FormItem', + }; + + const initializerItem = { + remove: removeGridFormItem, + } + return !field.treeChildren, + getSchema: (field, { targetCollection }) => { + const isFileCollection = targetCollection?.template === 'file'; + const isAssociationField = targetCollection; + const fieldNames = field?.uiSchema?.['x-component-props']?.['fieldNames']; + + return { + ...fieldItemSchema, + 'x-component-props': isFileCollection + ? { fieldNames: { label: 'preview', value: 'id' } } + : isAssociationField && fieldNames? { fieldNames: { ...fieldNames, label: targetCollection?.titleField || fieldNames.label }} + : {}, + } + }, + getInitializerItem: () => { + return { + ...initializerItem, + find: props?.block === 'Kanban' ? findKanbanFormItem : undefined + } + } + }} + parentField={{ + getSchema: () => fieldItemSchema, + getInitializerItem: () => initializerItem, + }} + associationField={{ + filterSelfField: (field) => { + if (block !== 'Form') return true; + return field?.interface === 'm2o' + }, + filterAssociationField(collectionField) { + return collectionField?.interface && !['subTable'].includes(collectionField?.interface) && !collectionField.treeChildren + }, + getSchema: () => fieldItemSchema, + getInitializerItem: () => initializerItem, + }} + /> +``` + +##### selfField + +- `filter`:过滤字段 + +`filter: (field) => !field.treeChildren` 表示过滤掉树形结构的字段。 + +因为 ? + +- `getSchema`:获取字段对应的 schema + +参考 [FormItem](/components/form-item) 以及 [Field](/components/checkbox) 文档,希望最终获得的 Schema 如下: + +```json +{ + "type": "string", + "name": "nickname", + "x-toolbar": "FormItemSchemaToolbar", + "x-settings": "fieldSettings:FormItem", + "x-component": "CollectionField", + "x-decorator": "FormItem", + "x-collection-field": "users.nickname", + "x-component-props": { + // ... + }, +} +``` + +其中 [公共部分](/core/data-source/collection-fields-initializer-items#commoncollectionfieldsprops) 如下: + +```json +{ + "type": "string", + "name": "nickname", + "x-component": "CollectionField", + "x-collection-field": "users.nickname", + "x-read-pretty": true +} +``` + +所以我们只需要返回: + +```json +{ + "x-toolbar": "FormItemSchemaToolbar", + "x-settings": "fieldSettings:FormItem", + "x-decorator": "FormItem", + "x-component-props": { + // ... + }, +} +``` + +- `getInitializerItem`:获取字段对应的 initializer item + +因为其对应的 [SchemaInitializer](/core/ui-schema/schema-initializer) 有 wrap 属性,将每个字段包裹在 `Grid` 中,方便布局和拖拽。我们在删除时则不仅需要删除自身的 Schema 还需要删除对应的 `Grid`。所以我们返回: + +```ts +{ + "remove": removeGridFormItem +} +``` + +##### parentField + +略。 + +##### associationField + +- `filterSelfField`:过滤当前表字段 + +表单这里仅需要展示多对一的关联字段,所以我们过滤掉非多对一关联字段。? + +- `filterAssociationField`:过滤关联表字段 + +同样过滤掉树形结构的字段。 + + +#### 使用 + +```diff +const formItemInitializers = new CompatibleSchemaInitializer({ + name: 'form:configureFields', + wrap: gridRowColWrap, + icon: 'SettingOutlined', + title: '{{t("Configure fields")}}', + items: [ ++ { ++ name: 'collectionFields', ++ Component: CollectionFieldsToFormInitializerItems, ++ }, + // ... + ] +}) +``` + +## CollectionFieldsToFormInitializerItems + +`CollectionFieldsToFormInitializerItems` 是 `CollectionFieldsToInitializerItems` 的一个封装,用于表单场景。 + +目前使用在了 `Form`、`List`、`Kanban`、`Grid Card` 和 `Details` 区块中。 + +## CollectionFieldsToTableInitializerItems + +`CollectionFieldsToTableInitializerItems` 是 `CollectionFieldsToInitializerItems` 的一个封装,用于表格场景。 + +目前使用在了 `Table` 和 `Gantt` 区块中。 + diff --git a/packages/core/client/src/data-source/collection-fields-to-initializer-items/CollectionFieldsToFormInitializerItems.tsx b/packages/core/client/src/data-source/collection-fields-to-initializer-items/CollectionFieldsToFormInitializerItems.tsx new file mode 100644 index 0000000000..e3b20df7a3 --- /dev/null +++ b/packages/core/client/src/data-source/collection-fields-to-initializer-items/CollectionFieldsToFormInitializerItems.tsx @@ -0,0 +1,78 @@ +/** + * 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 React, { FC } from 'react'; +import { Schema } from '@formily/json-schema'; +import { CollectionFieldsToInitializerItems } from './CollectionFieldsToInitializerItems'; +import { removeGridFormItem, findSchema } from '../../schema-initializer/utils'; + +export const findKanbanFormItem = (schema: Schema, key: string, action: string) => { + const s = findSchema(schema, 'x-component', 'Kanban'); + return findSchema(s, key, action); +}; + +export const CollectionFieldsToFormInitializerItems: FC<{ block?: string }> = (props) => { + const block = props?.block || 'Form'; + const fieldItemSchema = { + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-decorator': 'FormItem', + }; + + const initializerItem = { + remove: removeGridFormItem, + }; + return ( + !field.treeChildren, + getSchema: (field, { targetCollection }) => { + const isFileCollection = targetCollection?.template === 'file'; + const isAssociationField = targetCollection; + const fieldNames = field?.uiSchema?.['x-component-props']?.['fieldNames']; + + return { + ...fieldItemSchema, + 'x-component-props': isFileCollection + ? { fieldNames: { label: 'preview', value: 'id' } } + : isAssociationField && fieldNames + ? { fieldNames: { ...fieldNames, label: targetCollection?.titleField || fieldNames.label } } + : {}, + }; + }, + getInitializerItem: () => { + return { + ...initializerItem, + find: props?.block === 'Kanban' ? findKanbanFormItem : undefined, + }; + }, + }} + parentField={{ + getSchema: () => fieldItemSchema, + getInitializerItem: () => initializerItem, + }} + associationField={{ + filterSelfField: (field) => { + if (block !== 'Form') return true; + return field?.interface === 'm2o'; + }, + filterAssociationField(collectionField) { + return ( + collectionField?.interface && + !['subTable'].includes(collectionField?.interface) && + !collectionField.treeChildren + ); + }, + getSchema: () => fieldItemSchema, + getInitializerItem: () => initializerItem, + }} + /> + ); +}; diff --git a/packages/core/client/src/data-source/collection-fields-to-initializer-items/CollectionFieldsToInitializerItems.tsx b/packages/core/client/src/data-source/collection-fields-to-initializer-items/CollectionFieldsToInitializerItems.tsx new file mode 100644 index 0000000000..631cb797c8 --- /dev/null +++ b/packages/core/client/src/data-source/collection-fields-to-initializer-items/CollectionFieldsToInitializerItems.tsx @@ -0,0 +1,37 @@ +/** + * 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 React, { FC } from 'react'; +import { CollectionFieldsProps, useCollectionFieldContext } from './utils'; +import { AssociationCollectionFields, ParentCollectionFields, SelfFields } from './items'; + +export const CollectionFieldsToInitializerItems: FC = (props) => { + const { selfField, parentField, associationField, block } = props; + const context = useCollectionFieldContext(); + if (!context.collection) return null; + return ( + <> + + {parentField && ( + + )} + {associationField && ( + + )} + + ); +}; diff --git a/packages/core/client/src/data-source/collection-fields-to-initializer-items/CollectionFieldsToTableInitializerItems.tsx b/packages/core/client/src/data-source/collection-fields-to-initializer-items/CollectionFieldsToTableInitializerItems.tsx new file mode 100644 index 0000000000..7c813b2677 --- /dev/null +++ b/packages/core/client/src/data-source/collection-fields-to-initializer-items/CollectionFieldsToTableInitializerItems.tsx @@ -0,0 +1,127 @@ +/** + * 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 React, { FC } from 'react'; +import { CollectionFieldsToInitializerItems } from './CollectionFieldsToInitializerItems'; +import { findTableColumn, removeGridFormItem, removeTableColumn } from '../../schema-initializer/utils'; + +const quickEditField = [ + 'attachment', + 'textarea', + 'markdown', + 'json', + 'richText', + 'polygon', + 'circle', + 'point', + 'lineString', +]; + +export const CollectionFieldsToTableInitializerItems: FC = (props) => { + function isReadPretty({ fieldSchema, form }) { + const isSubTable = fieldSchema['x-component'] === 'AssociationField.SubTable'; + const isReadPretty = isSubTable ? form.readPretty : true; + + return isReadPretty; + } + return ( + field.interface !== 'subTable' && !field.treeChildren, + getSchema: (field, { targetCollection, fieldSchema, form }) => { + const isFileCollection = targetCollection?.template === 'file'; + const isPreviewComponent = field.uiSchema?.['x-component'] === 'Preview'; + const isSubTable = fieldSchema['x-component'] === 'AssociationField.SubTable'; + const readPretty = isReadPretty({ fieldSchema, form }); + + return { + 'x-component-props': isFileCollection + ? { + fieldNames: { + label: 'preview', + value: 'id', + }, + } + : isPreviewComponent + ? { size: 'small' } + : {}, + 'x-read-pretty': readPretty || field.uiSchema?.['x-read-pretty'], + 'x-decorator': isSubTable + ? quickEditField.includes(field.interface) || isFileCollection + ? 'QuickEdit' + : 'FormItem' + : null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }; + }, + getInitializerItem: () => { + return { + find: findTableColumn, + remove: removeTableColumn, + }; + }, + }} + parentField={{ + isReadPretty, + getSchema(field, { targetCollection, fieldSchema, form }) { + const isFileCollection = targetCollection?.template === 'file'; + const isSubTable = fieldSchema['x-component'] === 'AssociationField.SubTable'; + const readPretty = isReadPretty({ fieldSchema, form }); + + return { + 'x-component-props': isFileCollection + ? { + fieldNames: { + label: 'preview', + value: 'id', + }, + } + : {}, + 'x-read-pretty': readPretty || field.uiSchema?.['x-read-pretty'], + 'x-decorator': isSubTable + ? quickEditField.includes(field.interface) || isFileCollection + ? 'QuickEdit' + : 'FormItem' + : null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }; + }, + getInitializerItem() { + return { + remove: removeGridFormItem, + }; + }, + }} + associationField={{ + filterAssociationField(collectionField) { + return !['subTable'].includes(collectionField.interface) && !collectionField.treeChildren; + }, + getSchema() { + return {}; + }, + getInitializerItem() { + return { + find: findTableColumn, + remove: removeTableColumn, + }; + }, + }} + /> + ); +}; diff --git a/packages/core/client/src/data-source/collection-fields-to-initializer-items/index.ts b/packages/core/client/src/data-source/collection-fields-to-initializer-items/index.ts new file mode 100644 index 0000000000..ac04d9928e --- /dev/null +++ b/packages/core/client/src/data-source/collection-fields-to-initializer-items/index.ts @@ -0,0 +1,12 @@ +/** + * 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. + */ + +export * from './CollectionFieldsToInitializerItems'; +export * from './CollectionFieldsToFormInitializerItems'; +export * from './CollectionFieldsToTableInitializerItems'; diff --git a/packages/core/client/src/data-source/collection-fields-to-initializer-items/items/AssociationCollectionFields.tsx b/packages/core/client/src/data-source/collection-fields-to-initializer-items/items/AssociationCollectionFields.tsx new file mode 100644 index 0000000000..6e0462e6af --- /dev/null +++ b/packages/core/client/src/data-source/collection-fields-to-initializer-items/items/AssociationCollectionFields.tsx @@ -0,0 +1,73 @@ +import React, { FC } from 'react'; + +import { InheritanceCollectionMixin } from '../../../collection-manager'; +import { AssociationCollectionFieldsProps, getInitializerItemsByFields } from '../utils'; +import { + SchemaInitializerChildren, + SchemaInitializerItemGroup, + SchemaInitializerItemType, +} from '../../../application/schema-initializer'; + +export const AssociationCollectionFields: FC = (props) => { + const { filterAssociationField, filterSelfField = () => true, getSchema, ...otherProps } = props; + const { collection, t, collectionManager } = props.context; + const fields = collection.getFields(); + const associationInterfaces = ['o2o', 'oho', 'obo', 'm2o']; // 关联字段类型 + const associationFields = fields + .filter((field) => { + return associationInterfaces.includes(field.interface); + }) + .filter((field) => filterSelfField(field, props.context)); + + if (!associationFields.length) return null; + + const children = associationFields + .map((associationField) => { + // 获取关联表 + const associationCollection = collectionManager.getCollection( + associationField.target!, + )!; + if (!associationCollection) return null; + // 获取父表 + const associationCollectionFields = associationCollection?.getAllFields(); + if (!associationCollectionFields.length) return null; + return { associationField, associationCollection, associationCollectionFields }; + }) + .filter(Boolean) + // 修改数据结构 + .map(({ associationField, associationCollection, associationCollectionFields }: any) => { + const newContext = { + ...props.context, + collection: associationCollection, + }; + + const getAssociationFieldSchema: AssociationCollectionFieldsProps['getSchema'] = (field, context) => { + const schema = getSchema(field, context); + return { + ...(schema || {}), + 'x-read-pretty': true, + name: `${associationField.name}.${field.name}`, + 'x-collection-field': `${collection.name}.${associationField.name}.${field.name}`, + }; + }; + + return { + type: 'subMenu', + name: associationField.uiSchema?.title, + title: associationField.uiSchema?.title, + children: getInitializerItemsByFields( + { + ...otherProps, + filter: filterAssociationField, + getSchema: getAssociationFieldSchema, + }, + associationCollectionFields!, + newContext, + ), + } as SchemaInitializerItemType; + }); + + if (!children.length) return null; + + return {children}; +}; diff --git a/packages/core/client/src/data-source/collection-fields-to-initializer-items/items/ParentCollectionFields.tsx b/packages/core/client/src/data-source/collection-fields-to-initializer-items/items/ParentCollectionFields.tsx new file mode 100644 index 0000000000..f3fa849281 --- /dev/null +++ b/packages/core/client/src/data-source/collection-fields-to-initializer-items/items/ParentCollectionFields.tsx @@ -0,0 +1,47 @@ +import React, { FC } from 'react'; + +import { CollectionFieldOptions } from '../../collection/Collection'; +import { InheritanceCollectionMixin } from '../../../collection-manager'; +import { ParentCollectionFieldsProps, getInitializerItemsByFields, useCollectionFieldContext } from '../utils'; +import { SchemaInitializerChildren, SchemaInitializerItemType } from '../../../application/schema-initializer'; + +export const ParentCollectionFields: FC = (props) => { + const context = useCollectionFieldContext(); + const { collection, t, collectionManager } = context; + + const parentCollectionNames = collection.getParentCollectionsName(); + if (!parentCollectionNames.length) return null; + + const children = parentCollectionNames + .map((parentCollectionName) => { + // 获取父表的字段 + const parentCollectionFields = collection.getParentCollectionFields(parentCollectionName); + // 如果没有父表字段,返回 null + if (parentCollectionFields.length === 0) return null; + // 获取父表 + const parentCollection = collectionManager.getCollection(parentCollectionName)!; + return { parentCollection, parentCollectionFields }; + }) + // 过滤掉 null + .filter(Boolean) + // 修改数据结构 + .map((options) => { + const { parentCollection, parentCollectionFields } = options as { + parentCollection: InheritanceCollectionMixin; + parentCollectionFields: CollectionFieldOptions[]; + }; + const newContext = { + ...context, + collection: parentCollection, + }; + + return { + type: 'itemGroup', + divider: true, + title: t(`Parent collection fields`) + '(' + context.compile(parentCollection.title) + ')', + children: getInitializerItemsByFields(props, parentCollectionFields, newContext), + } as SchemaInitializerItemType; + }); + + return {children}; +}; diff --git a/packages/core/client/src/data-source/collection-fields-to-initializer-items/items/SelfFields.tsx b/packages/core/client/src/data-source/collection-fields-to-initializer-items/items/SelfFields.tsx new file mode 100644 index 0000000000..2fc3423ccd --- /dev/null +++ b/packages/core/client/src/data-source/collection-fields-to-initializer-items/items/SelfFields.tsx @@ -0,0 +1,13 @@ +import React, { FC } from 'react'; + +import { SelfCollectionFieldsProps, getInitializerItemsByFields, useCollectionFieldContext } from '../utils'; +import { SchemaInitializerItemGroup } from '../../../application/schema-initializer'; + +export const SelfFields: FC = (props) => { + const callbackContext = useCollectionFieldContext(); + const { t, collection } = callbackContext; + const fields = collection.getFields(); + const children = getInitializerItemsByFields(props, fields, callbackContext); + + return {children}; +}; diff --git a/packages/core/client/src/data-source/collection-fields-to-initializer-items/items/index.ts b/packages/core/client/src/data-source/collection-fields-to-initializer-items/items/index.ts new file mode 100644 index 0000000000..9de9a0ec64 --- /dev/null +++ b/packages/core/client/src/data-source/collection-fields-to-initializer-items/items/index.ts @@ -0,0 +1,3 @@ +export { SelfFields } from './SelfFields'; +export { ParentCollectionFields } from './ParentCollectionFields'; +export { AssociationCollectionFields } from './AssociationCollectionFields'; diff --git a/packages/core/client/src/data-source/collection-fields-to-initializer-items/utils/getInitializerItemsByFields.ts b/packages/core/client/src/data-source/collection-fields-to-initializer-items/utils/getInitializerItemsByFields.ts new file mode 100644 index 0000000000..42f1081c5c --- /dev/null +++ b/packages/core/client/src/data-source/collection-fields-to-initializer-items/utils/getInitializerItemsByFields.ts @@ -0,0 +1,112 @@ +/** + * 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 { + CollectionFieldDefaultSchema, + CollectionFieldDefaultInitializerItem, + CommonCollectionFieldsProps, +} from './type'; +import { CollectionFieldOptions } from '../../collection/Collection'; +import { CollectionFieldContext } from './useCollectionFieldContext'; +import { ISchema } from '@formily/json-schema'; +import { SchemaInitializerItemType } from '../../../application/schema-initializer'; + +export function getInitializerItemsByFields( + props: CommonCollectionFieldsProps, + fields: CollectionFieldOptions[], + context: CollectionFieldContext, +) { + const { + block, + isReadPretty = ({ form }) => form.readPretty, + filter = () => true, + getInitializerItem = () => ({}), + getSchema = () => ({}), + } = props; + + const { collectionManager, collection, dataSourceManager, actionContext } = context; + const action = actionContext.fieldSchema?.['x-action']; + if (!collection) return []; + return fields + .map((collectionField) => { + const targetCollection = collectionManager.getCollection(collectionField.target!); + const collectionFieldInterface = dataSourceManager.collectionFieldInterfaceManager.getFieldInterface( + collectionField.interface, + ); + return { + collectionField, + context: { + ...context, + targetCollection, + collectionFieldInterface, + }, + }; + }) + .filter(({ collectionField }) => collectionField.interface) + .filter(({ collectionField, context }) => { + return filter(collectionField, context); + }) + .map(({ collectionField, context }) => { + const defaultSchema: CollectionFieldDefaultSchema = { + type: 'string', + title: collectionField?.uiSchema?.title || collectionField.name, + name: collectionField.name, + 'x-component': 'CollectionField', + 'x-collection-field': `${collection.name}.${collectionField.name}`, + 'x-read-pretty': collectionField?.uiSchema?.['x-read-pretty'], + }; + const customSchema = getSchema(collectionField, { ...context, defaultSchema: defaultSchema }); + const schema = { + ...defaultSchema, + ...(customSchema || {}), + }; + return { + collectionField, + schema, + context: { + ...context, + schema, + }, + }; + }) + .map(({ collectionField, context }) => { + const defaultInitializerItem = { + type: 'item', + name: collectionField.name, + title: collectionField?.uiSchema?.title || collectionField.name, + Component: 'CollectionFieldInitializer', + schemaInitialize: (s: ISchema) => { + context.collectionFieldInterface?.schemaInitialize?.(s, { + field: collectionField, + block, + readPretty: isReadPretty?.(context), + action, + targetCollection: context.targetCollection, + }); + }, + schema: context.schema, + } as CollectionFieldDefaultInitializerItem; + return { + collectionField, + context, + defaultInitializerItem, + }; + }) + .map(({ collectionField, defaultInitializerItem, context }) => { + const customInitializerItem = getInitializerItem(collectionField, { + ...context, + defaultInitializerItem, + }); + + return { + ...defaultInitializerItem, + ...(customInitializerItem || {}), + } as SchemaInitializerItemType; + }); +} diff --git a/packages/core/client/src/data-source/collection-fields-to-initializer-items/utils/index.ts b/packages/core/client/src/data-source/collection-fields-to-initializer-items/utils/index.ts new file mode 100644 index 0000000000..a68d3561cf --- /dev/null +++ b/packages/core/client/src/data-source/collection-fields-to-initializer-items/utils/index.ts @@ -0,0 +1,3 @@ +export * from './type'; +export * from './getInitializerItemsByFields'; +export * from './useCollectionFieldContext'; diff --git a/packages/core/client/src/data-source/collection-fields-to-initializer-items/utils/type.ts b/packages/core/client/src/data-source/collection-fields-to-initializer-items/utils/type.ts new file mode 100644 index 0000000000..6bca81c568 --- /dev/null +++ b/packages/core/client/src/data-source/collection-fields-to-initializer-items/utils/type.ts @@ -0,0 +1,135 @@ +/** + * 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 { ISchema, Schema } from '@formily/json-schema'; + +import { CollectionFieldContext } from './useCollectionFieldContext'; +import { CollectionFieldInterface } from '../../collection-field-interface'; +import { Collection, CollectionFieldOptions } from '../../collection/Collection'; +import { InheritanceCollectionMixin } from '../../../collection-manager'; + +export interface CollectionFieldDefaultSchema { + /** + * @default 'string' + */ + type: string; + /** + * @default collectionField.name + */ + name: string; + /** + * @default 'CollectionField' + */ + 'x-component': string; + /** + * @default `${collection.name}.${collectionField.name}` + */ + 'x-collection-field': string; + /** + * @default collectionField?.uiSchema?.['x-read-pretty'] + */ + 'x-read-pretty'?: boolean; + + /** + * @default collectionField?.uiSchema?.title || collectionField.name + */ + title: string; +} + +export interface CollectionFieldGetSchemaResult { + 'x-toolbar'?: string; + 'x-toolbar-props'?: any; + 'x-settings'?: string; + 'x-decorator'?: string; + 'x-decorator-props'?: any; + 'x-component-props'?: any; + 'x-use-component-props'?: string; +} + +export interface CollectionFieldDefaultInitializerItem { + /** + * @default 'item' + */ + type: string; + /** + * @default collectionField.name + */ + name: string; + /** + * @default collectionField?.uiSchema?.title || collectionField.name + */ + title: string; + /** + * @default 'CollectionFieldInitializer' + */ + Component: string; + schemaInitialize: (s: ISchema) => void; + /** + * @default schema + */ + schema: ISchema; +} + +export interface CollectionFieldGetInitializerItemResult { + find?: (schema: Schema, key: string, action: string) => any; + remove?: (schema: Schema, cb: (schema: Schema, stopProps: Record) => void) => void; +} + +export interface CommonCollectionFieldsProps { + block: string; + getSchema: ( + collectionField: CollectionFieldOptions, + context: CollectionFieldContext & { + defaultSchema: CollectionFieldDefaultSchema; + targetCollection?: Collection; + collectionFieldInterface?: CollectionFieldInterface; + }, + ) => CollectionFieldGetSchemaResult; + isReadPretty?: (context: CollectionFieldContext) => boolean; + filter?: (collectionField: CollectionFieldOptions, context: CollectionFieldContext) => boolean; + getInitializerItem?: ( + collectionField: CollectionFieldOptions, + context: CollectionFieldContext & { + schema: ISchema; + defaultInitializerItem: CollectionFieldDefaultInitializerItem; + targetCollection?: Collection; + collectionFieldInterface?: CollectionFieldInterface; + }, + ) => CollectionFieldGetInitializerItemResult; +} + +export interface SelfCollectionFieldsProps extends CommonCollectionFieldsProps { + context: Omit & { + collection: Collection; + }; +} + +export interface ParentCollectionFieldsProps extends CommonCollectionFieldsProps { + context: Omit & { + collection: Collection; + }; +} + +export interface AssociationCollectionFieldsProps extends Omit { + filterSelfField?: CommonCollectionFieldsProps['filter']; + filterAssociationField?: CommonCollectionFieldsProps['filter']; + context: Omit & { + collection: CollectionFieldContext['collection']; // 之前是可选的,这里是必须的 + }; +} + +export interface CollectionFieldsProps { + /** + * Block name. + */ + block: string; + selfField: Omit; + parentField?: Omit; + associationField?: Omit; +} diff --git a/packages/core/client/src/data-source/collection-fields-to-initializer-items/utils/useCollectionFieldContext.ts b/packages/core/client/src/data-source/collection-fields-to-initializer-items/utils/useCollectionFieldContext.ts new file mode 100644 index 0000000000..1503eefbb6 --- /dev/null +++ b/packages/core/client/src/data-source/collection-fields-to-initializer-items/utils/useCollectionFieldContext.ts @@ -0,0 +1,62 @@ +/** + * 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 { Form } from '@formily/core'; +import { useFieldSchema, useForm } from '@formily/react'; +import { ISchema } from '@formily/json-schema'; +import { TFunction, useTranslation } from 'react-i18next'; + +import { DataSource } from '../../data-source/DataSource'; +import { useCollection } from '../../collection/CollectionProvider'; +import { useDataSource } from '../../data-source/DataSourceProvider'; +import { CollectionManager } from '../../collection/CollectionManager'; +import { DataSourceManager } from '../../data-source/DataSourceManager'; +import { useActionContext } from '../../../schema-component/antd/action'; +import { Collection } from '../../collection/Collection'; +import { useCollectionManager } from '../../collection/CollectionManagerProvider'; +import { useDataSourceManager } from '../../data-source/DataSourceManagerProvider'; +import { InheritanceCollectionMixin } from '../../../collection-manager'; +import { useCompile } from '../../../schema-component'; + +export interface CollectionFieldContext { + fieldSchema: ISchema; + collection?: InheritanceCollectionMixin & Collection; + dataSource: DataSource; + form: Form; + actionContext: ReturnType; + t: TFunction<'translation', undefined>; + collectionManager: CollectionManager; + dataSourceManager: DataSourceManager; + compile: (source: any, ext?: any) => any; + targetCollection?: Collection; +} + +export function useCollectionFieldContext(): CollectionFieldContext { + const { t } = useTranslation(); + const collection = useCollection(); + const dataSourceManager = useDataSourceManager(); + const actionContext = useActionContext(); + const dataSource = useDataSource(); + const form = useForm(); + const fieldSchema = useFieldSchema(); + const collectionManager = useCollectionManager(); + const compile = useCompile(); + + return { + t, + compile, + actionContext, + fieldSchema, + collection, + dataSource, + form, + collectionManager, + dataSourceManager, + }; +} diff --git a/packages/core/client/src/data-source/index.ts b/packages/core/client/src/data-source/index.ts index 5bd59b6cce..788809ebb6 100644 --- a/packages/core/client/src/data-source/index.ts +++ b/packages/core/client/src/data-source/index.ts @@ -16,3 +16,4 @@ export * from './data-block'; export * from './data-source'; export * from './collection-record'; export * from './utils'; +export * from './collection-fields-to-initializer-items'; diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts index 323cec2496..a8676a498d 100644 --- a/packages/core/client/src/schema-initializer/utils.ts +++ b/packages/core/client/src/schema-initializer/utils.ts @@ -714,7 +714,7 @@ export const useCustomFormItemInitializerFields = (options?: any) => { }); }; -const findSchema = (schema: Schema, key: string, action: string) => { +export const findSchema = (schema: Schema, key: string, action: string) => { if (!Schema.isSchemaInstance(schema)) return null; return schema.reduceProperties((buf, s) => { if (s[key] === action) {