refactor: support dynamic field component (#4932)

* refactor: support dynamic field component

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: add addFieldInterfaceComponentOption() doc

* fix: bug

* fix: bug

* fix: add unit test

* fix: bug

* fix: bug
This commit is contained in:
jack zhang 2024-08-06 09:21:01 +08:00 committed by GitHub
parent 7276a404e4
commit a7c2f90260
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 628 additions and 99 deletions

View File

@ -20,6 +20,7 @@ export interface ApplicationOptions {
schemaInitializers?: SchemaInitializer[];
loadRemotePlugins?: boolean;
dataSourceManager?: DataSourceManagerOptions;
addFieldInterfaceComponentOption(fieldName: string, componentOption: CollectionFieldInterfaceComponentOption): void;
}
```
@ -35,6 +36,7 @@ export interface ApplicationOptions {
- `schemaInitializers`: Schema addition tool. For more information, refer to: [SchemaInitializerManager](/core/ui-schema/schema-initializer-manager)
- `loadRemotePlugins`: Used to control whether to load remote plugins. Default is `false`, meaning remote plugins are not loaded (convenient for unit testing and DEMO environments).
- `dataSourceManager`: Data source manager. For more details, refer to: [DataSourceManager](/core/data-source/data-source-manager)
- `addFieldInterfaceComponentOption`: Add field interface component options. For more details, refer to: [CollectionFieldInterfaceManager](/core/data-source/collection-field-interface-manager#addfieldinterfacecomponentoption)
## Example
@ -344,6 +346,23 @@ app.getCollectionManager() // Get the default data source collection manager
app.getCollectionManager('test') // Get the specified data source collection manager
```
### app.addFieldInterfaceComponentOption()
Add field interface component option.
For a detailed introduction, please refer to: [CollectionFieldInterfaceManager](/core/data-source/collection-field-interface-manager#addfieldinterfacecomponentoption)
```tsx | pure
class MyPlugin extends Plugin {
async load() {
this.app.addFieldInterfaceComponentOption('url', {
label: 'Preview',
value: 'Input.Preview',
});
}
}
```
## Hooks
### useApp()

View File

@ -89,6 +89,41 @@ class MyPlugin extends Plugin {
}
```
#### addFieldInterfaceComponentOption()
Add field interface component option.
![20240725113756](https://static-docs.nocobase.com/20240725113756.png)
- 类型
```tsx | pure
interface CollectionFieldInterfaceComponentOption {
label: string;
value: string;
useVisible?: () => boolean;
useProps?: () => any;
}
class CollectionFieldInterfaceManager {
addFieldInterfaceComponentOption(interfaceName: string, componentOption: CollectionFieldInterfaceComponentOption): void
}
```
- 示例
```tsx | pure
class MyPlugin extends Plugin {
async load() {
this.app.dataSourceManager.collectionFieldInterfaceManager.addFieldInterfaceComponentOption('url', {
label: 'Preview',
value: 'Input.Preview',
});
}
}
```
### field interface group
#### addFieldInterfaceGroups()

View File

@ -34,10 +34,6 @@ class CollectionFieldInterface {
validateSchema(fieldSchema: ISchema): Record<string, ISchema>
usePathOptions(field: CollectionFieldOptions): any
schemaInitialize(schema: ISchema, data: any): void
getOption<K extends keyof IField>(key: K): CollectionFieldInterfaceOptions[K]
getOptions(): CollectionFieldInterfaceOptions;
setOptions(options: CollectionFieldInterfaceOptions): void;
}
```

View File

@ -20,21 +20,23 @@ export interface ApplicationOptions {
schemaInitializers?: SchemaInitializer[];
loadRemotePlugins?: boolean;
dataSourceManager?: DataSourceManagerOptions;
addFieldInterfaceComponentOption(fieldName: string, componentOption: CollectionFieldInterfaceComponentOption): void;
}
```
- 详细信息
- apiClientAPI 请求实例,具体说明请参见:[https://docs.nocobase.com/api/sdk](https://docs.nocobase.com/api/sdk)
- i18n国际化具体请参考[https://www.i18next.com/overview/api#createinstance](https://www.i18next.com/overview/api#createinstance)
- providers上下文
- components全局组件
- scopes全局 scopes
- router配置路由具体请参考[RouterManager](/core/application/router-manager)
- pluginSettings: [PluginSettingsManager](/core/application/plugin-settings-manager)
- schemaSettingsSchema 设置工具,具体参考:[SchemaSettingsManager](/core/ui-schema/schema-initializer-manager)
- schemaInitializersSchema 添加工具,具体参考:[SchemaInitializerManager](/core/ui-schema/schema-initializer-manager)
- loadRemotePlugins用于控制是否加载远程插件默认为 `false`,即不加载远程插件(方便单测和 DEMO 环境)。
- dataSourceManager数据源管理器具体参考[DataSourceManager](/core/data-source/data-source-manager)
- `apiClient`API 请求实例,具体说明请参见:[https://docs.nocobase.com/api/sdk](https://docs.nocobase.com/api/sdk)
- `i18n`:国际化,具体请参考:[https://www.i18next.com/overview/api#createinstance](https://www.i18next.com/overview/api#createinstance)
- `providers`:上下文
- `components`:全局组件
- `scopes`:全局 scopes
- `router`:配置路由,具体请参考:[RouterManager](/core/application/router-manager)
- `pluginSettings`: [PluginSettingsManager](/core/application/plugin-settings-manager)
- `schemaSettings`Schema 设置工具,具体参考:[SchemaSettingsManager](/core/ui-schema/schema-initializer-manager)
- `schemaInitializers`Schema 添加工具,具体参考:[SchemaInitializerManager](/core/ui-schema/schema-initializer-manager)
- `loadRemotePlugins`:用于控制是否加载远程插件,默认为 `false`,即不加载远程插件(方便单测和 DEMO 环境)。
- `dataSourceManager`:数据源管理器,具体参考:[DataSourceManager](/core/data-source/data-source-manager)
- `addFieldInterfaceComponentOption`: 添加 Field interface 组件选项。具体参考: [CollectionFieldInterfaceManager](/core/data-source/collection-field-interface-manager#addfieldinterfacecomponentoption)
- 示例
```tsx
@ -343,6 +345,24 @@ app.getCollectionManager() // 获取默认数据源的 collection manager
app.getCollectionManager('test') // 获取指定数据源的 collection manager
```
### app.addFieldInterfaceComponentOption()
Add field interface component option.
添加 Field interface 组件选项。具体参考: [CollectionFieldInterfaceManager](/core/data-source/collection-field-interface-manager#addfieldinterfacecomponentoption)
```tsx | pure
class MyPlugin extends Plugin {
async load() {
this.app.addFieldInterfaceComponentOption('url', {
label: 'Preview',
value: 'Input.Preview',
});
}
}
```
## Hooks
### useApp()

View File

@ -89,6 +89,40 @@ class MyPlugin extends Plugin {
}
```
#### addFieldInterfaceComponentOption()
添加 Field interface 组件选项。
![20240725113756](https://static-docs.nocobase.com/20240725113756.png)
- 类型
```tsx | pure
interface CollectionFieldInterfaceComponentOption {
label: string;
value: string;
useVisible?: () => boolean;
useProps?: () => any;
}
class CollectionFieldInterfaceManager {
addFieldInterfaceComponentOption(interfaceName: string, componentOption: CollectionFieldInterfaceComponentOption): void
}
```
- 示例
```tsx | pure
class MyPlugin extends Plugin {
async load() {
this.app.dataSourceManager.collectionFieldInterfaceManager.addFieldInterfaceComponentOption('url', {
label: 'Preview',
value: 'Input.Preview',
});
}
}
```
### field interface group
#### addFieldInterfaceGroups()

View File

@ -34,10 +34,6 @@ class CollectionFieldInterface {
validateSchema(fieldSchema: ISchema): Record<string, ISchema>
usePathOptions(field: CollectionFieldOptions): any
schemaInitialize(schema: ISchema, data: any): void
getOption<K extends keyof IField>(key: K): CollectionFieldInterfaceOptions[K]
getOptions(): CollectionFieldInterfaceOptions;
setOptions(options: CollectionFieldInterfaceOptions): void;
}
```

View File

@ -38,6 +38,7 @@ import { CollectionField } from '../data-source/collection-field/CollectionField
import { DataSourceApplicationProvider } from '../data-source/components/DataSourceApplicationProvider';
import { DataBlockProvider } from '../data-source/data-block/DataBlockProvider';
import { DataSourceManager, type DataSourceManagerOptions } from '../data-source/data-source/DataSourceManager';
import { CollectionFieldInterfaceComponentOption } from '../data-source/collection-field-interface/CollectionFieldInterface';
import { OpenModeProvider } from '../modules/popup/OpenModeProvider';
import { AppSchemaComponentProvider } from './AppSchemaComponentProvider';
@ -355,4 +356,11 @@ export class Application {
root.render(<App />);
return root;
}
addFieldInterfaceComponentOption(fieldName: string, componentOption: CollectionFieldInterfaceComponentOption) {
return this.dataSourceManager.collectionFieldInterfaceManager.addFieldInterfaceComponentOption(
fieldName,
componentOption,
);
}
}

View File

@ -17,6 +17,7 @@ import { OpenModeProvider } from '../../modules/popup/OpenModeProvider';
import { Application } from '../Application';
import { Plugin } from '../Plugin';
import { useApp } from '../hooks';
import { CollectionFieldInterface } from '../../data-source';
describe('Application', () => {
beforeAll(() => {
@ -439,4 +440,43 @@ describe('Application', () => {
console.error = originalConsoleWarn;
});
});
describe('alias', () => {
test('addFieldInterfaceComponentOption', () => {
class TestInterface extends CollectionFieldInterface {
name = 'test';
default = {
type: 'string',
uiSchema: {
type: 'string',
'x-component': 'TestComponent',
},
};
}
const app = new Application({
dataSourceManager: {
fieldInterfaces: [TestInterface],
},
});
app.addFieldInterfaceComponentOption('test', {
label: 'A',
value: 'a',
});
expect(app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterface('test').componentOptions)
.toMatchInlineSnapshot(`
[
{
"label": "TestComponent",
"useProps": [Function],
"value": "TestComponent",
},
{
"label": "A",
"value": "a",
},
]
`);
});
});
});

View File

@ -12,9 +12,10 @@ import { ISchema, useFieldSchema } from '@formily/react';
import { TFunction, useTranslation } from 'react-i18next';
import { SchemaSettingsItemType } from '../types';
import { getNewSchema, useHookDefault } from './util';
import { getNewSchema, useHookDefault, useSchemaByType } from './util';
import { useCompile } from '../../../schema-component/hooks/useCompile';
import { useDesignable } from '../../../schema-component/hooks/useDesignable';
import { useColumnSchema } from '../../../schema-component';
export interface CreateModalSchemaSettingsItemProps {
name: string;
@ -27,6 +28,10 @@ export interface CreateModalSchemaSettingsItemProps {
useVisible?: () => boolean;
width?: number | string;
useSubmit?: () => (values: any) => void;
/**
* @default 'common'
*/
type?: 'common' | 'field';
}
/**
@ -47,26 +52,36 @@ export function createModalSettingsItem(options: CreateModalSchemaSettingsItemPr
defaultValue: propsDefaultValue,
useDefaultValue = useHookDefault,
width,
type = 'common',
} = options;
return {
name,
type: 'actionModal',
useVisible,
useComponentProps() {
const fieldSchema = useFieldSchema();
const { deepMerge } = useDesignable();
const fieldSchema = useSchemaByType(type);
const { dn } = useDesignable();
const defaultValue = useDefaultValue(propsDefaultValue);
const values = parentSchemaKey ? _.get(fieldSchema, parentSchemaKey) : undefined;
const compile = useCompile();
const { t } = useTranslation();
const onSubmit = useSubmit();
const { fieldSchema: tableColumnSchema } = useColumnSchema() || {};
return {
title: typeof title === 'function' ? title(t) : compile(title),
width,
schema: schema({ ...defaultValue, ...values }),
onSubmit(values) {
deepMerge(getNewSchema({ fieldSchema, parentSchemaKey, value: values, valueKeys }));
const newSchema = getNewSchema({ fieldSchema, parentSchemaKey: parentSchemaKey, value: values, valueKeys });
if (tableColumnSchema) {
dn.emit('patch', {
schema: newSchema,
});
dn.refresh();
} else {
dn.deepMerge(newSchema);
}
return onSubmit?.(values);
},
};

View File

@ -8,14 +8,15 @@
*/
import _ from 'lodash';
import { useFieldSchema } from '@formily/react';
import { useField, useFieldSchema } from '@formily/react';
import { TFunction, useTranslation } from 'react-i18next';
import { SchemaSettingsItemType } from '../types';
import { getNewSchema, useHookDefault } from './util';
import { getNewSchema, useHookDefault, useSchemaByType } from './util';
import { SelectProps } from '../../../schema-component/antd/select';
import { useCompile } from '../../../schema-component/hooks/useCompile';
import { useDesignable } from '../../../schema-component/hooks/useDesignable';
import { useColumnSchema } from '../../../schema-component';
interface CreateSelectSchemaSettingsItemProps {
name: string;
@ -26,6 +27,10 @@ interface CreateSelectSchemaSettingsItemProps {
defaultValue?: string | number;
useDefaultValue?: () => string | number;
useVisible?: () => boolean;
/**
* @default 'common'
*/
type?: 'common' | 'field';
}
/**
@ -44,6 +49,7 @@ export const createSelectSchemaSettingsItem = (
useOptions = useHookDefault,
schemaKey,
useVisible,
type = 'common',
defaultValue: propsDefaultValue,
useDefaultValue = useHookDefault,
} = options;
@ -52,8 +58,9 @@ export const createSelectSchemaSettingsItem = (
type: 'select',
useVisible,
useComponentProps() {
const filedSchema = useFieldSchema();
const { deepMerge } = useDesignable();
const fieldSchema = useSchemaByType(type);
const { fieldSchema: tableColumnSchema } = useColumnSchema() || {};
const { dn } = useDesignable();
const options = useOptions(propsOptions);
const defaultValue = useDefaultValue(propsDefaultValue);
const compile = useCompile();
@ -62,9 +69,17 @@ export const createSelectSchemaSettingsItem = (
return {
title: typeof title === 'function' ? title(t) : compile(title),
options,
value: _.get(filedSchema, schemaKey, defaultValue),
value: _.get(fieldSchema, schemaKey, defaultValue),
onChange(v) {
deepMerge(getNewSchema({ fieldSchema: filedSchema, schemaKey, value: v }));
const newSchema = getNewSchema({ fieldSchema, schemaKey, value: v });
if (tableColumnSchema) {
dn.emit('patch', {
schema: newSchema,
});
dn.refresh();
} else {
dn.deepMerge(newSchema);
}
},
};
},

View File

@ -12,9 +12,10 @@ import { useFieldSchema } from '@formily/react';
import { TFunction, useTranslation } from 'react-i18next';
import { SchemaSettingsItemType } from '../types';
import { getNewSchema, useHookDefault } from './util';
import { getNewSchema, useHookDefault, useSchemaByType } from './util';
import { useCompile } from '../../../schema-component/hooks/useCompile';
import { useDesignable } from '../../../schema-component/hooks/useDesignable';
import { useColumnSchema } from '../../../schema-component';
export interface CreateSwitchSchemaSettingsItemProps {
name: string;
@ -23,6 +24,10 @@ export interface CreateSwitchSchemaSettingsItemProps {
defaultValue?: boolean;
useDefaultValue?: () => boolean;
useVisible?: () => boolean;
/**
* @default 'common'
*/
type?: 'common' | 'field';
}
/**
@ -37,6 +42,7 @@ export function createSwitchSettingsItem(options: CreateSwitchSchemaSettingsItem
useVisible,
schemaKey,
title,
type = 'common',
defaultValue: propsDefaultValue,
useDefaultValue = useHookDefault,
} = options;
@ -45,17 +51,26 @@ export function createSwitchSettingsItem(options: CreateSwitchSchemaSettingsItem
useVisible,
type: 'switch',
useComponentProps() {
const filedSchema = useFieldSchema();
const { deepMerge } = useDesignable();
const fieldSchema = useSchemaByType(type);
const { dn } = useDesignable();
const defaultValue = useDefaultValue(propsDefaultValue);
const compile = useCompile();
const { t } = useTranslation();
const { fieldSchema: tableColumnSchema } = useColumnSchema() || {};
return {
title: typeof title === 'function' ? title(t) : compile(title),
checked: !!_.get(filedSchema, schemaKey, defaultValue),
checked: !!_.get(fieldSchema, schemaKey, defaultValue),
onChange(v) {
deepMerge(getNewSchema({ fieldSchema: filedSchema, schemaKey, value: v }));
const newSchema = getNewSchema({ fieldSchema, schemaKey, value: v });
if (tableColumnSchema) {
dn.emit('patch', {
schema: newSchema,
});
dn.refresh();
} else {
dn.deepMerge(newSchema);
}
},
};
},

View File

@ -8,28 +8,44 @@
*/
import { ISchema } from '@formily/json-schema';
import { useFieldSchema } from '@formily/react';
import _ from 'lodash';
import { useColumnSchema } from '../../../schema-component';
type IGetNewSchema = {
fieldSchema: ISchema;
// x-component-props.title
schemaKey?: string;
// x-component-props
parentSchemaKey?: string;
value: any;
valueKeys?: string[];
};
export function getNewSchema(options: IGetNewSchema) {
const { fieldSchema, schemaKey, value, parentSchemaKey, valueKeys } = options;
if (value != undefined && typeof value === 'object') {
Object.keys(value).forEach((key) => {
if (valueKeys && !valueKeys.includes(key)) return;
_.set(fieldSchema, `${parentSchemaKey}.${key}`, value[key]);
});
} else {
const { fieldSchema, schemaKey, parentSchemaKey, value, valueKeys } = options as any;
if (schemaKey) {
_.set(fieldSchema, schemaKey, value);
} else if (parentSchemaKey) {
if (value == undefined) return fieldSchema;
if (typeof value === 'object') {
Object.keys(value).forEach((key) => {
if (valueKeys && !valueKeys.includes(key)) return;
_.set(fieldSchema, `${parentSchemaKey}.${key}`, value[key]);
});
} else {
console.error('value must be object');
}
}
return fieldSchema;
}
export const useHookDefault = (defaultValues?: any) => defaultValues;
export const useSchemaByType = (type: 'common' | 'field' = 'common') => {
const schema = useFieldSchema();
const { fieldSchema: tableColumnSchema } = useColumnSchema() || {};
return type === 'field' ? tableColumnSchema || schema : schema;
};

View File

@ -24,6 +24,16 @@ export class UrlFieldInterface extends CollectionFieldInterface {
'x-component': 'Input.URL',
},
};
componentOptions = [
{
label: 'URL',
value: 'Input.URL',
},
{
label: 'Preview',
value: 'Input.Preview',
},
];
availableTypes = ['string', 'text'];
schemaInitialize(schema: ISchema, { block }) {}
properties = {

View File

@ -132,4 +132,191 @@ describe('CollectionFieldInterfaceManager', () => {
expect(collectionFieldInterfaceManager.getFieldInterfaceGroup('nonExistentGroup')).toBeUndefined();
});
});
describe('addFieldInterfaceComponentOption', () => {
it('should add field interface component option', () => {
class A extends CollectionFieldInterface {
name = 'a';
default = {
type: 'string',
uiSchema: {
type: 'string',
'x-component': 'A',
},
};
}
collectionFieldInterfaceManager.addFieldInterfaces([A]);
collectionFieldInterfaceManager.addFieldInterfaceComponentOption('a', {
label: 'Test',
value: 'test',
});
const fieldInterface = collectionFieldInterfaceManager.getFieldInterface('a');
expect(fieldInterface.componentOptions).toMatchInlineSnapshot(`
[
{
"label": "A",
"useProps": [Function],
"value": "A",
},
{
"label": "Test",
"value": "test",
},
]
`);
});
it('async addFieldInterfaceComponentOptions', async () => {
class A extends CollectionFieldInterface {
name = 'a';
default = {
type: 'string',
uiSchema: {
type: 'string',
'x-component': 'A',
},
};
}
collectionFieldInterfaceManager.addFieldInterfaceComponentOption('a', {
label: 'Test',
value: 'test',
});
collectionFieldInterfaceManager.addFieldInterfaces([A]);
const fieldInterface = collectionFieldInterfaceManager.getFieldInterface('a');
expect(fieldInterface.componentOptions).toMatchInlineSnapshot(`
[
{
"label": "A",
"useProps": [Function],
"value": "A",
},
{
"label": "Test",
"value": "test",
},
]
`);
});
it('not default properties', () => {
class A extends CollectionFieldInterface {
name = 'a';
}
collectionFieldInterfaceManager.addFieldInterfaces([A]);
collectionFieldInterfaceManager.addFieldInterfaceComponentOption('a', {
label: 'Test',
value: 'test',
});
const fieldInterface = collectionFieldInterfaceManager.getFieldInterface('a');
expect(fieldInterface.componentOptions).toMatchInlineSnapshot(`
[
{
"label": "Test",
"value": "test",
},
]
`);
});
it('not uiSchema properties', () => {
class A extends CollectionFieldInterface {
name = 'a';
}
collectionFieldInterfaceManager.addFieldInterfaces([A]);
collectionFieldInterfaceManager.addFieldInterfaceComponentOption('a', {
label: 'Test',
value: 'test',
});
const fieldInterface = collectionFieldInterfaceManager.getFieldInterface('a');
expect(fieldInterface.componentOptions).toMatchInlineSnapshot(`
[
{
"label": "Test",
"value": "test",
},
]
`);
});
it('Label', () => {
class A extends CollectionFieldInterface {
name = 'a';
default = {
type: 'string',
uiSchema: {
type: 'string',
'x-component': 'A.B',
},
};
}
collectionFieldInterfaceManager.addFieldInterfaces([A]);
collectionFieldInterfaceManager.addFieldInterfaceComponentOption('a', {
label: 'Test',
value: 'test',
});
const fieldInterface = collectionFieldInterfaceManager.getFieldInterface('a');
expect(fieldInterface.componentOptions).toMatchInlineSnapshot(`
[
{
"label": "B",
"useProps": [Function],
"value": "A.B",
},
{
"label": "Test",
"value": "test",
},
]
`);
});
it('have componentOptions', () => {
class A extends CollectionFieldInterface {
name = 'a';
default = {
type: 'string',
uiSchema: {
type: 'string',
'x-component': 'A',
},
};
componentOptions = [
{
label: 'A',
value: 'A',
},
];
}
collectionFieldInterfaceManager.addFieldInterfaces([A]);
collectionFieldInterfaceManager.addFieldInterfaceComponentOption('a', {
label: 'Test',
value: 'test',
});
const fieldInterface = collectionFieldInterfaceManager.getFieldInterface('a');
expect(fieldInterface.componentOptions).toMatchInlineSnapshot(`
[
{
"label": "A",
"value": "A",
},
{
"label": "Test",
"value": "test",
},
]
`);
});
});
});

View File

@ -15,6 +15,13 @@ export type CollectionFieldInterfaceFactory = new (
collectionFieldInterfaceManager: CollectionFieldInterfaceManager,
) => CollectionFieldInterface;
export interface CollectionFieldInterfaceComponentOption {
label: string;
value: string;
useVisible?: () => boolean;
useProps?: () => any;
}
export abstract class CollectionFieldInterface {
constructor(public collectionFieldInterfaceManager: CollectionFieldInterfaceManager) {}
name: string;
@ -32,6 +39,7 @@ export abstract class CollectionFieldInterface {
supportDataSourceType?: string[];
notSupportDataSourceType?: string[];
hasDefaultValue?: boolean;
componentOptions?: CollectionFieldInterfaceComponentOption[];
isAssociation?: boolean;
operators?: any[];
/**
@ -54,4 +62,24 @@ export abstract class CollectionFieldInterface {
usePathOptions?(field: CollectionFieldOptions): any;
schemaInitialize?(schema: ISchema, data: any): void;
hidden?: boolean;
addComponentOption(componentOption: CollectionFieldInterfaceComponentOption) {
if (!this.componentOptions) {
this.componentOptions = [];
const xComponent = this.default?.uiSchema?.['x-component'];
const componentProps = this.default?.uiSchema?.['x-component-props'];
if (xComponent) {
this.componentOptions = [
{
label: xComponent.split('.').pop(),
value: xComponent,
useProps() {
return componentProps || {};
},
},
];
}
}
this.componentOptions.push(componentOption);
}
}

View File

@ -8,11 +8,21 @@
*/
import type { DataSourceManager } from '../data-source';
import type { CollectionFieldInterface, CollectionFieldInterfaceFactory } from './CollectionFieldInterface';
import type {
CollectionFieldInterface,
CollectionFieldInterfaceComponentOption,
CollectionFieldInterfaceFactory,
} from './CollectionFieldInterface';
interface ActionType {
type: 'addComponentOption';
data: any;
}
export class CollectionFieldInterfaceManager {
protected collectionFieldInterfaceInstances: Record<string, CollectionFieldInterface> = {};
protected collectionFieldGroups: Record<string, { label: string; order?: number }> = {};
protected actionList: Record<string, ActionType[]> = {};
constructor(
fieldInterfaceClasses: CollectionFieldInterfaceFactory[],
@ -27,11 +37,30 @@ export class CollectionFieldInterfaceManager {
const newCollectionFieldInterfaces = fieldInterfaceClasses.reduce((acc, Interface) => {
const instance = new Interface(this);
acc[instance.name] = instance;
if (Array.isArray(this.actionList[instance.name])) {
this.actionList[instance.name].forEach((item) => {
instance[item.type](item.data);
});
this.actionList[instance.name] = undefined;
}
return acc;
}, {});
Object.assign(this.collectionFieldInterfaceInstances, newCollectionFieldInterfaces);
}
addFieldInterfaceComponentOption(interfaceName: string, componentOption: CollectionFieldInterfaceComponentOption) {
const fieldInterface = this.getFieldInterface(interfaceName);
if (!fieldInterface) {
if (!this.actionList[interfaceName]) {
this.actionList[interfaceName] = [];
}
this.actionList[interfaceName].push({ type: 'addComponentOption', data: componentOption });
return;
}
fieldInterface.addComponentOption(componentOption);
}
getFieldInterface<T extends CollectionFieldInterface>(name: string) {
return this.collectionFieldInterfaceInstances[name] as T;
}

View File

@ -0,0 +1,76 @@
/**
* 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 } from '@formily/core';
import { useField, useFieldSchema } from '@formily/react';
import _ from 'lodash';
import { useTranslation } from 'react-i18next';
import { useDataSourceManager } from '../data-source/DataSourceManagerProvider';
import { useCollectionField } from '../collection-field/CollectionFieldProvider';
import { useColumnSchema, useCompile, useDesignable } from '../../schema-component';
import type { SchemaSettingsItemType } from '../../application';
export const fieldComponentSettingsItem: SchemaSettingsItemType = {
name: 'fieldComponent',
type: 'select',
useVisible() {
const collectionField = useCollectionField();
const dm = useDataSourceManager();
if (!collectionField) return false;
const collectionInterface = dm.collectionFieldInterfaceManager.getFieldInterface(collectionField?.interface);
return (
Array.isArray(collectionInterface?.componentOptions) &&
collectionInterface.componentOptions.length > 1 &&
collectionInterface.componentOptions.filter((item) => !item.useVisible || item.useVisible()).length > 1
);
},
useComponentProps() {
const { t } = useTranslation();
const field = useField<Field>();
const schema = useFieldSchema();
const collectionField = useCollectionField();
const dm = useDataSourceManager();
const collectionInterface = dm.collectionFieldInterfaceManager.getFieldInterface(collectionField?.interface);
const { fieldSchema: tableColumnSchema } = useColumnSchema();
const fieldSchema = tableColumnSchema || schema;
const { dn } = useDesignable();
const compile = useCompile();
const options =
collectionInterface?.componentOptions
?.filter((item) => !item.useVisible || item.useVisible())
?.map((item) => {
return {
label: compile(item.label),
value: item.value,
useProps: item.useProps,
};
}) || [];
return {
title: t('Field component'),
options,
value: fieldSchema['x-component-props']?.['component'] || options[0]?.value,
onChange(component) {
const componentOptions = options.find((item) => item.value === component);
const componentProps = {
component,
...(componentOptions?.useProps?.() || {}),
};
_.set(fieldSchema, 'x-component-props', componentProps);
field.componentProps = componentProps;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-component-props': componentProps,
},
});
},
};
},
};

View File

@ -0,0 +1,10 @@
/**
* 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 './fieldComponent';

View File

@ -17,3 +17,4 @@ export * from './data-source';
export * from './collection-record';
export * from './utils';
export * from './collection-fields-to-initializer-items';
export * from './commonsSettingsItem';

View File

@ -25,6 +25,8 @@ import { isPatternDisabled } from '../../../../schema-settings';
import { ActionType } from '../../../../schema-settings/LinkageRules/type';
import { SchemaSettingsDefaultValue } from '../../../../schema-settings/SchemaSettingsDefaultValue';
import { useIsAllowToSetDefaultValue } from '../../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem';
import { SchemaSettingsLinkageRules } from '../../../../schema-settings';
import { useIsFieldReadPretty } from '../../../../schema-component/antd/form-item/FormItem.Settings';
export const fieldSettingsFormItem = new SchemaSettings({
@ -464,6 +466,7 @@ export const fieldSettingsFormItem = new SchemaSettings({
};
},
},
fieldComponentSettingsItem,
];
},
},

View File

@ -21,6 +21,7 @@ import { useAssociationFieldContext } from '../../../../schema-component/antd/as
import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator';
import { SchemaSettingsDefaultValue } from '../../../../schema-settings/SchemaSettingsDefaultValue';
import { isPatternDisabled } from '../../../../schema-settings/isPatternDisabled';
import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem';
import { SchemaSettingsLinkageRules } from '../../../../schema-settings';
export const tableColumnSettings = new SchemaSettings({
@ -378,6 +379,7 @@ export const tableColumnSettings = new SchemaSettings({
};
},
},
fieldComponentSettingsItem,
],
},
{

View File

@ -17,6 +17,7 @@ import { useCollectionManager } from '../../../../data-source/collection/Collect
import { useCompile, useDesignable } from '../../../../schema-component';
import { SchemaSettingsDefaultSortingRules } from '../../../../schema-settings';
import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope';
import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem';
export const filterCollapseItemFieldSettings = new SchemaSettings({
name: 'fieldSettings:FilterCollapseItem',
@ -197,6 +198,7 @@ export const filterCollapseItemFieldSettings = new SchemaSettings({
};
},
},
fieldComponentSettingsItem,
];
},
},

View File

@ -18,6 +18,7 @@ import { useCollectionManager_deprecated, useCollection_deprecated } from '../..
import { useFieldComponentName } from '../../../../common/useFieldComponentName';
import { EditOperator, useDesignable, useValidateSchema } from '../../../../schema-component';
import { SchemaSettingsDefaultValue } from '../../../../schema-settings/SchemaSettingsDefaultValue';
import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem';
export const filterFormItemFieldSettings = new SchemaSettings({
name: 'fieldSettings:FilterFormItem',
@ -329,6 +330,7 @@ export const filterFormItemFieldSettings = new SchemaSettings({
name: 'operator',
Component: EditOperator,
},
fieldComponentSettingsItem,
];
},
},

View File

@ -12,9 +12,11 @@ import { useField, useFieldSchema } from '@formily/react';
import { useTranslation } from 'react-i18next';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { useColumnSchema, useDesignable } from '../../../../schema-component';
import { fieldComponent } from '../Input.URL/settings';
import { SchemaSettingsItemType } from '../../../../application/schema-settings';
// import { createSelectSchemaSettingsItem } from '../../../../application';
// import { fieldComponent } from '../Input.URL/settings';
const size = {
const size: SchemaSettingsItemType = {
name: 'size',
type: 'select',
useComponentProps() {
@ -50,7 +52,23 @@ const size = {
},
};
// const size2 = createSelectSchemaSettingsItem({
// name: 'size2',
// title: 'Size2',
// type: 'field',
// schemaKey: 'x-component-props.size',
// options: [
// { value: 'small', label: 'Small' },
// { value: 'middle', label: 'Middle' },
// { value: 'large', label: 'Large' },
// { value: 'auto', label: 'Auto' },
// ],
// });
export const inputPreviewComponentFieldSettings = new SchemaSettings({
name: 'fieldSettings:component:Input.Preview',
items: [fieldComponent, size],
items: [
size,
// size2
],
});

View File

@ -1,51 +0,0 @@
/**
* 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 } from '@formily/core';
import { useField, useFieldSchema } from '@formily/react';
import _ from 'lodash';
import { useTranslation } from 'react-i18next';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { useColumnSchema, useDesignable } from '../../../../schema-component';
export const fieldComponent: any = {
name: 'fieldComponent',
type: 'select',
useComponentProps() {
const { t } = useTranslation();
const field = useField<Field>();
const schema = useFieldSchema();
const { fieldSchema: tableColumnSchema } = useColumnSchema();
const fieldSchema = tableColumnSchema || schema;
const { dn } = useDesignable();
return {
title: t('Field component'),
options: [
{ value: 'Input.URL', label: 'URL' },
{ value: 'Input.Preview', label: 'Preview' },
],
value: fieldSchema['x-component-props']?.['component'] || 'Input.URL',
onChange(component) {
_.set(fieldSchema, 'x-component-props.component', component);
field.componentProps.component = component;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-component-props': fieldSchema['x-component-props'],
},
});
},
};
},
};
export const inputURLComponentFieldSettings = new SchemaSettings({
name: 'fieldSettings:component:Input.URL',
items: [fieldComponent],
});

View File

@ -53,7 +53,7 @@ import { fileManagerComponentFieldSettings } from '../modules/fields/component/F
import { previewComponentFieldSettings } from '../modules/fields/component/FileManager/previewComponentFieldSettings';
import { uploadAttachmentComponentFieldSettings } from '../modules/fields/component/FileManager/uploadAttachmentComponentFieldSettings';
import { inputPreviewComponentFieldSettings } from '../modules/fields/component/Input.Preview/settings';
import { inputURLComponentFieldSettings } from '../modules/fields/component/Input.URL/settings';
// import { inputURLComponentFieldSettings } from '../modules/fields/component/Input.URL/settings';
import { inputNumberComponentFieldSettings } from '../modules/fields/component/InputNumber/inputNumberComponentFieldSettings';
import { subformComponentFieldSettings } from '../modules/fields/component/Nester/subformComponentFieldSettings';
import { recordPickerComponentFieldSettings } from '../modules/fields/component/Picker/recordPickerComponentFieldSettings';
@ -120,7 +120,7 @@ export class SchemaSettingsPlugin extends Plugin {
this.schemaSettingsManager.add(tagComponentFieldSettings);
this.schemaSettingsManager.add(cascadeSelectComponentFieldSettings);
this.schemaSettingsManager.add(inputPreviewComponentFieldSettings);
this.schemaSettingsManager.add(inputURLComponentFieldSettings);
// this.schemaSettingsManager.add(inputURLComponentFieldSettings);
this.schemaSettingsManager.add(uploadAttachmentComponentFieldSettings);
this.schemaSettingsManager.add(previewComponentFieldSettings);
}

View File

@ -20,6 +20,7 @@ import {
useFormBlockContext,
useIsFormReadPretty,
useValidateSchema,
fieldComponentSettingsItem,
EditValidationRules,
} from '@nocobase/client';
import _ from 'lodash';
@ -201,6 +202,7 @@ export const bulkEditFormItemSettings = new SchemaSettings({
return form && !isFormReadPretty && validateSchema;
},
},
fieldComponentSettingsItem,
];
},
},

View File

@ -233,6 +233,7 @@ test.describe('PageHeader', () => {
await page.getByRole('menuitem', { name: 'Edit button' }).click();
await page.getByRole('textbox').fill('Test_changed');
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByLabel('Edit button')).not.toBeVisible();
await expect(navigationBarPositionElement).toContainText('Test_changed');
// 编辑 URL