refactor: collection fields to initializer items (#4900)

* refactor: add collection-fields-initializer-items

* fix: bug

* fix: bug

* fix: rename and docs

* fix: change dir

* docs: imporve doc

* Update packages/core/client/docs/en-US/core/data-source/collection-fields-to-initializer-items.md

Co-authored-by: Zeke Zhang <958414905@qq.com>

* Update packages/core/client/docs/en-US/core/data-source/collection-fields-to-initializer-items.md

Co-authored-by: Zeke Zhang <958414905@qq.com>

---------

Co-authored-by: Zeke Zhang <958414905@qq.com>
This commit is contained in:
jack zhang 2024-07-22 11:25:12 +08:00 committed by GitHub
parent ef06db3eb7
commit 1be5b2f578
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 1356 additions and 1 deletions

View File

@ -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',
},
]
},
{

View File

@ -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)。 <br />
> 关于 Initializer item可以参考 [SchemaInitializer](/core/ui-schema/schema-initializer)。<br />
> 两者的关系是 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<any>;
actionContext: ReturnType<typeof useActionContext>;
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<CommonCollectionFieldsProps, 'filter'> {
filterSelfField?: CommonCollectionFieldsProps['filter'];
filterAssociationField?: CommonCollectionFieldsProps['filter'];
}
interface CollectionFieldsProps {
/**
* Block name.
*/
block: string;
selfField: Omit<SelfCollectionFieldsProps, 'block' | 'context'>;
parentField?: Omit<ParentCollectionFieldsProps, 'block' | 'context'>;
associationField?: Omit<AssociationCollectionFieldsProps, 'block' | 'context'>;
}
```
#### 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 <CollectionFieldsToInitializerItems
block={block}
selfField={{
filter: (field) => !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,
},
// ...
]
})
```

View File

@ -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)。 <br />
> 关于 Initializer item可以参考 [SchemaInitializer](/core/ui-schema/schema-initializer)。<br />
> 两者的关系是 Initializer item 通过类似 `onClick` 的事件触发,将 Schema 插入到 Schema 树中,并渲染到界面上。
`CollectionFieldsToInitializerItems` 是一个组件,用于将 `Collection Fields` 转换为 `Initializer Items`
### Types
```ts
interface CollectionFieldContext {
fieldSchema: ISchema;
collection?: InheritanceCollectionMixin & Collection;
dataSource: DataSource;
form: Form<any>;
actionContext: ReturnType<typeof useActionContext>;
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<CommonCollectionFieldsProps, 'filter'> {
filterSelfField?: CommonCollectionFieldsProps['filter'];
filterAssociationField?: CommonCollectionFieldsProps['filter'];
}
interface CollectionFieldsProps {
/**
* Block name.
*/
block: string;
selfField: Omit<SelfCollectionFieldsProps, 'block' | 'context'>;
parentField?: Omit<ParentCollectionFieldsProps, 'block' | 'context'>;
associationField?: Omit<AssociationCollectionFieldsProps, 'block' | 'context'>;
}
```
#### 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 <CollectionFieldsToInitializerItems
block={block}
selfField={{
filter: (field) => !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` 区块中。

View File

@ -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 (
<CollectionFieldsToInitializerItems
block={block}
selfField={{
filter: (field) => !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,
}}
/>
);
};

View File

@ -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<CollectionFieldsProps> = (props) => {
const { selfField, parentField, associationField, block } = props;
const context = useCollectionFieldContext();
if (!context.collection) return null;
return (
<>
<SelfFields block={block} {...selfField} context={{ ...context, collection: context.collection }} />
{parentField && (
<ParentCollectionFields
block={block}
{...parentField}
context={{ ...context, collection: context.collection }}
/>
)}
{associationField && (
<AssociationCollectionFields
block={block}
{...associationField}
context={{ ...context, collection: context.collection }}
/>
)}
</>
);
};

View File

@ -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 (
<CollectionFieldsToInitializerItems
block={'Table'}
selfField={{
isReadPretty,
filter: (field) => 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,
};
},
}}
/>
);
};

View File

@ -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';

View File

@ -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<AssociationCollectionFieldsProps> = (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<InheritanceCollectionMixin>(
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 <SchemaInitializerItemGroup title={t('Display association fields')}>{children}</SchemaInitializerItemGroup>;
};

View File

@ -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<ParentCollectionFieldsProps> = (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<InheritanceCollectionMixin>(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 <SchemaInitializerChildren>{children}</SchemaInitializerChildren>;
};

View File

@ -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<SelfCollectionFieldsProps> = (props) => {
const callbackContext = useCollectionFieldContext();
const { t, collection } = callbackContext;
const fields = collection.getFields();
const children = getInitializerItemsByFields(props, fields, callbackContext);
return <SchemaInitializerItemGroup title={t('Display fields')}>{children}</SchemaInitializerItemGroup>;
};

View File

@ -0,0 +1,3 @@
export { SelfFields } from './SelfFields';
export { ParentCollectionFields } from './ParentCollectionFields';
export { AssociationCollectionFields } from './AssociationCollectionFields';

View File

@ -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;
});
}

View File

@ -0,0 +1,3 @@
export * from './type';
export * from './getInitializerItemsByFields';
export * from './useCollectionFieldContext';

View File

@ -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<string, any>) => 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<CollectionFieldContext, 'collection'> & {
collection: Collection;
};
}
export interface ParentCollectionFieldsProps extends CommonCollectionFieldsProps {
context: Omit<CollectionFieldContext, 'collection'> & {
collection: Collection;
};
}
export interface AssociationCollectionFieldsProps extends Omit<CommonCollectionFieldsProps, 'filter'> {
filterSelfField?: CommonCollectionFieldsProps['filter'];
filterAssociationField?: CommonCollectionFieldsProps['filter'];
context: Omit<CollectionFieldContext, 'collection'> & {
collection: CollectionFieldContext['collection']; // 之前是可选的,这里是必须的
};
}
export interface CollectionFieldsProps {
/**
* Block name.
*/
block: string;
selfField: Omit<SelfCollectionFieldsProps, 'block' | 'context'>;
parentField?: Omit<ParentCollectionFieldsProps, 'block' | 'context'>;
associationField?: Omit<AssociationCollectionFieldsProps, 'block' | 'context'>;
}

View File

@ -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<any>;
actionContext: ReturnType<typeof useActionContext>;
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<InheritanceCollectionMixin>();
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,
};
}

View File

@ -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';

View File

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