diff --git a/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx b/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx index 8e3c7c1eb..e5b1b8c69 100644 --- a/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx +++ b/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx @@ -1,9 +1,14 @@ import React from 'react'; import { Plugin } from '../application/Plugin'; import { ActionSchemaToolbar } from '../modules/actions/ActionSchemaToolbar'; -import { CollapseItemSchemaToolbar } from '../modules/blocks/filter-blocks/collapse/CollapseItemSchemaToolbar'; +import { BlockSchemaToolbar } from '../modules/blocks/BlockSchemaToolbar'; import { FormItemSchemaToolbar } from '../modules/blocks/data-blocks/form/FormItemSchemaToolbar'; +import { useCreateFormBlockDecoratorProps } from '../modules/blocks/data-blocks/form/hooks/useCreateFormBlockDecoratorProps'; +import { useCreateFormBlockProps } from '../modules/blocks/data-blocks/form/hooks/useCreateFormBlockProps'; +import { useEditFormBlockDecoratorProps } from '../modules/blocks/data-blocks/form/hooks/useEditFormBlockDecoratorProps'; +import { useEditFormBlockProps } from '../modules/blocks/data-blocks/form/hooks/useEditFormBlockProps'; import { TableColumnSchemaToolbar } from '../modules/blocks/data-blocks/table/TableColumnSchemaToolbar'; +import { CollapseItemSchemaToolbar } from '../modules/blocks/filter-blocks/collapse/CollapseItemSchemaToolbar'; import { SchemaComponentOptions } from '../schema-component'; import { RecordLink, useParamsFromRecord, useSourceIdFromParentRecord, useSourceIdFromRecord } from './BlockProvider'; import { DetailsBlockProvider, useDetailsBlockProps } from './DetailsBlockProvider'; @@ -15,8 +20,10 @@ import { useTableBlockProps } from '../modules/blocks/data-blocks/table/hooks/us import { TableFieldProvider, useTableFieldProps } from './TableFieldProvider'; import { TableSelectorProvider, useTableSelectorProps } from './TableSelectorProvider'; import * as bp from './hooks'; -import { BlockSchemaToolbar } from '../modules/blocks/BlockSchemaToolbar'; import { useTableBlockDecoratorProps } from '../modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps'; +import { useListBlockDecoratorProps } from '../modules/blocks/data-blocks/list/hooks/useListBlockDecoratorProps'; +import { useTableSelectorDecoratorProps } from '../modules/blocks/data-blocks/table-selector/hooks/useTableSelectorDecoratorProps'; +import { useCollapseBlockDecoratorProps } from '../modules/blocks/filter-blocks/collapse/hooks/useCollapseBlockDecoratorProps'; // TODO: delete this, replaced by `BlockSchemaComponentPlugin` export const BlockSchemaComponentProvider: React.FC = (props) => { @@ -38,12 +45,19 @@ export const BlockSchemaComponentProvider: React.FC = (props) => { useSourceIdFromParentRecord, useParamsFromRecord, useFormBlockProps, + useCreateFormBlockProps, + useCreateFormBlockDecoratorProps, + useEditFormBlockDecoratorProps, + useEditFormBlockProps, useFormFieldProps, useDetailsBlockProps, useTableFieldProps, useTableBlockProps, useTableSelectorProps, useTableBlockDecoratorProps, + useListBlockDecoratorProps, + useTableSelectorDecoratorProps, + useCollapseBlockDecoratorProps, }} > {props.children} @@ -82,12 +96,19 @@ export class BlockSchemaComponentPlugin extends Plugin { useSourceIdFromParentRecord, useParamsFromRecord, useFormBlockProps, + useCreateFormBlockProps, + useCreateFormBlockDecoratorProps, + useEditFormBlockDecoratorProps, + useEditFormBlockProps, useFormFieldProps, useDetailsBlockProps, useTableFieldProps, useTableBlockProps, useTableSelectorProps, useTableBlockDecoratorProps, + useListBlockDecoratorProps, + useTableSelectorDecoratorProps, + useCollapseBlockDecoratorProps, }); } } diff --git a/packages/core/client/src/block-provider/FormBlockProvider.tsx b/packages/core/client/src/block-provider/FormBlockProvider.tsx index 7ac794662..1ebbb0584 100644 --- a/packages/core/client/src/block-provider/FormBlockProvider.tsx +++ b/packages/core/client/src/block-provider/FormBlockProvider.tsx @@ -10,6 +10,7 @@ import { Templates as DataTemplateSelect } from '../schema-component/antd/form-v import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { TemplateBlockProvider } from './TemplateBlockProvider'; import { FormActiveFieldsProvider } from './hooks/useFormActiveFields'; +import { withDynamicSchemaProps } from '../application/hoc/withDynamicSchemaProps'; export const FormBlockContext = createContext<{ form?: any; @@ -29,7 +30,7 @@ FormBlockContext.displayName = 'FormBlockContext'; const InternalFormBlockProvider = (props) => { const ctx = useFormBlockContext(); - const { action, readPretty, params, association, collection } = props; + const { action, readPretty, params, collection } = props; const field = useField(); const form = useMemo( () => @@ -89,7 +90,7 @@ export const useIsDetailBlock = () => { return ctx.type !== 'create' && fieldSchema?.['x-acl-action'] !== 'create' && fieldSchema?.['x-action'] !== 'create'; }; -export const FormBlockProvider = (props) => { +export const FormBlockProvider = withDynamicSchemaProps((props) => { const record = useRecord(); const parentRecordData = useCollectionParentRecordData(); const { collection, isCusomeizeCreate } = props; @@ -120,7 +121,7 @@ export const FormBlockProvider = (props) => { ); -}; +}); export const useFormBlockContext = () => { return useContext(FormBlockContext); diff --git a/packages/core/client/src/block-provider/TableSelectorProvider.tsx b/packages/core/client/src/block-provider/TableSelectorProvider.tsx index 11f3903e0..506c70e0e 100644 --- a/packages/core/client/src/block-provider/TableSelectorProvider.tsx +++ b/packages/core/client/src/block-provider/TableSelectorProvider.tsx @@ -11,6 +11,7 @@ import { RecordProvider, useRecord } from '../record-provider'; import { SchemaComponentOptions } from '../schema-component'; import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider'; import { useParsedFilter } from './hooks'; +import { withDynamicSchemaProps } from '../application/hoc/withDynamicSchemaProps'; type Params = { filter?: any; @@ -141,7 +142,7 @@ const useAssociationNames = (collection) => { ); }; -export const TableSelectorProvider = (props: TableSelectorProviderProps) => { +export const TableSelectorProvider = withDynamicSchemaProps((props: TableSelectorProviderProps) => { const parentParams = useTableSelectorParams(); const fieldSchema = useFieldSchema(); const { getCollectionJoinField, getCollectionFields } = useCollectionManager_deprecated(); @@ -263,7 +264,7 @@ export const TableSelectorProvider = (props: TableSelectorProviderProps) => { ); -}; +}); export const useTableSelectorContext = () => { return useContext(TableSelectorContext); diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index b6d88a516..836bc1479 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -36,6 +36,7 @@ import { TableFieldResource } from '../TableFieldProvider'; export * from './useFormActiveFields'; export * from './useParsedFilter'; +export * from './useDataBlockSourceId'; export const usePickActionProps = () => { const form = useForm(); diff --git a/packages/core/client/src/block-provider/hooks/useDataBlockSourceId.tsx b/packages/core/client/src/block-provider/hooks/useDataBlockSourceId.tsx index b7efefeff..d677198b7 100644 --- a/packages/core/client/src/block-provider/hooks/useDataBlockSourceId.tsx +++ b/packages/core/client/src/block-provider/hooks/useDataBlockSourceId.tsx @@ -7,6 +7,7 @@ import { } from '../..'; /** + * @internal * 注意:这里有一个需要更改 schema 才能解决的问题,就是在获取 sourceId 的时候无法确定(在关系字段和当前表同表时) * 是需要从 recordData 还是 parentRecordData 中获取;解决方法是通过更改 schema,在不同类型的关系区块中 * (`通过点击关系字段按钮打开的弹窗中创建的非关系字段区块`和`关系字段区块`)使用不同的 hook。 diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/CreateFormBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/CreateFormBlockInitializer.tsx index 68614c002..d5728269c 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/CreateFormBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/form/CreateFormBlockInitializer.tsx @@ -4,8 +4,9 @@ import React from 'react'; import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; import { useBlockAssociationContext } from '../../../../block-provider'; import { useCollection_deprecated } from '../../../../collection-manager'; +import { useRecordCollectionDataSourceItems } from '../../../../schema-initializer/utils'; import { useSchemaTemplateManager } from '../../../../schema-templates'; -import { createFormBlockSchema, useRecordCollectionDataSourceItems } from '../../../../schema-initializer/utils'; +import { createCreateFormBlockUISchema } from './createCreateFormBlockUISchema'; // TODO: `SchemaInitializerItem` items export const CreateFormBlockInitializer = () => { @@ -23,14 +24,19 @@ export const CreateFormBlockInitializer = () => { if (item.template) { const s = await getTemplateSchemaByMode(item); if (item.template.componentName === 'FormItem') { - const blockSchema = createFormBlockSchema({ - actionInitializers: 'createForm:configureActions', - association, - dataSource: collection.dataSource, - collection: collection.name, - template: s, - settings: 'blockSettings:createForm', - }); + const blockSchema = createCreateFormBlockUISchema( + association + ? { + association, + dataSource: collection.dataSource, + templateSchema: s, + } + : { + collectionName: collection.name, + dataSource: collection.dataSource, + templateSchema: s, + }, + ); if (item.mode === 'reference') { blockSchema['x-template-key'] = item.template.key; } @@ -40,13 +46,17 @@ export const CreateFormBlockInitializer = () => { } } else { insert( - createFormBlockSchema({ - actionInitializers: 'createForm:configureActions', - association, - dataSource: collection.dataSource, - collection: collection.name, - settings: 'blockSettings:createForm', - }), + createCreateFormBlockUISchema( + association + ? { + association, + dataSource: collection.dataSource, + } + : { + collectionName: collection.name, + dataSource: collection.dataSource, + }, + ), ); } }} diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/FormBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/FormBlockInitializer.tsx index 5ea124e2e..e86edbbdd 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/FormBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/form/FormBlockInitializer.tsx @@ -1,9 +1,9 @@ import { FormOutlined } from '@ant-design/icons'; import React, { useCallback } from 'react'; import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; -import { createFormBlockSchema } from '../../../../schema-initializer/utils'; -import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer'; import { Collection, CollectionFieldOptions } from '../../../../data-source/collection/Collection'; +import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer'; +import { createCreateFormBlockUISchema } from './createCreateFormBlockUISchema'; export const FormBlockInitializer = ({ filterCollections, @@ -44,11 +44,10 @@ export const FormBlockInitializer = ({ } insert( - createFormBlockSchema({ - collection: item.collectionName || item.name, + createCreateFormBlockUISchema({ + collectionName: item.collectionName || item.name, dataSource: item.dataSource, isCusomeizeCreate, - settings: 'blockSettings:createForm', }), ); }, @@ -65,12 +64,11 @@ export const FormBlockInitializer = ({ return templateWrap(templateSchema, { item }); } - const schema = createFormBlockSchema({ + const schema = createCreateFormBlockUISchema({ isCusomeizeCreate, dataSource: item.dataSource, - template: templateSchema, - collection: item.name, - settings: 'blockSettings:createForm', + templateSchema: templateSchema, + collectionName: item.name, }); if (item.template && item.mode === 'reference') { schema['x-template-key'] = item.template.key; diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/RecordFormBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/RecordFormBlockInitializer.tsx index a080b596d..c207b7550 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/RecordFormBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/form/RecordFormBlockInitializer.tsx @@ -3,8 +3,9 @@ import React, { useCallback } from 'react'; import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; import { useBlockAssociationContext } from '../../../../block-provider'; import { useCollection_deprecated } from '../../../../collection-manager'; +import { useRecordCollectionDataSourceItems } from '../../../../schema-initializer/utils'; import { useSchemaTemplateManager } from '../../../../schema-templates'; -import { createFormBlockSchema, useRecordCollectionDataSourceItems } from '../../../../schema-initializer/utils'; +import { createEditFormBlockUISchema } from './createEditFormBlockUISchema'; /** * @deprecated @@ -43,16 +44,17 @@ export function useCreateEditFormBlock() { const createEditFormBlock = useCallback( ({ item }) => { insert( - createFormBlockSchema({ - association, - collection: item.collectionName || item.name, - dataSource: item.dataSource, - action: 'get', - useSourceId: '{{ useSourceIdFromParentRecord }}', - useParams: '{{ useParamsFromRecord }}', - actionInitializers: 'editForm:configureActions', - settings: 'blockSettings:editForm', - }), + createEditFormBlockUISchema( + association + ? { + association, + dataSource: item.dataSource, + } + : { + collectionName: item.collectionName || item.name, + dataSource: item.dataSource, + }, + ), ); }, [association, insert], @@ -61,17 +63,19 @@ export function useCreateEditFormBlock() { const templateWrap = useCallback( (templateSchema, { item }) => { if (item.template.componentName === 'FormItem') { - const blockSchema = createFormBlockSchema({ - association, - collection: item.collectionName || item.name, - dataSource: item.dataSource, - action: 'get', - useSourceId: '{{ useSourceIdFromParentRecord }}', - useParams: '{{ useParamsFromRecord }}', - actionInitializers: 'editForm:configureActions', - template: templateSchema, - settings: 'blockSettings:editForm', - }); + const blockSchema = createEditFormBlockUISchema( + association + ? { + association, + dataSource: item.dataSource, + templateSchema: templateSchema, + } + : { + collectionName: item.collectionName || item.name, + dataSource: item.dataSource, + templateSchema: templateSchema, + }, + ); if (item.mode === 'reference') { blockSchema['x-template-key'] = item.template.key; } diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__tests__/createCreateFormBlockUISchema.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__tests__/createCreateFormBlockUISchema.test.ts new file mode 100644 index 000000000..e1daebb3b --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__tests__/createCreateFormBlockUISchema.test.ts @@ -0,0 +1,64 @@ +import { CreateFormBlockUISchemaOptions, createCreateFormBlockUISchema } from '../createCreateFormBlockUISchema'; + +vi.mock('@formily/shared', () => { + return { + uid: () => 'mocked-uid', + }; +}); + +describe('createCreateFormBlockUISchema', () => { + it('should create a schema with all options', () => { + const options: CreateFormBlockUISchemaOptions = { + collectionName: 'users', + dataSource: 'userDataSource', + association: 'userAssociation', + templateSchema: { type: 'string' }, + isCusomeizeCreate: true, + }; + + const result = createCreateFormBlockUISchema(options); + expect(result).toMatchInlineSnapshot(` + { + "properties": { + "mocked-uid": { + "properties": { + "grid": { + "type": "string", + }, + "mocked-uid": { + "type": "void", + "x-component": "ActionBar", + "x-component-props": { + "layout": "one-column", + "style": { + "marginTop": 24, + }, + }, + "x-initializer": "createForm:configureActions", + }, + }, + "type": "void", + "x-component": "FormV2", + "x-use-component-props": "useCreateFormBlockProps", + }, + }, + "type": "void", + "x-acl-action": "userAssociation:create", + "x-acl-action-props": { + "skipScopeCheck": true, + }, + "x-component": "CardItem", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "association": "userAssociation", + "collection": "users", + "dataSource": "userDataSource", + "isCusomeizeCreate": true, + }, + "x-settings": "blockSettings:createForm", + "x-toolbar": "BlockSchemaToolbar", + "x-use-decorator-props": "useCreateFormBlockDecoratorProps", + } + `); + }); +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__tests__/createEditFormBlockUISchema.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__tests__/createEditFormBlockUISchema.test.ts new file mode 100644 index 000000000..8082b18d3 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__tests__/createEditFormBlockUISchema.test.ts @@ -0,0 +1,62 @@ +import { ISchema } from '@formily/react'; +import { vi } from 'vitest'; +import { createEditFormBlockUISchema } from '../createEditFormBlockUISchema'; + +vi.mock('@formily/shared', () => ({ + uid: () => 'uniqueId', +})); + +describe('createEditFormBlockUISchema', () => { + it('should create a schema with all options', () => { + const schema = createEditFormBlockUISchema({ + collectionName: 'users', + dataSource: 'UserDataSource', + association: 'users:update', + templateSchema: { type: 'string' }, + }); + + expect(schema).toMatchInlineSnapshot(` + { + "properties": { + "uniqueId": { + "properties": { + "grid": { + "type": "string", + }, + "uniqueId": { + "type": "void", + "x-component": "ActionBar", + "x-component-props": { + "layout": "one-column", + "style": { + "marginTop": 24, + }, + }, + "x-initializer": "editForm:configureActions", + }, + }, + "type": "void", + "x-component": "FormV2", + "x-use-component-props": "useEditFormBlockProps", + }, + }, + "type": "void", + "x-acl-action": "users:update:update", + "x-acl-action-props": { + "skipScopeCheck": false, + }, + "x-component": "CardItem", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "action": "get", + "association": "users:update", + "collection": "users", + "dataSource": "UserDataSource", + }, + "x-settings": "blockSettings:editForm", + "x-toolbar": "BlockSchemaToolbar", + "x-use-decorator-props": "useEditFormBlockDecoratorProps", + } + `); + }); +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/createCreateFormBlockUISchema.ts b/packages/core/client/src/modules/blocks/data-blocks/form/createCreateFormBlockUISchema.ts new file mode 100644 index 000000000..cd399656e --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/createCreateFormBlockUISchema.ts @@ -0,0 +1,70 @@ +import { ISchema } from '@formily/react'; +import { uid } from '@formily/shared'; + +export interface CreateFormBlockUISchemaOptions { + dataSource: string; + /** 如果传了 association 就不需要再传 collectionName 了 */ + collectionName?: string; + association?: string; + templateSchema?: ISchema; + isCusomeizeCreate?: boolean; +} + +/** + * 创建新增表单的 UI Schema + * @returns + */ +export function createCreateFormBlockUISchema(options: CreateFormBlockUISchemaOptions): ISchema { + const { collectionName, association, dataSource, templateSchema, isCusomeizeCreate } = options; + const resourceName = association || collectionName; + + if (!dataSource) { + throw new Error('dataSource are required'); + } + + const schema: ISchema = { + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-acl-action': `${resourceName}:create`, + 'x-decorator': 'FormBlockProvider', + 'x-use-decorator-props': 'useCreateFormBlockDecoratorProps', + 'x-decorator-props': { + dataSource, + collection: collectionName, + association, + isCusomeizeCreate, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:createForm', + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useCreateFormBlockProps', + properties: { + grid: templateSchema || { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + properties: {}, + }, + [uid()]: { + type: 'void', + 'x-initializer': 'createForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + style: { + marginTop: 24, + }, + }, + }, + }, + }, + }, + }; + return schema; +} diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/createEditFormBlockUISchema.ts b/packages/core/client/src/modules/blocks/data-blocks/form/createEditFormBlockUISchema.ts new file mode 100644 index 000000000..509b0183b --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/createEditFormBlockUISchema.ts @@ -0,0 +1,64 @@ +import { ISchema } from '@formily/react'; +import { uid } from '@formily/shared'; + +interface EditFormBlockOptions { + dataSource: string; + /** 如果传了 association 就不需要再传 collectionName 了 */ + collectionName?: string; + association?: string; + templateSchema?: ISchema; +} + +export function createEditFormBlockUISchema(options: EditFormBlockOptions): ISchema { + const { collectionName, dataSource, association, templateSchema } = options; + const resourceName = association || collectionName; + + if (!dataSource) { + throw new Error('dataSource are required'); + } + + return { + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: false, + }, + 'x-acl-action': `${resourceName}:update`, + 'x-decorator': 'FormBlockProvider', + 'x-use-decorator-props': 'useEditFormBlockDecoratorProps', + 'x-decorator-props': { + action: 'get', + dataSource, + collection: collectionName, + association, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:editForm', + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useEditFormBlockProps', + properties: { + grid: templateSchema || { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + properties: {}, + }, + [uid()]: { + type: 'void', + 'x-initializer': 'editForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + style: { + marginTop: 24, + }, + }, + }, + }, + }, + }, + }; +} diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useCreateFormBlockDecoratorProps.ts b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useCreateFormBlockDecoratorProps.ts new file mode 100644 index 000000000..d0e78303c --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useCreateFormBlockDecoratorProps.ts @@ -0,0 +1,15 @@ +import { useFormBlockSourceId } from './useFormBlockSourceId'; + +export function useCreateFormBlockDecoratorProps(props) { + let sourceId; + + // association 的值是固定不变的,所以这里可以使用 hooks + if (props.association) { + // eslint-disable-next-line react-hooks/rules-of-hooks + sourceId = useFormBlockSourceId(props); + } + + return { + sourceId, + }; +} diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useCreateFormBlockProps.ts b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useCreateFormBlockProps.ts new file mode 100644 index 000000000..992e8ed52 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useCreateFormBlockProps.ts @@ -0,0 +1,5 @@ +import { useFormBlockProps } from '../../../../../block-provider/FormBlockProvider'; + +export function useCreateFormBlockProps() { + return useFormBlockProps(); +} diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockDecoratorProps.ts b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockDecoratorProps.ts new file mode 100644 index 000000000..af759e656 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockDecoratorProps.ts @@ -0,0 +1,22 @@ +import { useParamsFromRecord } from '../../../../../block-provider/BlockProvider'; +import { useFormBlockSourceId } from './useFormBlockSourceId'; + +export function useEditFormBlockDecoratorProps(props) { + const params = useFormBlockParams(); + let sourceId; + + // association 的值是固定不变的,所以这里可以使用 hooks + if (props.association) { + // eslint-disable-next-line react-hooks/rules-of-hooks + sourceId = useFormBlockSourceId(props); + } + + return { + params, + sourceId, + }; +} + +function useFormBlockParams() { + return useParamsFromRecord(); +} diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockProps.ts b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockProps.ts new file mode 100644 index 000000000..840a02da1 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useEditFormBlockProps.ts @@ -0,0 +1,5 @@ +import { useFormBlockProps } from '../../../../../block-provider/FormBlockProvider'; + +export function useEditFormBlockProps() { + return useFormBlockProps(); +} diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useFormBlockSourceId.ts b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useFormBlockSourceId.ts new file mode 100644 index 000000000..5863c1d6f --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/form/hooks/useFormBlockSourceId.ts @@ -0,0 +1,5 @@ +import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId'; + +export function useFormBlockSourceId(props) { + return useDataBlockSourceId(props); +} diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/index.ts b/packages/core/client/src/modules/blocks/data-blocks/form/index.ts index b78f5584f..4e579df30 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/index.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/index.ts @@ -1,11 +1,12 @@ -export * from './createFormBlockSettings'; export * from './CreateFormBlockInitializer'; export * from './FormBlockInitializer'; export * from './FormItemSchemaToolbar'; export * from './RecordFormBlockInitializer'; -export * from './editFormBlockSettings'; +export * from './createCreateFormBlockUISchema'; export * from './createFormActionInitializers'; +export * from './createFormBlockSettings'; +export * from './editFormBlockSettings'; +export * from './fieldSettingsFormItem'; export * from './formActionInitializers'; export * from './formItemInitializers'; -export * from './fieldSettingsFormItem'; export * from './updateFormActionInitializers'; diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/ListBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/list/ListBlockInitializer.tsx index cd74176d2..c73b94654 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/list/ListBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/list/ListBlockInitializer.tsx @@ -2,7 +2,7 @@ import { OrderedListOutlined } from '@ant-design/icons'; import React from 'react'; import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; import { useCollectionManager_deprecated } from '../../../../collection-manager'; -import { createListBlockSchema } from '../../../../schema-initializer/utils'; +import { createListBlockSchema } from './createListBlockSchema'; import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer'; import { Collection, CollectionFieldOptions } from '../../../../data-source/collection/Collection'; @@ -48,10 +48,9 @@ export const ListBlockInitializer = ({ const collection = getCollection(item.name, item.dataSource); const schema = createListBlockSchema({ - collection: item.name, + collectionName: item.name, dataSource: item.dataSource, rowKey: collection.filterTargetKey || 'id', - settings: 'blockSettings:list', }); insert(schema); }} diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/__tests__/createListBlockSchema.test.ts b/packages/core/client/src/modules/blocks/data-blocks/list/__tests__/createListBlockSchema.test.ts new file mode 100644 index 000000000..20772fe13 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/list/__tests__/createListBlockSchema.test.ts @@ -0,0 +1,97 @@ +import { createListBlockSchema } from '../createListBlockSchema'; + +describe('createListBlockSchema', () => { + test('should return the correct schema', () => { + const options = { + collectionName: 'users', + dataSource: 'users', + association: 'user', + templateSchema: { + type: 'object', + properties: { + name: { + type: 'string', + }, + age: { + type: 'number', + }, + }, + }, + rowKey: 'id', + }; + + const schema = createListBlockSchema(options); + + expect(schema).toMatchInlineSnapshot(` + { + "properties": { + "actionBar": { + "type": "void", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)", + }, + }, + "x-initializer": "list:configureActions", + }, + "list": { + "properties": { + "item": { + "properties": { + "actionBar": { + "type": "void", + "x-align": "left", + "x-component": "ActionBar", + "x-component-props": { + "layout": "one-column", + }, + "x-initializer": "list:configureItemActions", + "x-use-component-props": "useListActionBarProps", + }, + "grid": { + "properties": { + "age": { + "type": "number", + }, + "name": { + "type": "string", + }, + }, + "type": "object", + }, + }, + "type": "object", + "x-component": "List.Item", + "x-read-pretty": true, + "x-use-component-props": "useListItemProps", + }, + }, + "type": "array", + "x-component": "List", + "x-use-component-props": "useListBlockProps", + }, + }, + "type": "void", + "x-acl-action": "user:view", + "x-component": "CardItem", + "x-decorator": "List.Decorator", + "x-decorator-props": { + "action": "list", + "association": "user", + "collection": "users", + "dataSource": "users", + "params": { + "pageSize": 10, + }, + "readPretty": true, + "rowKey": "id", + "runWhenParamsChanged": true, + }, + "x-settings": "blockSettings:list", + "x-toolbar": "BlockSchemaToolbar", + "x-use-decorator-props": "useListBlockDecoratorProps", + } + `); + }); +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/createListBlockSchema.ts b/packages/core/client/src/modules/blocks/data-blocks/list/createListBlockSchema.ts new file mode 100644 index 000000000..fe56872c1 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/list/createListBlockSchema.ts @@ -0,0 +1,76 @@ +import { ISchema } from '@formily/react'; + +export const createListBlockSchema = (options: { + dataSource: string; + collectionName?: string; + association?: string; + templateSchema?: ISchema; + rowKey?: string; +}): ISchema => { + const { collectionName, dataSource, association, templateSchema, rowKey } = options; + const resourceName = association || collectionName; + + return { + type: 'void', + 'x-acl-action': `${resourceName}:view`, + 'x-decorator': 'List.Decorator', + 'x-use-decorator-props': 'useListBlockDecoratorProps', + 'x-decorator-props': { + collection: collectionName, + dataSource, + association, + readPretty: true, + action: 'list', + params: { + pageSize: 10, + }, + runWhenParamsChanged: true, + rowKey, + }, + 'x-component': 'CardItem', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:list', + properties: { + actionBar: { + type: 'void', + 'x-initializer': 'list:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + }, + list: { + type: 'array', + 'x-component': 'List', + 'x-use-component-props': 'useListBlockProps', + properties: { + item: { + type: 'object', + 'x-component': 'List.Item', + 'x-read-pretty': true, + 'x-use-component-props': 'useListItemProps', + properties: { + grid: templateSchema || { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'details:configureFields', + }, + actionBar: { + type: 'void', + 'x-align': 'left', + 'x-initializer': 'list:configureItemActions', + 'x-component': 'ActionBar', + 'x-use-component-props': 'useListActionBarProps', + 'x-component-props': { + layout: 'one-column', + }, + }, + }, + }, + }, + }, + }, + }; +}; diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/hooks/useListBlockDecoratorProps.ts b/packages/core/client/src/modules/blocks/data-blocks/list/hooks/useListBlockDecoratorProps.ts new file mode 100644 index 000000000..c63d2936f --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/list/hooks/useListBlockDecoratorProps.ts @@ -0,0 +1,15 @@ +import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId'; + +export function useListBlockDecoratorProps(props) { + let sourceId; + + // 因为 association 的值是固定的,所以这里可以使用 hooks + if (props.association) { + // eslint-disable-next-line react-hooks/rules-of-hooks + sourceId = useDataBlockSourceId({ association: props.association }); + } + + return { + sourceId, + }; +} diff --git a/packages/core/client/src/modules/blocks/data-blocks/table-selector/TableSelectorInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/table-selector/TableSelectorInitializer.tsx index 85733dc98..908a2894a 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table-selector/TableSelectorInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table-selector/TableSelectorInitializer.tsx @@ -2,7 +2,7 @@ import { FormOutlined } from '@ant-design/icons'; import React from 'react'; import { useCollection_deprecated } from '../../../../collection-manager'; -import { createTableSelectorSchema } from '../../../../schema-initializer/utils'; +import { createTableSelectorUISchema } from './createTableSelectorUISchema'; import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; export const TableSelectorInitializer = () => { @@ -16,11 +16,10 @@ export const TableSelectorInitializer = () => { {...others} onClick={async () => { insert( - createTableSelectorSchema({ + createTableSelectorUISchema({ rowKey: collection.filterTargetKey, - collection: collection.name, + collectionName: collection.name, dataSource: collection.dataSource, - resource: collection.name, }), ); }} diff --git a/packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/schemaInitializer.test.ts index 32bd72dce..a4d333bed 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table-selector/__e2e__/schemaInitializer.test.ts @@ -2,7 +2,7 @@ import { Page, expect, test } from '@nocobase/test/e2e'; import { createTable } from './utils'; test.describe('where table data selector can be added', () => { - test('popup', async ({ page, mockPage, mockRecord }) => { + test('popup', async ({ page, mockPage }) => { await createTable({ page, mockPage, fieldName: 'manyToOne' }); // 选中一行数据之后,弹窗自动关闭,且数据被填充到关联字段中 diff --git a/packages/core/client/src/modules/blocks/data-blocks/table-selector/__tests__/createTableSelectorSchema.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table-selector/__tests__/createTableSelectorSchema.test.ts new file mode 100644 index 000000000..1821c6079 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/table-selector/__tests__/createTableSelectorSchema.test.ts @@ -0,0 +1,61 @@ +import { createTableSelectorUISchema } from '../createTableSelectorUISchema'; + +vi.mock('@formily/shared', () => { + return { + uid: () => 'mocked-uid', + }; +}); + +describe('createTableSelectorSchema', () => { + test('should return the correct schema', () => { + const schema = createTableSelectorUISchema({ + collectionName: 'example', + dataSource: 'example', + rowKey: 'id', + }); + + expect(schema).toMatchInlineSnapshot(` + { + "properties": { + "mocked-uid": { + "type": "void", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)", + }, + }, + "x-initializer": "table:configureActions", + }, + "value": { + "type": "array", + "x-component": "TableV2.Selector", + "x-component-props": { + "rowSelection": { + "type": "checkbox", + }, + }, + "x-initializer": "table:configureColumns", + "x-use-component-props": "useTableSelectorProps", + }, + }, + "type": "void", + "x-acl-action": "example:list", + "x-component": "CardItem", + "x-decorator": "TableSelectorProvider", + "x-decorator-props": { + "action": "list", + "collection": "example", + "dataSource": "example", + "params": { + "pageSize": 20, + }, + "rowKey": "id", + }, + "x-settings": "blockSettings:tableSelector", + "x-toolbar": "BlockSchemaToolbar", + "x-use-decorator-props": "useTableSelectorDecoratorProps", + } + `); + }); +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table-selector/createTableSelectorUISchema.ts b/packages/core/client/src/modules/blocks/data-blocks/table-selector/createTableSelectorUISchema.ts new file mode 100644 index 000000000..3fb226e4b --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/table-selector/createTableSelectorUISchema.ts @@ -0,0 +1,56 @@ +import { ISchema } from '@formily/react'; +import { uid } from '@formily/shared'; + +export const createTableSelectorUISchema = (options: { + collectionName: string; + dataSource: string; + rowKey: string; +}): ISchema => { + const { collectionName, dataSource, rowKey } = options; + + if (!collectionName || !dataSource || !rowKey) { + throw new Error('collectionName, dataSource, rowKey is required'); + } + + return { + type: 'void', + 'x-acl-action': `${collectionName}:list`, + 'x-decorator': 'TableSelectorProvider', + 'x-use-decorator-props': 'useTableSelectorDecoratorProps', + 'x-decorator-props': { + collection: collectionName, + dataSource, + action: 'list', + params: { + pageSize: 20, + }, + rowKey, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:tableSelector', + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + }, + value: { + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2.Selector', + 'x-use-component-props': 'useTableSelectorProps', + 'x-component-props': { + rowSelection: { + type: 'checkbox', + }, + }, + }, + }, + }; +}; diff --git a/packages/core/client/src/modules/blocks/data-blocks/table-selector/hooks/useTableSelectorDecoratorProps.ts b/packages/core/client/src/modules/blocks/data-blocks/table-selector/hooks/useTableSelectorDecoratorProps.ts new file mode 100644 index 000000000..634013b9a --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/table-selector/hooks/useTableSelectorDecoratorProps.ts @@ -0,0 +1 @@ +export function useTableSelectorDecoratorProps() {} diff --git a/packages/core/client/src/modules/blocks/filter-blocks/collapse/FilterCollapseBlockInitializer.tsx b/packages/core/client/src/modules/blocks/filter-blocks/collapse/FilterCollapseBlockInitializer.tsx index 075f59343..28016afed 100644 --- a/packages/core/client/src/modules/blocks/filter-blocks/collapse/FilterCollapseBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/filter-blocks/collapse/FilterCollapseBlockInitializer.tsx @@ -2,7 +2,7 @@ import { TableOutlined } from '@ant-design/icons'; import React from 'react'; import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application'; -import { createCollapseBlockSchema } from '../../../../schema-initializer/utils'; +import { createCollapseBlockSchema } from './createFilterCollapseBlockSchema'; import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer'; import { Collection, CollectionFieldOptions } from '../../../../data-source'; @@ -27,7 +27,7 @@ export const FilterCollapseBlockInitializer = ({ onCreateBlockSchema={async ({ item }) => { const schema = createCollapseBlockSchema({ dataSource: item.dataSource, - collection: item.collectionName || item.name, + collectionName: item.collectionName || item.name, // 与数据区块做区分 blockType: 'filter', }); diff --git a/packages/core/client/src/modules/blocks/filter-blocks/collapse/__tests__/createCollapseBlockSchema.test.ts b/packages/core/client/src/modules/blocks/filter-blocks/collapse/__tests__/createCollapseBlockSchema.test.ts new file mode 100644 index 000000000..5fa7b6c27 --- /dev/null +++ b/packages/core/client/src/modules/blocks/filter-blocks/collapse/__tests__/createCollapseBlockSchema.test.ts @@ -0,0 +1,44 @@ +import { createCollapseBlockSchema } from '../createFilterCollapseBlockSchema'; + +vi.mock('@formily/shared', () => ({ + uid: vi.fn().mockReturnValue('mocked-uid'), +})); + +describe('createCollapseBlockSchema', () => { + it('should return the correct schema', () => { + const options = { + collectionName: 'testCollection', + dataSource: 'testDataSource', + blockType: 'testBlockType', + }; + + const schema = createCollapseBlockSchema(options); + + expect(schema).toEqual({ + type: 'void', + 'x-decorator': 'AssociationFilter.Provider', + 'x-use-decorator-props': 'useCollapseBlockDecoratorProps', + 'x-decorator-props': { + collection: 'testCollection', + dataSource: 'testDataSource', + blockType: 'testBlockType', + associationFilterStyle: { + width: '100%', + }, + name: 'filter-collapse', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:filterCollapse', + 'x-component': 'CardItem', + 'x-filter-targets': [], + properties: { + 'mocked-uid': { + type: 'void', + 'x-action': 'associateFilter', + 'x-initializer': 'filterCollapse:configureFields', + 'x-component': 'AssociationFilter', + }, + }, + }); + }); +}); diff --git a/packages/core/client/src/modules/blocks/filter-blocks/collapse/createFilterCollapseBlockSchema.ts b/packages/core/client/src/modules/blocks/filter-blocks/collapse/createFilterCollapseBlockSchema.ts new file mode 100644 index 000000000..2d713a61b --- /dev/null +++ b/packages/core/client/src/modules/blocks/filter-blocks/collapse/createFilterCollapseBlockSchema.ts @@ -0,0 +1,37 @@ +import { ISchema } from '@formily/react'; +import { uid } from '@formily/shared'; + +export const createCollapseBlockSchema = (options: { + collectionName: string; + dataSource: string; + blockType: string; +}): ISchema => { + const { collectionName, dataSource, blockType } = options; + + return { + type: 'void', + 'x-decorator': 'AssociationFilter.Provider', + 'x-use-decorator-props': 'useCollapseBlockDecoratorProps', + 'x-decorator-props': { + collection: collectionName, + dataSource, + blockType, + associationFilterStyle: { + width: '100%', + }, + name: 'filter-collapse', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:filterCollapse', + 'x-component': 'CardItem', + 'x-filter-targets': [], + properties: { + [uid()]: { + type: 'void', + 'x-action': 'associateFilter', + 'x-initializer': 'filterCollapse:configureFields', + 'x-component': 'AssociationFilter', + }, + }, + }; +}; diff --git a/packages/core/client/src/modules/blocks/filter-blocks/collapse/hooks/useCollapseBlockDecoratorProps.ts b/packages/core/client/src/modules/blocks/filter-blocks/collapse/hooks/useCollapseBlockDecoratorProps.ts new file mode 100644 index 000000000..eadf9277d --- /dev/null +++ b/packages/core/client/src/modules/blocks/filter-blocks/collapse/hooks/useCollapseBlockDecoratorProps.ts @@ -0,0 +1 @@ +export function useCollapseBlockDecoratorProps() {} diff --git a/packages/core/client/src/modules/fields/component/Picker/TableSelectorInitializers.tsx b/packages/core/client/src/modules/fields/component/Picker/TableSelectorInitializers.tsx index 403ea1d41..319d66c17 100644 --- a/packages/core/client/src/modules/fields/component/Picker/TableSelectorInitializers.tsx +++ b/packages/core/client/src/modules/fields/component/Picker/TableSelectorInitializers.tsx @@ -2,11 +2,7 @@ import { useCollection_deprecated } from '../../../..'; import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer'; import { gridRowColWrap } from '../../../../schema-initializer/utils'; -/** - * @deprecated - */ -export const tableSelectorInitializers_deprecated = new CompatibleSchemaInitializer({ - name: 'TableSelectorInitializers', +const commonOptions = { wrap: gridRowColWrap, title: '{{t("Add block")}}', icon: 'PlusOutlined', @@ -84,88 +80,20 @@ export const tableSelectorInitializers_deprecated = new CompatibleSchemaInitiali ], }, ], +}; + +/** + * @deprecated + */ +export const tableSelectorInitializers_deprecated = new CompatibleSchemaInitializer({ + name: 'TableSelectorInitializers', + ...commonOptions, }); export const tableSelectorInitializers = new CompatibleSchemaInitializer( { name: 'popup:tableSelector:addBlock', - wrap: gridRowColWrap, - title: '{{t("Add block")}}', - icon: 'PlusOutlined', - items: [ - { - type: 'itemGroup', - title: '{{t("Selector")}}', - name: 'selector', - children: [ - { - name: 'title', - title: 'Table', - Component: 'TableSelectorInitializer', - }, - ], - }, - { - type: 'itemGroup', - title: '{{t("Filter blocks")}}', - name: 'filterBlocks', - useChildren() { - const { name, dataSource } = useCollection_deprecated(); - return [ - { - name: 'filterFormBlockInTableSelector', - title: '{{t("Form")}}', - Component: 'FilterFormBlockInitializer', - componentProps: { - filterCollections() { - return false; - }, - onlyCurrentDataSource: true, - }, - collectionName: name, - dataSource, - }, - { - name: 'filterCollapseBlockInTableSelector', - title: '{{t("Collapse")}}', - Component: 'FilterCollapseBlockInitializer', - componentProps: { - filterCollections() { - return false; - }, - onlyCurrentDataSource: true, - }, - collectionName: name, - dataSource, - }, - ]; - }, - }, - { - type: 'itemGroup', - title: '{{t("Other blocks")}}', - name: 'otherBlocks', - children: [ - { - title: '{{t("Add text")}}', - Component: 'BlockItemInitializer', - name: 'addText', - schema: { - type: 'void', - 'x-editable': false, - 'x-decorator': 'BlockItem', - // 'x-designer': 'Markdown.Void.Designer', - 'x-toolbar': 'BlockSchemaToolbar', - 'x-settings': 'blockSettings:markdown', - 'x-component': 'Markdown.Void', - 'x-component-props': { - content: '{{t("This is a demo text, **supports Markdown syntax**.")}}', - }, - }, - }, - ], - }, - ], + ...commonOptions, }, tableSelectorInitializers_deprecated, ); diff --git a/packages/core/client/src/schema-component/antd/action/ActionBar.tsx b/packages/core/client/src/schema-component/antd/action/ActionBar.tsx index c703b7e1c..bd6da7fee 100644 --- a/packages/core/client/src/schema-component/antd/action/ActionBar.tsx +++ b/packages/core/client/src/schema-component/antd/action/ActionBar.tsx @@ -6,6 +6,7 @@ import { createPortal } from 'react-dom'; import { DndContext } from '../../common'; import { useDesignable, useProps } from '../../hooks'; import { useSchemaInitializerRender } from '../../../application'; +import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps'; interface ActionBarContextForceProps { layout?: 'one-column' | 'tow-columns'; @@ -46,10 +47,13 @@ const Portal: React.FC = (props) => { ); }; -export const ActionBar = observer( - (props: any) => { +export const ActionBar = withDynamicSchemaProps( + observer((props: any) => { const { forceProps = {} } = useActionBarContext(); + + // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema const { layout = 'tow-columns', style, spaceProps, ...others } = { ...useProps(props), ...forceProps } as any; + const fieldSchema = useFieldSchema(); const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']); const { designable } = useDesignable(); @@ -128,6 +132,6 @@ export const ActionBar = observer( {render()} ); - }, + }), { displayName: 'ActionBar' }, ); diff --git a/packages/core/client/src/schema-component/antd/details/Details.tsx b/packages/core/client/src/schema-component/antd/details/Details.tsx index 14025ff9b..06bf20f36 100644 --- a/packages/core/client/src/schema-component/antd/details/Details.tsx +++ b/packages/core/client/src/schema-component/antd/details/Details.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { FormV2 } from '../form-v2'; -import { useDetailsBlockContext } from '../../../block-provider/DetailsBlockProvider'; import _ from 'lodash'; import { Empty } from 'antd'; import { useDataBlockRequest } from '../../../data-source'; diff --git a/packages/core/client/src/schema-component/antd/form-v2/Form.tsx b/packages/core/client/src/schema-component/antd/form-v2/Form.tsx index 2358b4894..7890fb896 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/Form.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/Form.tsx @@ -18,6 +18,7 @@ import { isVariable, REGEX_OF_VARIABLE } from '../../../variables/utils/isVariab import { getInnermostKeyAndValue, getTargetField } from '../../common/utils/uitls'; import { useProps } from '../../hooks/useProps'; import { collectFieldStateOfLinkageRules, getTempFieldState } from './utils'; +import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps'; export interface FormProps { [key: string]: any; @@ -197,14 +198,18 @@ export const Form: React.FC & { FilterDesigner?: any; ReadPrettyDesigner?: any; Templates?: any; -} = observer( - (props) => { +} = withDynamicSchemaProps( + observer((props) => { const field = useField(); + + // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema const { form, disabled, ...others } = useProps(props); + const formDisabled = disabled || field.disabled; return (
e.preventDefault()} className={css` .ant-formily-item-feedback-layout-loose { margin-bottom: 12px; @@ -221,7 +226,7 @@ export const Form: React.FC & {
); - }, + }), { displayName: 'Form' }, ); diff --git a/packages/core/client/src/schema-component/antd/list/List.Decorator.tsx b/packages/core/client/src/schema-component/antd/list/List.Decorator.tsx index 13f6e4ca8..e014f3f40 100644 --- a/packages/core/client/src/schema-component/antd/list/List.Decorator.tsx +++ b/packages/core/client/src/schema-component/antd/list/List.Decorator.tsx @@ -5,6 +5,7 @@ import { FormContext, useField } from '@formily/react'; import _ from 'lodash'; import React, { createContext, useContext, useEffect, useMemo } from 'react'; import { BlockProvider, useBlockRequestContext, useParsedFilter } from '../../../block-provider'; +import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps'; export const ListBlockContext = createContext({}); ListBlockContext.displayName = 'ListBlockContext'; @@ -52,7 +53,7 @@ const InternalListBlockProvider = (props) => { ); }; -export const ListBlockProvider = (props) => { +export const ListBlockProvider = withDynamicSchemaProps((props) => { const { params } = props; const { filter: parsedFilter } = useParsedFilter({ filterOption: params?.filter, @@ -75,7 +76,7 @@ export const ListBlockProvider = (props) => { ); -}; +}); export const useListBlockContext = () => { return useContext(ListBlockContext); diff --git a/packages/core/client/src/schema-component/antd/list/List.Item.tsx b/packages/core/client/src/schema-component/antd/list/List.Item.tsx index ff60c56f1..d5d83a99b 100644 --- a/packages/core/client/src/schema-component/antd/list/List.Item.tsx +++ b/packages/core/client/src/schema-component/antd/list/List.Item.tsx @@ -7,8 +7,9 @@ import { useDesignable } from '../../hooks'; import { useCollectionParentRecordData } from '../../../data-source/collection-record/CollectionRecordProvider'; import { RecordProvider } from '../../../record-provider'; +import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps'; -export const ListItem = (props) => { +export const ListItem = withDynamicSchemaProps((props) => { const field = useField(); const { designable } = useDesignable(); const parentRecordData = useCollectionParentRecordData(); @@ -29,4 +30,4 @@ export const ListItem = (props) => { ); -}; +}); diff --git a/packages/core/client/src/schema-component/antd/list/List.tsx b/packages/core/client/src/schema-component/antd/list/List.tsx index 2df4977d5..694be37a4 100644 --- a/packages/core/client/src/schema-component/antd/list/List.tsx +++ b/packages/core/client/src/schema-component/antd/list/List.tsx @@ -11,6 +11,7 @@ import { ListDesigner } from './List.Designer'; import { ListItem } from './List.Item'; import useStyles from './List.style'; import { useListActionBarProps } from './hooks'; +import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps'; const InternalList = (props) => { const { service } = useListBlockContext(); @@ -93,7 +94,7 @@ const InternalList = (props) => { ); }; -export const List = InternalList as typeof InternalList & { +export const List = withDynamicSchemaProps(InternalList) as typeof InternalList & { Item: typeof ListItem; Designer: typeof ListDesigner; Decorator: typeof ListBlockProvider; diff --git a/packages/core/client/src/schema-component/hooks/useProps.ts b/packages/core/client/src/schema-component/hooks/useProps.ts index f4f93fbe8..acad65d9f 100644 --- a/packages/core/client/src/schema-component/hooks/useProps.ts +++ b/packages/core/client/src/schema-component/hooks/useProps.ts @@ -8,6 +8,12 @@ interface Options { } const useDef = () => ({}); + +/** + * 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema + * @param originalProps + * @returns + */ export const useProps = (originalProps: any = {}) => { const { useProps: useDynamicHook = useDef, ...others } = originalProps; let useDynamicProps = useDynamicHook; diff --git a/packages/core/client/src/schema-initializer/items/RecordAssociationFormBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordAssociationFormBlockInitializer.tsx index 9b73c2ae4..a64df1e7f 100644 --- a/packages/core/client/src/schema-initializer/items/RecordAssociationFormBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordAssociationFormBlockInitializer.tsx @@ -1,10 +1,11 @@ -import React, { useCallback, useMemo } from 'react'; import { FormOutlined } from '@ant-design/icons'; +import React, { useCallback, useMemo } from 'react'; import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -import { useSchemaTemplateManager } from '../../schema-templates'; -import { createFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; import { useCollectionManager_deprecated } from '../../collection-manager'; +import { createCreateFormBlockUISchema } from '../../modules/blocks/data-blocks/form/createCreateFormBlockUISchema'; +import { useSchemaTemplateManager } from '../../schema-templates'; +import { useRecordCollectionDataSourceItems } from '../utils'; /** * @deprecated @@ -18,31 +19,18 @@ export const RecordAssociationFormBlockInitializer = () => { const field = itemConfig.field; const collectionName = field.target; const collection = useMemo(() => getCollection(collectionName), [collectionName]); - const resource = `${field.collectionName}.${field.name}`; return ( } {...others} onClick={async ({ item }) => { - const action = ['hasOne', 'belongsTo'].includes(field.type) ? 'get' : null; - const actionInitializers = ['hasOne', 'belongsTo'].includes(field.type) - ? 'editForm:configureActions' - : 'createForm:configureActions'; - if (item.template) { const template = await getTemplateSchemaByMode(item); if (item.template.componentName === 'FormItem') { - const blockSchema = createFormBlockSchema({ - collection: collectionName, + const blockSchema = createCreateFormBlockUISchema({ dataSource: collection.dataSource, - resource, - association: resource, - action, - useSourceId: '{{ useSourceIdFromParentRecord }}', - useParams: '{{ useParamsFromRecord }}', - actionInitializers, - template: template, - settings: 'blockSettings:createForm', + association: `${field.collectionName}.${field.name}`, + templateSchema: template, }); if (item.mode === 'reference') { blockSchema['x-template-key'] = item.template.key; @@ -53,21 +41,19 @@ export const RecordAssociationFormBlockInitializer = () => { } } else { insert( - createFormBlockSchema({ - collection: collectionName, - resource, + createCreateFormBlockUISchema({ dataSource: collection.dataSource, - association: resource, - action, - useSourceId: '{{ useSourceIdFromParentRecord }}', - useParams: '{{ useParamsFromRecord }}', - actionInitializers, - settings: 'blockSettings:createForm', + association: `${field.collectionName}.${field.name}`, }), ); } }} - items={useRecordCollectionDataSourceItems('FormItem', itemConfig, collection, resource)} + items={useRecordCollectionDataSourceItems( + 'FormItem', + itemConfig, + collection, + `${field.collectionName}.${field.name}`, + )} /> ); }; @@ -80,21 +66,11 @@ export function useCreateAssociationFormBlock() { ({ item }) => { const field = item.associationField; const collection = getCollection(field.target); - const action = ['hasOne', 'belongsTo'].includes(field.type) ? 'get' : null; - const actionInitializers = ['hasOne', 'belongsTo'].includes(field.type) - ? 'editForm:configureActions' - : 'createForm:configureActions'; insert( - createFormBlockSchema({ - collection: field.target, + createCreateFormBlockUISchema({ dataSource: collection.dataSource, association: `${field.collectionName}.${field.name}`, - action, - useSourceId: '{{ useSourceIdFromParentRecord }}', - useParams: '{{ useParamsFromRecord }}', - actionInitializers, - settings: 'blockSettings:createForm', }), ); }, @@ -104,23 +80,13 @@ export function useCreateAssociationFormBlock() { const templateWrap = useCallback( (templateSchema, { item }) => { const field = item.associationField; - const action = ['hasOne', 'belongsTo'].includes(field.type) ? 'get' : null; const collection = getCollection(field.target); - const actionInitializers = ['hasOne', 'belongsTo'].includes(field.type) - ? 'editForm:configureActions' - : 'createForm:configureActions'; if (item.template.componentName === 'FormItem') { - const blockSchema = createFormBlockSchema({ - collection: field.target, + const blockSchema = createCreateFormBlockUISchema({ dataSource: collection.dataSource, association: `${field.collectionName}.${field.name}`, - action, - useSourceId: '{{ useSourceIdFromParentRecord }}', - useParams: '{{ useParamsFromRecord }}', - actionInitializers, - template: templateSchema, - settings: 'blockSettings:createForm', + templateSchema: templateSchema, }); if (item.mode === 'reference') { blockSchema['x-template-key'] = item.template.key; diff --git a/packages/core/client/src/schema-initializer/items/RecordAssociationListBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordAssociationListBlockInitializer.tsx index 444841954..4e47b3e70 100644 --- a/packages/core/client/src/schema-initializer/items/RecordAssociationListBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordAssociationListBlockInitializer.tsx @@ -4,7 +4,8 @@ import React, { useCallback } from 'react'; import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; import { useCollectionManager_deprecated } from '../../collection-manager'; import { useSchemaTemplateManager } from '../../schema-templates'; -import { createListBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; +import { useRecordCollectionDataSourceItems } from '../utils'; +import { createListBlockSchema } from '../../modules/blocks/data-blocks/list/createListBlockSchema'; export const RecordAssociationListBlockInitializer = () => { const itemConfig = useSchemaInitializerItem(); @@ -28,11 +29,8 @@ export const RecordAssociationListBlockInitializer = () => { insert( createListBlockSchema({ rowKey: collection.filterTargetKey, - collection: field.target, - resource, dataSource: collection.dataSource, association: resource, - settings: 'blockSettings:list', }), ); } @@ -54,10 +52,8 @@ export function useCreateAssociationListBlock() { insert( createListBlockSchema({ rowKey: collection.filterTargetKey, - collection: field.target, dataSource: collection.dataSource, association: `${field.collectionName}.${field.name}`, - settings: 'blockSettings:list', }), ); }, diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts index 6626f2c36..68695ee20 100644 --- a/packages/core/client/src/schema-initializer/utils.ts +++ b/packages/core/client/src/schema-initializer/utils.ts @@ -1,6 +1,7 @@ import { Field, Form } from '@formily/core'; import { ISchema, Schema, useFieldSchema, useForm } from '@formily/react'; import { uid } from '@formily/shared'; +import _ from 'lodash'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -12,13 +13,12 @@ import { useFormActiveFields, useFormBlockContext, } from '../'; -import { FieldOptions, useCollection_deprecated, useCollectionManager_deprecated } from '../collection-manager'; +import { FieldOptions, useCollectionManager_deprecated, useCollection_deprecated } from '../collection-manager'; +import { Collection, CollectionFieldOptions } from '../data-source/collection/Collection'; +import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider'; import { isAssocField } from '../filter-provider/utils'; import { useActionContext, useCompile, useDesignable } from '../schema-component'; import { useSchemaTemplateManager } from '../schema-templates'; -import { Collection, CollectionFieldOptions } from '../data-source/collection/Collection'; -import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider'; -import _ from 'lodash'; export const itemsMerge = (items1) => { return items1; @@ -972,94 +972,6 @@ export const createDetailsBlockSchema = (options) => { return schema; }; -export const createListBlockSchema = (options) => { - const { - formItemInitializers = 'details:configureFields', - actionInitializers = 'list:configureActions', - itemActionInitializers = 'list:configureItemActions', - collection, - dataSource, - association, - template, - settings, - ...others - } = options; - const resourceName = association || collection; - const schema: ISchema = { - type: 'void', - 'x-acl-action': `${resourceName}:view`, - 'x-decorator': 'List.Decorator', - 'x-decorator-props': { - collection, - dataSource, - association, - readPretty: true, - action: 'list', - params: { - pageSize: 10, - }, - runWhenParamsChanged: true, - ...others, - }, - 'x-component': 'CardItem', - 'x-toolbar': 'BlockSchemaToolbar', - 'x-settings': settings, - properties: { - actionBar: { - type: 'void', - 'x-initializer': actionInitializers, - 'x-component': 'ActionBar', - 'x-component-props': { - style: { - marginBottom: 'var(--nb-spacing)', - }, - }, - properties: {}, - }, - list: { - type: 'array', - 'x-component': 'List', - 'x-component-props': { - props: '{{ useListBlockProps }}', - }, - properties: { - item: { - type: 'object', - 'x-component': 'List.Item', - 'x-read-pretty': true, - 'x-component-props': { - useProps: '{{ useListItemProps }}', - }, - properties: { - grid: template || { - type: 'void', - 'x-component': 'Grid', - 'x-initializer': formItemInitializers, - 'x-initializer-props': { - useProps: '{{ useListItemInitializerProps }}', - }, - properties: {}, - }, - actionBar: { - type: 'void', - 'x-align': 'left', - 'x-initializer': itemActionInitializers, - 'x-component': 'ActionBar', - 'x-component-props': { - useProps: '{{ useListActionBarProps }}', - layout: 'one-column', - }, - properties: {}, - }, - }, - }, - }, - }, - }, - }; - return schema; -}; - export const createGridCardBlockSchema = (options) => { const { formItemInitializers = 'details:configureFields', @@ -1150,7 +1062,28 @@ export const createGridCardBlockSchema = (options) => { }; return schema; }; -export const createFormBlockSchema = (options) => { + +/** + * @deprecated + * 已弃用,请使用 createCreateFormBlockUISchema 或者 createEditFormBlockUISchema 替代 + * @param options + * @returns + */ +export const createFormBlockSchema = (options: { + formItemInitializers?: string; + actionInitializers?: string; + collection: string; + resource?: string; + dataSource?: string; + association?: string; + action?: string; + actions?: Record; + template?: any; + title?: string; + settings?: any; + 'x-designer'?: string; + [key: string]: any; +}) => { const { formItemInitializers = 'form:configureFields', actionInitializers = 'createForm:configureActions', @@ -1453,87 +1386,6 @@ export const createTableBlockSchema = (options) => { return schema; }; -export const createCollapseBlockSchema = (options) => { - const { collection, dataSource, blockType } = options; - const schema: ISchema = { - type: 'void', - 'x-decorator': 'AssociationFilter.Provider', - 'x-decorator-props': { - collection, - dataSource, - blockType, - associationFilterStyle: { - width: '100%', - }, - name: 'filter-collapse', - }, - 'x-toolbar': 'BlockSchemaToolbar', - 'x-settings': 'blockSettings:filterCollapse', - 'x-component': 'CardItem', - 'x-filter-targets': [], - properties: { - [uid()]: { - type: 'void', - 'x-action': 'associateFilter', - 'x-initializer': 'filterCollapse:configureFields', - 'x-component': 'AssociationFilter', - properties: {}, - }, - }, - }; - - return schema; -}; - -export const createTableSelectorSchema = (options) => { - const { collection, dataSource, resource, rowKey, ...others } = options; - const schema: ISchema = { - type: 'void', - 'x-acl-action': `${resource || collection}:list`, - 'x-decorator': 'TableSelectorProvider', - 'x-decorator-props': { - collection, - resource: resource || collection, - dataSource, - action: 'list', - params: { - pageSize: 20, - }, - rowKey, - ...others, - }, - 'x-toolbar': 'BlockSchemaToolbar', - 'x-settings': 'blockSettings:tableSelector', - 'x-component': 'CardItem', - properties: { - [uid()]: { - type: 'void', - 'x-initializer': 'table:configureActions', - 'x-component': 'ActionBar', - 'x-component-props': { - style: { - marginBottom: 'var(--nb-spacing)', - }, - }, - properties: {}, - }, - value: { - type: 'array', - 'x-initializer': 'table:configureColumns', - 'x-component': 'TableV2.Selector', - 'x-component-props': { - rowSelection: { - type: 'checkbox', - }, - useProps: '{{ useTableSelectorProps }}', - }, - properties: {}, - }, - }, - }; - return schema; -}; - const getChildren = ({ collections, dataSource, diff --git a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/CreateFormBulkEditBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/CreateFormBulkEditBlockInitializer.tsx index f5afd1b22..ab2291b42 100644 --- a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/CreateFormBulkEditBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/CreateFormBulkEditBlockInitializer.tsx @@ -1,15 +1,16 @@ import { FormOutlined } from '@ant-design/icons'; -import React from 'react'; import { + SchemaInitializerItem, + createCreateFormBlockUISchema, useBlockAssociationContext, useCollection_deprecated, - useSchemaTemplateManager, - createFormBlockSchema, useRecordCollectionDataSourceItems, - SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem, + useSchemaTemplateManager, } from '@nocobase/client'; +import React from 'react'; +import { createBulkEditBlockUISchema } from './createBulkEditBlockUISchema'; export const CreateFormBulkEditBlockInitializer = () => { const itemConfig = useSchemaInitializerItem(); @@ -26,13 +27,19 @@ export const CreateFormBulkEditBlockInitializer = () => { if (item.template) { const s = await getTemplateSchemaByMode(item); if (item.template.componentName === 'FormItem') { - const blockSchema = createFormBlockSchema({ - actionInitializers: 'createForm:configureActions', - association, - collection: collection.name, - dataSource: collection.dataSource, - template: s, - }); + const blockSchema = createCreateFormBlockUISchema( + association + ? { + association, + dataSource: collection.dataSource, + templateSchema: s, + } + : { + collectionName: collection.name, + dataSource: collection.dataSource, + templateSchema: s, + }, + ); if (item.mode === 'reference') { blockSchema['x-template-key'] = item.template.key; } @@ -42,11 +49,9 @@ export const CreateFormBulkEditBlockInitializer = () => { } } else { insert( - createFormBlockSchema({ - formItemInitializers: 'bulkEditForm:configureFields', - actionInitializers: 'bulkEditForm:configureActions', + createBulkEditBlockUISchema({ association, - collection: collection.name, + collectionName: collection.name, dataSource: collection.dataSource, }), ); diff --git a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/createBulkEditBlockUISchema.ts b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/createBulkEditBlockUISchema.ts new file mode 100644 index 000000000..208e44e7b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/createBulkEditBlockUISchema.ts @@ -0,0 +1,65 @@ +import { ISchema } from '@formily/react'; +import { uid } from '@formily/shared'; + +/** + * 创建批量编辑表单的 UI Schema + * @returns + */ +export function createBulkEditBlockUISchema(options: { + collectionName: string; + dataSource: string; + association?: string; +}): ISchema { + const { collectionName, association, dataSource } = options; + const resourceName = association || collectionName; + + if (!collectionName || !dataSource) { + throw new Error('collectionName and dataSource are required'); + } + + const schema: ISchema = { + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-acl-action': `${resourceName}:create`, + 'x-decorator': 'FormBlockProvider', + 'x-decorator-props': { + dataSource, + collection: collectionName, + association, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:createForm', + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'void', + 'x-component': 'FormV2', + 'x-component-props': { + useProps: '{{ useCreateFormBlockProps }}', + }, + properties: { + grid: { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'bulkEditForm:configureFields', + properties: {}, + }, + [uid()]: { + type: 'void', + 'x-initializer': 'bulkEditForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + style: { + marginTop: 24, + }, + }, + }, + }, + }, + }, + }; + return schema; +} diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/__e2e__/schemaInitializer.test.ts b/packages/plugins/@nocobase/plugin-calendar/src/client/__e2e__/schemaInitializer.test.ts new file mode 100644 index 000000000..2a2792c66 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/__e2e__/schemaInitializer.test.ts @@ -0,0 +1,35 @@ +import { test, expect } from '@nocobase/test/e2e'; +import { emptyPageWithCalendarCollection, oneTableWithCalendarCollection } from './templates'; + +test.describe('where can be added', () => { + test('page', async ({ page, mockPage }) => { + await mockPage(emptyPageWithCalendarCollection).goto(); + + await page.getByLabel('schema-initializer-Grid-page:').hover(); + await page.getByRole('menuitem', { name: 'form Calendar right' }).hover(); + await page.getByRole('menuitem', { name: 'calendar', exact: true }).click(); + + await page.getByLabel('block-item-Select-Title field').getByTestId('select-single').click(); + await page.getByRole('option', { name: 'Repeats' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + await expect(page.getByLabel('block-item-CardItem-calendar-').getByText('Sun', { exact: true })).toBeVisible(); + }); + + test('association block in popup', async ({ page, mockPage, mockRecord }) => { + await mockPage(oneTableWithCalendarCollection).goto(); + await mockRecord('toManyCalendar'); + + // 打开弹窗 + await page.getByLabel('action-Action.Link-View-view-').first().click(); + await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'form Calendar right' }).hover(); + await page.getByRole('menuitem', { name: 'manyToMany -> calendar' }).click(); + + await page.getByLabel('block-item-Select-Title field').getByTestId('select-single').click(); + await page.getByRole('option', { name: 'Repeats' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + await expect(page.getByLabel('block-item-CardItem-calendar-').getByText('Sun', { exact: true })).toBeVisible(); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/__e2e__/templates.ts b/packages/plugins/@nocobase/plugin-calendar/src/client/__e2e__/templates.ts new file mode 100644 index 000000000..9198108de --- /dev/null +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/__e2e__/templates.ts @@ -0,0 +1,238 @@ +import { PageConfig } from '@nocobase/test/e2e'; + +const calendarCollection = { + name: 'calendar', + template: 'calendar', +}; + +export const emptyPageWithCalendarCollection: PageConfig = { + collections: [calendarCollection], +}; + +export const oneTableWithCalendarCollection: PageConfig = { + collections: [ + calendarCollection, + { + name: 'toManyCalendar', + fields: [ + { + name: 'manyToMany', + interface: 'm2m', + target: 'calendar', + }, + ], + }, + ], + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + lqs2pzl6li1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: { + viawniezd4p: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + properties: { + s0nef2zgi5m: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + ibwqgtls50q: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'toManyCalendar:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'toManyCalendar', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-uid': 'xi12fv3arso', + 'x-async': false, + 'x-index': 1, + }, + v931jk5mpmg: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-designer': 'TableV2.ActionColumnDesigner', + 'x-initializer': 'table:configureItemActions', + properties: { + w4rc8u7s5q0: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + properties: { + hd95fsevokf: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("View") }}', + 'x-action': 'view', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:view', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'drawer', + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("View record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'TabPaneInitializers', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Details")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:common:addBlock', + 'x-uid': 'wfpwj2q55xi', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'tvwvyrlvpv6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'bys1tnlre1o', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ml2scl3y6se', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'q9gwy03bevt', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'f8i1npjyidq', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'h26hp47w83k', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'bjbs9yvab1k', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '5i6112zy12n', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '6vh9ncefvs2', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'u4b441tpuzq', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'mworqfx7jaf', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'kgor3l32s12', + 'x-async': true, + 'x-index': 1, + }, +}; diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/__tests__/createCalendarBlockSchema.test.ts b/packages/plugins/@nocobase/plugin-calendar/src/client/__tests__/createCalendarBlockSchema.test.ts new file mode 100644 index 000000000..b716f94d0 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/__tests__/createCalendarBlockSchema.test.ts @@ -0,0 +1,116 @@ +import { createCalendarBlockUISchema } from '../schema-initializer/createCalendarBlockUISchema'; + +vi.mock('@formily/shared', async () => { + const actual = await vi.importActual('@formily/shared'); + return { + ...actual, + uid: () => 'mocked-uid', + }; +}); + +describe('createCalendarBlockSchema', () => { + it('should return the correct schema', () => { + const options = { + collectionName: 'users', + dataSource: 'events', + fieldNames: { + title: 'title', + startDate: 'start_date', + endDate: 'end_date', + }, + association: 'users.roles', + }; + + const schema = createCalendarBlockUISchema(options); + + expect(schema).toMatchInlineSnapshot(` + { + "properties": { + "mocked-uid": { + "properties": { + "event": { + "properties": { + "drawer": { + "properties": { + "tabs": { + "properties": { + "tab1": { + "properties": { + "grid": { + "type": "void", + "x-component": "Grid", + "x-initializer": "popup:common:addBlock", + "x-initializer-props": { + "actionInitializers": "details:configureActions", + }, + }, + }, + "title": "{{t('Details', { ns: 'calendar' })}}", + "type": "void", + "x-component": "Tabs.TabPane", + "x-component-props": {}, + "x-designer": "Tabs.Designer", + }, + }, + "type": "void", + "x-component": "Tabs", + "x-component-props": {}, + "x-initializer": "TabPaneInitializers", + "x-initializer-props": { + "gridInitializer": "popup:common:addBlock", + }, + }, + }, + "title": "{{t('View record', { ns: 'calendar' })}}", + "type": "void", + "x-component": "Action.Drawer", + "x-component-props": { + "className": "nb-action-popup", + }, + }, + }, + "type": "void", + "x-component": "CalendarV2.Event", + }, + "toolBar": { + "type": "void", + "x-component": "CalendarV2.ActionBar", + "x-component-props": { + "style": { + "marginBottom": 24, + }, + }, + "x-initializer": "calendar:configureActions", + }, + }, + "type": "void", + "x-component": "CalendarV2", + "x-use-component-props": "useCalendarBlockProps", + }, + }, + "type": "void", + "x-acl-action": "users.roles:list", + "x-component": "CardItem", + "x-decorator": "CalendarBlockProvider", + "x-decorator-props": { + "action": "list", + "association": "users.roles", + "collection": "users", + "dataSource": "events", + "fieldNames": { + "endDate": "end_date", + "id": "id", + "startDate": "start_date", + "title": "title", + }, + "params": { + "paginate": false, + }, + }, + "x-settings": "blockSettings:calendar", + "x-toolbar": "BlockSchemaToolbar", + "x-use-decorator-props": "useCalendarBlockDecoratorProps", + } + `); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calendar.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calendar.tsx index 0a4c1479c..244511a68 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calendar.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calendar.tsx @@ -1,6 +1,12 @@ import { LeftOutlined, RightOutlined } from '@ant-design/icons'; import { RecursionField, Schema, observer, useFieldSchema } from '@formily/react'; -import { ActionContextProvider, RecordProvider, useCollectionParentRecordData, useProps } from '@nocobase/client'; +import { + ActionContextProvider, + RecordProvider, + useCollectionParentRecordData, + useProps, + withDynamicSchemaProps, +} from '@nocobase/client'; import { parseExpression } from 'cron-parser'; import type { Dayjs } from 'dayjs'; import dayjs from 'dayjs'; @@ -171,100 +177,104 @@ const CalendarRecordViewer = (props) => { ); }; -export const Calendar: any = observer( - (props: any) => { - const { dataSource, fieldNames, showLunar, fixedBlock } = useProps(props); - const [date, setDate] = useState(new Date()); - const [view, setView] = useState('month'); - const events = useEvents(dataSource, fieldNames, date, view); - const [visible, setVisible] = useState(false); - const [record, setRecord] = useState({}); - const { wrapSSR, hashId, componentCls: containerClassName } = useStyle(); +export const Calendar: any = withDynamicSchemaProps( + observer( + (props: any) => { + // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema + const { dataSource, fieldNames, showLunar, fixedBlock } = useProps(props); - const components = useMemo(() => { - return { - toolbar: (props) => , - // week: { - // header: (props) =>
, - // }, - month: { - dateHeader: (props) =>
, - }, + const [date, setDate] = useState(new Date()); + const [view, setView] = useState('month'); + const events = useEvents(dataSource, fieldNames, date, view); + const [visible, setVisible] = useState(false); + const [record, setRecord] = useState({}); + const { wrapSSR, hashId, componentCls: containerClassName } = useStyle(); + + const components = useMemo(() => { + return { + toolbar: (props) => , + // week: { + // header: (props) =>
, + // }, + month: { + dateHeader: (props) =>
, + }, + }; + }, [showLunar]); + + const messages: any = { + allDay: '', + previous: ( +
+ +
+ ), + next: ( +
+ +
+ ), + today: i18nt('Today'), + month: i18nt('Month'), + week: i18nt('Week'), + work_week: i18nt('Work week'), + day: i18nt('Day'), + agenda: i18nt('Agenda'), + date: i18nt('Date'), + time: i18nt('Time'), + event: i18nt('Event'), + noEventsInRange: i18nt('None'), + showMore: (count) => i18nt('{{count}} more items', { count }), }; - }, [showLunar]); - - const messages: any = { - allDay: '', - previous: ( -
- -
- ), - next: ( -
- -
- ), - today: i18nt('Today'), - month: i18nt('Month'), - week: i18nt('Week'), - work_week: i18nt('Work week'), - day: i18nt('Day'), - agenda: i18nt('Agenda'), - date: i18nt('Date'), - time: i18nt('Time'), - event: i18nt('Event'), - noEventsInRange: i18nt('None'), - showMore: (count) => i18nt('{{count}} more items', { count }), - }; - return wrapSSR( -
- - - { - console.log('onSelectSlot', slotInfo); - }} - onDoubleClickEvent={() => { - console.log('onDoubleClickEvent'); - }} - onSelectEvent={(event) => { - const record = dataSource?.find((item) => item[fieldNames.id] === event.id); - if (!record) { - return; - } - record.__event = { ...event, start: formatDate(dayjs(event.start)), end: formatDate(dayjs(event.end)) }; - - setRecord(record); - setVisible(true); - }} - formats={{ - monthHeaderFormat: 'YYYY-M', - agendaDateFormat: 'M-DD', - dayHeaderFormat: 'YYYY-M-DD', - dayRangeHeaderFormat: ({ start, end }, culture, local) => { - if (dates.eq(start, end, 'month')) { - return local.format(start, 'YYYY-M', culture); + return wrapSSR( +
+ + + { + console.log('onSelectSlot', slotInfo); + }} + onDoubleClickEvent={() => { + console.log('onDoubleClickEvent'); + }} + onSelectEvent={(event) => { + const record = dataSource?.find((item) => item[fieldNames.id] === event.id); + if (!record) { + return; } - return `${local.format(start, 'YYYY-M', culture)} - ${local.format(end, 'YYYY-M', culture)}`; - }, - }} - components={components} - localizer={localizer} - /> -
, - ); - }, - { displayName: 'Calendar' }, + record.__event = { ...event, start: formatDate(dayjs(event.start)), end: formatDate(dayjs(event.end)) }; + + setRecord(record); + setVisible(true); + }} + formats={{ + monthHeaderFormat: 'YYYY-M', + agendaDateFormat: 'M-DD', + dayHeaderFormat: 'YYYY-M-DD', + dayRangeHeaderFormat: ({ start, end }, culture, local) => { + if (dates.eq(start, end, 'month')) { + return local.format(start, 'YYYY-M', culture); + } + return `${local.format(start, 'YYYY-M', culture)} - ${local.format(end, 'YYYY-M', culture)}`; + }, + }} + components={components} + localizer={localizer} + /> +
, + ); + }, + { displayName: 'Calendar' }, + ), ); diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/hooks/useCalendarBlockDecoratorProps.ts b/packages/plugins/@nocobase/plugin-calendar/src/client/hooks/useCalendarBlockDecoratorProps.ts new file mode 100644 index 000000000..edc67ee95 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/hooks/useCalendarBlockDecoratorProps.ts @@ -0,0 +1,18 @@ +import { useDataBlockSourceId } from '@nocobase/client'; +import { useCalendarBlockParams } from './useCalendarBlockParams'; + +export function useCalendarBlockDecoratorProps(props) { + const params = useCalendarBlockParams(props); + let sourceId: string; + + // 因为 association 是一个固定的值,所以可以在 hooks 中直接使用 + if (props.association) { + // eslint-disable-next-line react-hooks/rules-of-hooks + sourceId = useDataBlockSourceId({ association: props.association }); + } + + return { + params, + sourceId, + }; +} diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/hooks/useCalendarBlockParams.ts b/packages/plugins/@nocobase/plugin-calendar/src/client/hooks/useCalendarBlockParams.ts new file mode 100644 index 000000000..bd4f60af0 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/hooks/useCalendarBlockParams.ts @@ -0,0 +1,22 @@ +import { useMemo } from 'react'; + +export function useCalendarBlockParams(props) { + const appends = useMemo(() => { + const arr: string[] = []; + const start = props.fieldNames?.start; + const end = props.fieldNames?.end; + + if (Array.isArray(start) && start.length >= 2) { + arr.push(start[0]); + } + if (Array.isArray(end) && end.length >= 2) { + arr.push(end[0]); + } + + return arr; + }, [props.fieldNames]); + + return useMemo(() => { + return { ...props.params, appends: [...appends, ...(props.params.appends || [])], paginate: false }; + }, [appends, props.params]); +} diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/index.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/index.tsx index 49e4680f0..9d281eaa3 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/index.tsx @@ -16,6 +16,7 @@ import { useCreateAssociationCalendarBlock, } from './schema-initializer/items'; import { useMemo } from 'react'; +import { useCalendarBlockDecoratorProps } from './hooks/useCalendarBlockDecoratorProps'; export class PluginCalendarClient extends Plugin { async load() { @@ -60,7 +61,7 @@ export class PluginCalendarClient extends Plugin { RecordAssociationCalendarBlockInitializer, CalendarV2, }); - this.app.addScopes({ useCalendarBlockProps }); + this.app.addScopes({ useCalendarBlockProps, useCalendarBlockDecoratorProps }); this.schemaSettingsManager.add(calendarBlockSettings); this.app.schemaInitializerManager.add(CalendarActionInitializers_deprecated); this.app.schemaInitializerManager.add(calendarActionInitializers); diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/CalendarBlockProvider.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/CalendarBlockProvider.tsx index 92fd7822b..04b2f18c7 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/CalendarBlockProvider.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/CalendarBlockProvider.tsx @@ -1,8 +1,15 @@ import { ArrayField } from '@formily/core'; -import { useField } from '@formily/react'; -import { BlockProvider, FixedBlockWrapper, useBlockRequestContext, useParsedFilter } from '@nocobase/client'; +import { useField, useFieldSchema } from '@formily/react'; +import { + BlockProvider, + FixedBlockWrapper, + useBlockRequestContext, + useParsedFilter, + withDynamicSchemaProps, +} from '@nocobase/client'; import _ from 'lodash'; import React, { createContext, useContext, useEffect, useMemo } from 'react'; +import { useCalendarBlockParams } from '../hooks/useCalendarBlockParams'; export const CalendarBlockContext = createContext({}); CalendarBlockContext.displayName = 'CalendarBlockContext'; @@ -38,31 +45,26 @@ const InternalCalendarBlockProvider = (props) => { ); }; -export const CalendarBlockProvider = (props) => { - const appends = useMemo(() => { - const arr: string[] = []; - const start = props.fieldNames?.start; - const end = props.fieldNames?.end; +const useCompatCalendarBlockParams = (props) => { + const fieldSchema = useFieldSchema(); - if (Array.isArray(start) && start.length >= 2) { - arr.push(start[0]); - } - if (Array.isArray(end) && end.length >= 2) { - arr.push(end[0]); - } + // 因为 x-use-decorator-props 的值是固定不变的,所以可以在条件中使用 hooks + if (fieldSchema['x-use-decorator-props']) { + return props.params; + } else { + // eslint-disable-next-line react-hooks/rules-of-hooks + return useCalendarBlockParams(props); + } +}; - return arr; - }, [props.fieldNames]); +export const CalendarBlockProvider = withDynamicSchemaProps((props) => { + const params = useCompatCalendarBlockParams(props); return ( - + ); -}; +}); export const useCalendarBlockContext = () => { return useContext(CalendarBlockContext); diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/utils.ts b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/createCalendarBlockUISchema.ts similarity index 82% rename from packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/utils.ts rename to packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/createCalendarBlockUISchema.ts index 2cfced89a..872437377 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/utils.ts +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/createCalendarBlockUISchema.ts @@ -2,16 +2,23 @@ import { ISchema } from '@formily/react'; import { uid } from '@formily/shared'; import { generateNTemplate } from '../../locale'; -export const createCalendarBlockSchema = (options) => { - const { collection, dataSource, resource, fieldNames, settings, ...others } = options; - const schema: ISchema = { +export const createCalendarBlockUISchema = (options: { + dataSource: string; + fieldNames: object; + collectionName?: string; + association?: string; +}): ISchema => { + const { collectionName, dataSource, fieldNames, association } = options; + + return { type: 'void', - 'x-acl-action': `${resource || collection}:list`, + 'x-acl-action': `${association || collectionName}:list`, 'x-decorator': 'CalendarBlockProvider', + 'x-use-decorator-props': 'useCalendarBlockDecoratorProps', 'x-decorator-props': { - collection: collection, + collection: collectionName, dataSource, - resource: resource || collection, + association, action: 'list', fieldNames: { id: 'id', @@ -20,18 +27,15 @@ export const createCalendarBlockSchema = (options) => { params: { paginate: false, }, - ...others, }, 'x-toolbar': 'BlockSchemaToolbar', - 'x-settings': settings, + 'x-settings': 'blockSettings:calendar', 'x-component': 'CardItem', properties: { [uid()]: { type: 'void', 'x-component': 'CalendarV2', - 'x-component-props': { - useProps: '{{ useCalendarBlockProps }}', - }, + 'x-use-component-props': 'useCalendarBlockProps', properties: { toolBar: { type: 'void', @@ -42,7 +46,6 @@ export const createCalendarBlockSchema = (options) => { }, }, 'x-initializer': 'calendar:configureActions', - properties: {}, }, event: { type: 'void', @@ -79,7 +82,6 @@ export const createCalendarBlockSchema = (options) => { actionInitializers: 'details:configureActions', }, 'x-initializer': 'popup:common:addBlock', - properties: {}, }, }, }, @@ -93,6 +95,4 @@ export const createCalendarBlockSchema = (options) => { }, }, }; - - return schema; }; diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx index 8995f0e4a..b2c726e51 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx @@ -14,7 +14,7 @@ import { useSchemaInitializerItem, } from '@nocobase/client'; import React, { useContext } from 'react'; -import { createCalendarBlockSchema } from '../utils'; +import { createCalendarBlockUISchema } from '../createCalendarBlockUISchema'; import { useTranslation } from '../../../locale'; export const CalendarBlockInitializer = ({ @@ -95,13 +95,12 @@ export const CalendarBlockInitializer = ({ initialValues: {}, }); insert( - createCalendarBlockSchema({ - collection: item.name, + createCalendarBlockUISchema({ + collectionName: item.name, dataSource: item.dataSource, fieldNames: { ...values, }, - settings: 'blockSettings:calendar', }), ); }} diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/RecordAssociationCalendarBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/RecordAssociationCalendarBlockInitializer.tsx index 6a2b20ec7..97b0b327d 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/RecordAssociationCalendarBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/RecordAssociationCalendarBlockInitializer.tsx @@ -15,7 +15,7 @@ import { useSchemaInitializer, SchemaInitializerItem, } from '@nocobase/client'; -import { createCalendarBlockSchema } from '../utils'; +import { createCalendarBlockUISchema } from '../createCalendarBlockUISchema'; import { useTranslation } from '../../../locale'; export const RecordAssociationCalendarBlockInitializer = () => { @@ -98,10 +98,9 @@ export const RecordAssociationCalendarBlockInitializer = () => { initialValues: {}, }); insert( - createCalendarBlockSchema({ - collection: field.target, - resource, + createCalendarBlockUISchema({ association: resource, + dataSource: item.dataSource, fieldNames: { ...values, }, @@ -183,13 +182,12 @@ export function useCreateAssociationCalendarBlock() { initialValues: {}, }); insert( - createCalendarBlockSchema({ - collection: field.target, + createCalendarBlockUISchema({ association: `${field.collectionName}.${field.name}`, + dataSource: item.dataSource, fieldNames: { ...values, }, - settings: 'blockSettings:calendar', }), ); }; diff --git a/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockInitializer.tsx index 10835af39..041cc2a22 100644 --- a/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockInitializer.tsx @@ -14,7 +14,7 @@ import { DataBlockInitializer, SchemaComponentOptions, } from '@nocobase/client'; -import { createGanttBlockSchema } from './utils'; +import { createGanttBlockUISchema } from './createGanttBlockUISchema'; export const GanttBlockInitializer = () => { const { insert } = useSchemaInitializer(); @@ -120,8 +120,8 @@ export const GanttBlockInitializer = () => { initialValues: {}, }); insert( - createGanttBlockSchema({ - collection: item.name, + createGanttBlockUISchema({ + collectionName: item.name, dataSource: item.dataSource, fieldNames: { ...values, diff --git a/packages/plugins/@nocobase/plugin-gantt/src/client/__tests__/createGanttBlockSchema.test.ts b/packages/plugins/@nocobase/plugin-gantt/src/client/__tests__/createGanttBlockSchema.test.ts new file mode 100644 index 000000000..9040fcb28 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-gantt/src/client/__tests__/createGanttBlockSchema.test.ts @@ -0,0 +1,143 @@ +import { createGanttBlockUISchema } from '../createGanttBlockUISchema'; + +vi.mock('@formily/shared', () => { + return { + uid: () => 'mocked-uid', + }; +}); + +describe('createGanttBlockSchema', () => { + it('should generate schema correctly', () => { + const options = { + collectionName: 'TestCollection', + fieldNames: { + label: 'field1', + value: 'field2', + }, + dataSource: 'TestDataSource', + }; + const schema = createGanttBlockUISchema(options); + + expect(schema).toMatchInlineSnapshot(` + { + "properties": { + "mocked-uid": { + "properties": { + "detail": { + "properties": { + "drawer": { + "properties": { + "tabs": { + "properties": { + "tab1": { + "properties": { + "grid": { + "type": "void", + "x-component": "Grid", + "x-initializer": "popup:common:addBlock", + }, + }, + "title": "{{t("Details")}}", + "type": "void", + "x-component": "Tabs.TabPane", + "x-component-props": {}, + "x-designer": "Tabs.Designer", + }, + }, + "type": "void", + "x-component": "Tabs", + "x-component-props": {}, + "x-initializer": "TabPaneInitializers", + }, + }, + "title": "{{ t("View record") }}", + "type": "void", + "x-component": "Action.Drawer", + "x-component-props": { + "className": "nb-action-popup", + }, + }, + }, + "type": "void", + "x-component": "Gantt.Event", + }, + "table": { + "properties": { + "actions": { + "properties": { + "actions": { + "type": "void", + "x-component": "Space", + "x-component-props": { + "split": "|", + }, + "x-decorator": "DndContext", + }, + }, + "title": "{{ t("Actions") }}", + "type": "void", + "x-action-column": "actions", + "x-component": "TableV2.Column", + "x-decorator": "TableV2.Column.ActionBar", + "x-designer": "TableV2.ActionColumnDesigner", + "x-initializer": "table:configureItemActions", + }, + }, + "type": "array", + "x-component": "TableV2", + "x-component-props": { + "pagination": false, + "rowKey": "id", + "rowSelection": { + "type": "checkbox", + }, + }, + "x-decorator": "div", + "x-decorator-props": { + "style": { + "float": "left", + "maxWidth": "35%", + }, + }, + "x-initializer": "table:configureColumns", + "x-use-component-props": "useTableBlockProps", + }, + "toolBar": { + "properties": {}, + "type": "void", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": 24, + }, + }, + "x-initializer": "gantt:configureActions", + }, + }, + "type": "void", + "x-component": "Gantt", + "x-use-component-props": "useGanttBlockProps", + }, + }, + "type": "void", + "x-acl-action": "TestCollection:list", + "x-component": "CardItem", + "x-decorator": "GanttBlockProvider", + "x-decorator-props": { + "action": "list", + "collection": "TestCollection", + "dataSource": "TestDataSource", + "fieldNames": { + "label": "field1", + "value": "field2", + }, + "params": { + "paginate": false, + }, + }, + "x-settings": "blockSettings:gantt", + "x-toolbar": "BlockSchemaToolbar", + } + `); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-gantt/src/client/components/gantt/gantt.tsx b/packages/plugins/@nocobase/plugin-gantt/src/client/components/gantt/gantt.tsx index a7f721b0c..e38f8c428 100644 --- a/packages/plugins/@nocobase/plugin-gantt/src/client/components/gantt/gantt.tsx +++ b/packages/plugins/@nocobase/plugin-gantt/src/client/components/gantt/gantt.tsx @@ -9,6 +9,8 @@ import { useCollectionParentRecordData, useTableBlockContext, useToken, + withDynamicSchemaProps, + useProps, } from '@nocobase/client'; import { message } from 'antd'; import { debounce } from 'lodash'; @@ -81,7 +83,8 @@ const debounceHandleProcessChange = debounce(async (task: Task, resource, fieldN message.success(t('Saved successfully')); await service?.refresh(); }, 300); -export const Gantt: any = (props: any) => { + +export const Gantt: any = withDynamicSchemaProps((props: any) => { const { styles } = useStyles(); const { token } = useToken(); const api = useAPIClient(); @@ -116,12 +119,13 @@ export const Gantt: any = (props: any) => { viewDate, TooltipContent = StandardTooltipContent, onDoubleClick, - onClick, onDelete, onSelect, - useProps, - } = props; - const { onExpanderClick, tasks, expandAndCollapseAll } = useProps(); + onExpanderClick, + tasks, + expandAndCollapseAll, + fieldNames, + } = useProps(props); // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema const ctx = useGanttBlockContext(); const appInfo = useCurrentAppInfo(); const { t } = useTranslation(); @@ -129,7 +133,6 @@ export const Gantt: any = (props: any) => { const tableCtx = useTableBlockContext(); const { resource, service } = useBlockRequestContext(); const fieldSchema = useFieldSchema(); - const { fieldNames } = useProps(props); const viewMode = fieldNames.range || 'day'; const wrapperRef = useRef(null); const taskListRef = useRef(null); @@ -559,4 +562,4 @@ export const Gantt: any = (props: any) => { ); -}; +}); diff --git a/packages/plugins/@nocobase/plugin-gantt/src/client/createGanttBlockUISchema.tsx b/packages/plugins/@nocobase/plugin-gantt/src/client/createGanttBlockUISchema.tsx new file mode 100644 index 000000000..8ba59d29c --- /dev/null +++ b/packages/plugins/@nocobase/plugin-gantt/src/client/createGanttBlockUISchema.tsx @@ -0,0 +1,129 @@ +import { ISchema } from '@formily/react'; +import { uid } from '@formily/shared'; + +export const createGanttBlockUISchema = (options: { + collectionName: string; + fieldNames: object; + dataSource: string; +}): ISchema => { + const { collectionName, fieldNames, dataSource } = options; + + return { + type: 'void', + 'x-acl-action': `${collectionName}:list`, + 'x-decorator': 'GanttBlockProvider', + 'x-decorator-props': { + collection: collectionName, + dataSource, + action: 'list', + fieldNames, + params: { + paginate: false, + }, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:gantt', + // 'x-designer': 'Gantt.Designer', + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'void', + 'x-component': 'Gantt', + 'x-use-component-props': 'useGanttBlockProps', + properties: { + toolBar: { + type: 'void', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 24, + }, + }, + 'x-initializer': 'gantt:configureActions', + properties: {}, + }, + table: { + type: 'array', + 'x-decorator': 'div', + 'x-decorator-props': { + style: { + float: 'left', + maxWidth: '35%', + }, + }, + + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + pagination: false, + }, + properties: { + actions: { + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-designer': 'TableV2.ActionColumnDesigner', + 'x-initializer': 'table:configureItemActions', + properties: { + actions: { + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + }, + }, + }, + }, + }, + detail: { + type: 'void', + 'x-component': 'Gantt.Event', + properties: { + drawer: { + type: 'void', + 'x-component': 'Action.Drawer', + 'x-component-props': { + className: 'nb-action-popup', + }, + title: '{{ t("View record") }}', + properties: { + tabs: { + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'TabPaneInitializers', + properties: { + tab1: { + type: 'void', + title: '{{t("Details")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:common:addBlock', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; +}; diff --git a/packages/plugins/@nocobase/plugin-gantt/src/client/index.tsx b/packages/plugins/@nocobase/plugin-gantt/src/client/index.tsx index 496f02507..c24339362 100644 --- a/packages/plugins/@nocobase/plugin-gantt/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-gantt/src/client/index.tsx @@ -41,6 +41,10 @@ export class GanttPlugin extends Plugin { title: "{{t('Gantt')}}", Component: 'GanttBlockInitializer', }); + + this.app.addScopes({ + useGanttBlockProps, + }); } } diff --git a/packages/plugins/@nocobase/plugin-gantt/src/client/utils.tsx b/packages/plugins/@nocobase/plugin-gantt/src/client/utils.tsx index 667011506..43949db23 100644 --- a/packages/plugins/@nocobase/plugin-gantt/src/client/utils.tsx +++ b/packages/plugins/@nocobase/plugin-gantt/src/client/utils.tsx @@ -1,5 +1,3 @@ -import { ISchema } from '@formily/react'; -import { uid } from '@formily/shared'; import { useCollection_deprecated, useCompile } from '@nocobase/client'; import { useTranslation } from 'react-i18next'; @@ -19,131 +17,3 @@ export const useOptions = (type = 'string') => { }); return options; }; -export const createGanttBlockSchema = (options) => { - const { collection, resource, fieldNames, ...others } = options; - const schema: ISchema = { - type: 'void', - 'x-acl-action': `${resource || collection}:list`, - 'x-decorator': 'GanttBlockProvider', - 'x-decorator-props': { - collection: collection, - resource: resource || collection, - action: 'list', - fieldNames: { - ...fieldNames, - }, - params: { - paginate: false, - }, - ...others, - }, - 'x-designer': 'Gantt.Designer', - 'x-component': 'CardItem', - properties: { - [uid()]: { - type: 'void', - 'x-component': 'Gantt', - 'x-component-props': { - useProps: '{{ useGanttBlockProps }}', - }, - properties: { - toolBar: { - type: 'void', - 'x-component': 'ActionBar', - 'x-component-props': { - style: { - marginBottom: 24, - }, - }, - 'x-initializer': 'gantt:configureActions', - properties: {}, - }, - table: { - type: 'array', - 'x-decorator': 'div', - 'x-decorator-props': { - style: { - float: 'left', - maxWidth: '35%', - }, - }, - - 'x-initializer': 'table:configureColumns', - 'x-component': 'TableV2', - 'x-component-props': { - rowKey: 'id', - rowSelection: { - type: 'checkbox', - }, - useProps: '{{ useTableBlockProps }}', - pagination: false, - }, - properties: { - actions: { - type: 'void', - title: '{{ t("Actions") }}', - 'x-action-column': 'actions', - 'x-decorator': 'TableV2.Column.ActionBar', - 'x-component': 'TableV2.Column', - 'x-designer': 'TableV2.ActionColumnDesigner', - 'x-initializer': 'table:configureItemActions', - properties: { - actions: { - type: 'void', - 'x-decorator': 'DndContext', - 'x-component': 'Space', - 'x-component-props': { - split: '|', - }, - properties: {}, - }, - }, - }, - }, - }, - detail: { - type: 'void', - 'x-component': 'Gantt.Event', - properties: { - drawer: { - type: 'void', - 'x-component': 'Action.Drawer', - 'x-component-props': { - className: 'nb-action-popup', - }, - title: '{{ t("View record") }}', - properties: { - tabs: { - type: 'void', - 'x-component': 'Tabs', - 'x-component-props': {}, - 'x-initializer': 'TabPaneInitializers', - properties: { - tab1: { - type: 'void', - title: '{{t("Details")}}', - 'x-component': 'Tabs.TabPane', - 'x-designer': 'Tabs.Designer', - 'x-component-props': {}, - properties: { - grid: { - type: 'void', - 'x-component': 'Grid', - 'x-initializer': 'popup:common:addBlock', - properties: {}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - return schema; -}; diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.tsx b/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.tsx index e40fd5aaf..bb4c933fe 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.tsx +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.tsx @@ -6,6 +6,7 @@ import { useCreateActionProps as useCAP, useCollectionParentRecordData, useProps, + withDynamicSchemaProps, } from '@nocobase/client'; import { Spin, Tag } from 'antd'; import React, { useContext, useMemo, useState } from 'react'; @@ -56,100 +57,105 @@ export const toColumns = (groupField: any, dataSource: Array = [], primaryK return Object.values(columns); }; -export const Kanban: any = observer( - (props: any) => { - const { styles } = useStyles(); - const { groupField, onCardDragEnd, dataSource, setDataSource, ...restProps } = useProps(props); - const parentRecordData = useCollectionParentRecordData(); - const field = useField(); - const fieldSchema = useFieldSchema(); - const [disableCardDrag, setDisableCardDrag] = useState(false); - const schemas = useMemo( - () => - fieldSchema.reduceProperties( - (buf, current) => { - if (current['x-component'].endsWith('.Card')) { - buf.card = current; - } else if (current['x-component'].endsWith('.CardAdder')) { - buf.cardAdder = current; - } else if (current['x-component'].endsWith('.CardViewer')) { - buf.cardViewer = current; - } - return buf; - }, - { card: null, cardAdder: null, cardViewer: null }, - ), - [], - ); - const handleCardRemove = (card, column) => { - const updatedBoard = Board.removeCard({ columns: field.value }, column, card); - field.value = updatedBoard.columns; - setDataSource(updatedBoard.columns); - }; - const handleCardDragEnd = (card, fromColumn, toColumn) => { - onCardDragEnd?.({ columns: field.value, groupField }, fromColumn, toColumn); - const updatedBoard = Board.moveCard({ columns: field.value }, fromColumn, toColumn); - field.value = updatedBoard.columns; - setDataSource(updatedBoard.columns); - }; - return ( - - ( -
- {title} -
- )} - renderCard={(card, { column, dragging }) => { - const columnIndex = dataSource?.indexOf(column); - const cardIndex = column?.cards?.indexOf(card); - return ( - schemas.card && ( - - - - - - ) - ); - }} - renderCardAdder={({ column }) => { - if (!schemas.cardAdder) { - return null; - } - return ( - - - - - - ); - }} - > - {{ - columns: dataSource || [], - }} -
-
- ); - }, - { displayName: 'Kanban' }, +export const Kanban: any = withDynamicSchemaProps( + observer( + (props: any) => { + const { styles } = useStyles(); + + // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema + const { groupField, onCardDragEnd, dataSource, setDataSource, ...restProps } = useProps(props); + + const parentRecordData = useCollectionParentRecordData(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const [disableCardDrag, setDisableCardDrag] = useState(false); + const schemas = useMemo( + () => + fieldSchema.reduceProperties( + (buf, current) => { + if (current['x-component'].endsWith('.Card')) { + buf.card = current; + } else if (current['x-component'].endsWith('.CardAdder')) { + buf.cardAdder = current; + } else if (current['x-component'].endsWith('.CardViewer')) { + buf.cardViewer = current; + } + return buf; + }, + { card: null, cardAdder: null, cardViewer: null }, + ), + [], + ); + const handleCardRemove = (card, column) => { + const updatedBoard = Board.removeCard({ columns: field.value }, column, card); + field.value = updatedBoard.columns; + setDataSource(updatedBoard.columns); + }; + const handleCardDragEnd = (card, fromColumn, toColumn) => { + onCardDragEnd?.({ columns: field.value, groupField }, fromColumn, toColumn); + const updatedBoard = Board.moveCard({ columns: field.value }, fromColumn, toColumn); + field.value = updatedBoard.columns; + setDataSource(updatedBoard.columns); + }; + return ( + + ( +
+ {title} +
+ )} + renderCard={(card, { column, dragging }) => { + const columnIndex = dataSource?.indexOf(column); + const cardIndex = column?.cards?.indexOf(card); + return ( + schemas.card && ( + + + + + + ) + ); + }} + renderCardAdder={({ column }) => { + if (!schemas.cardAdder) { + return null; + } + return ( + + + + + + ); + }} + > + {{ + columns: dataSource || [], + }} +
+
+ ); + }, + { displayName: 'Kanban' }, + ), ); diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockInitializer.tsx index ea1b30492..409a14fe0 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockInitializer.tsx @@ -16,7 +16,7 @@ import { useSchemaInitializerItem, useAPIClient, } from '@nocobase/client'; -import { createKanbanBlockSchema } from './utils'; +import { createKanbanBlockUISchema } from './createKanbanBlockUISchema'; import { CreateAndSelectSort } from './CreateAndSelectSort'; import { NAMESPACE } from './locale'; @@ -157,14 +157,13 @@ export const KanbanBlockInitializer = () => { initialValues: {}, }); insert( - createKanbanBlockSchema({ + createKanbanBlockUISchema({ sortField: values.dragSortBy, groupField: values.groupField.value, - collection: item.name, + collectionName: item.name, dataSource: item.dataSource, params: { sort: [values.dragSortBy], - paginate: false, }, }), ); diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/__tests__/createKanbanBlockSchema.test.ts b/packages/plugins/@nocobase/plugin-kanban/src/client/__tests__/createKanbanBlockSchema.test.ts new file mode 100644 index 000000000..a06d344a5 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/__tests__/createKanbanBlockSchema.test.ts @@ -0,0 +1,126 @@ +import { createKanbanBlockUISchema } from '../createKanbanBlockUISchema'; + +vi.mock('@formily/shared', () => { + return { + uid: vi.fn(() => 'mocked-uid'), + }; +}); + +test('createKanbanBlockSchema should return an object with expected properties', () => { + const options = { + collectionName: 'testCollection', + groupField: 'testGroupField', + sortField: 'testSortField', + dataSource: 'testDataSource', + params: { testParam: 'testValue' }, + }; + + const result = createKanbanBlockUISchema(options); + + expect(result).toMatchInlineSnapshot(` + { + "properties": { + "actions": { + "properties": {}, + "type": "void", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)", + }, + }, + "x-initializer": "kanban:configureActions", + }, + "mocked-uid": { + "properties": { + "card": { + "properties": { + "grid": { + "type": "void", + "x-component": "Grid", + "x-component-props": { + "dndContext": false, + }, + }, + }, + "type": "void", + "x-component": "Kanban.Card", + "x-component-props": { + "openMode": "drawer", + }, + "x-decorator": "BlockItem", + "x-designer": "Kanban.Card.Designer", + "x-label-disabled": true, + "x-read-pretty": true, + }, + "cardViewer": { + "properties": { + "drawer": { + "properties": { + "tabs": { + "properties": { + "tab1": { + "properties": { + "grid": { + "properties": {}, + "type": "void", + "x-component": "Grid", + "x-initializer": "popup:common:addBlock", + }, + }, + "title": "{{t("Details")}}", + "type": "void", + "x-component": "Tabs.TabPane", + "x-component-props": {}, + "x-designer": "Tabs.Designer", + }, + }, + "type": "void", + "x-component": "Tabs", + "x-component-props": {}, + "x-initializer": "TabPaneInitializers", + }, + }, + "title": "{{ t("View record") }}", + "type": "void", + "x-component": "Action.Container", + "x-component-props": { + "className": "nb-action-popup", + }, + }, + }, + "title": "{{ t("View") }}", + "type": "void", + "x-action": "view", + "x-component": "Kanban.CardViewer", + "x-component-props": { + "openMode": "drawer", + }, + "x-designer": "Action.Designer", + }, + }, + "type": "array", + "x-component": "Kanban", + "x-use-component-props": "useKanbanBlockProps", + }, + }, + "type": "void", + "x-acl-action": "testCollection:list", + "x-component": "CardItem", + "x-decorator": "KanbanBlockProvider", + "x-decorator-props": { + "action": "list", + "collection": "testCollection", + "dataSource": "testDataSource", + "groupField": "testGroupField", + "params": { + "paginate": false, + "testParam": "testValue", + }, + "sortField": "testSortField", + }, + "x-settings": "blockSettings:kanban", + "x-toolbar": "BlockSchemaToolbar", + } + `); +}); diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/utils.ts b/packages/plugins/@nocobase/plugin-kanban/src/client/createKanbanBlockUISchema.ts similarity index 83% rename from packages/plugins/@nocobase/plugin-kanban/src/client/utils.ts rename to packages/plugins/@nocobase/plugin-kanban/src/client/createKanbanBlockUISchema.ts index 3304820da..88b5aef02 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/utils.ts +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/createKanbanBlockUISchema.ts @@ -1,24 +1,33 @@ import { ISchema } from '@formily/react'; import { uid } from '@formily/shared'; -export const createKanbanBlockSchema = (options) => { - const { collection, resource, groupField, sortField, ...others } = options; - const schema: ISchema = { +export const createKanbanBlockUISchema = (options: { + collectionName: string; + groupField: string; + sortField: string; + dataSource: string; + params?: Record; +}): ISchema => { + const { collectionName, groupField, sortField, dataSource, params } = options; + + return { type: 'void', - 'x-acl-action': `${resource || collection}:list`, + 'x-acl-action': `${collectionName}:list`, 'x-decorator': 'KanbanBlockProvider', 'x-decorator-props': { - collection: collection, - resource: resource || collection, + collection: collectionName, action: 'list', groupField, sortField, params: { paginate: false, + ...params, }, - ...others, + dataSource, }, - 'x-designer': 'Kanban.Designer', + // 'x-designer': 'Kanban.Designer', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:kanban', 'x-component': 'CardItem', properties: { actions: { @@ -35,9 +44,7 @@ export const createKanbanBlockSchema = (options) => { [uid()]: { type: 'array', 'x-component': 'Kanban', - 'x-component-props': { - useProps: '{{ useKanbanBlockProps }}', - }, + 'x-use-component-props': 'useKanbanBlockProps', properties: { card: { type: 'void', @@ -106,5 +113,4 @@ export const createKanbanBlockSchema = (options) => { }, }, }; - return schema; }; diff --git a/packages/plugins/@nocobase/plugin-map/src/client/__tests__/block/createMapBlockSchema.test.ts b/packages/plugins/@nocobase/plugin-map/src/client/__tests__/block/createMapBlockSchema.test.ts new file mode 100644 index 000000000..5f5726eb4 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-map/src/client/__tests__/block/createMapBlockSchema.test.ts @@ -0,0 +1,95 @@ +import { createMapBlockUISchema } from '../../block/createMapBlockUISchema'; + +vi.mock('@formily/shared', () => { + return { + uid: () => 'mocked-uid', + }; +}); + +test('createMapBlockSchema should return an object with expected properties', () => { + const options = { + collectionName: 'testCollection', + dataSource: 'testDataSource', + fieldNames: { + label: 'field1', + value: 'field2', + }, + }; + + const result = createMapBlockUISchema(options); + + expect(result).toMatchInlineSnapshot(` + { + "properties": { + "actions": { + "type": "void", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": 16, + }, + }, + "x-initializer": "map:configureActions", + }, + "mocked-uid": { + "properties": { + "drawer": { + "properties": { + "tabs": { + "properties": { + "tab1": { + "properties": { + "grid": { + "type": "void", + "x-component": "Grid", + "x-initializer": "popup:common:addBlock", + }, + }, + "title": "{{t("Details")}}", + "type": "void", + "x-component": "Tabs.TabPane", + "x-component-props": {}, + "x-designer": "Tabs.Designer", + }, + }, + "type": "void", + "x-component": "Tabs", + "x-component-props": {}, + "x-initializer": "TabPaneInitializers", + }, + }, + "title": "{{ t("View record") }}", + "type": "void", + "x-component": "Action.Drawer", + "x-component-props": { + "className": "nb-action-popup", + }, + }, + }, + "type": "void", + "x-component": "MapBlock", + "x-use-component-props": "useMapBlockProps", + }, + }, + "type": "void", + "x-acl-action": "testCollection:list", + "x-component": "CardItem", + "x-decorator": "MapBlockProvider", + "x-decorator-props": { + "action": "list", + "collection": "testCollection", + "dataSource": "testDataSource", + "fieldNames": { + "label": "field1", + "value": "field2", + }, + "params": { + "paginate": false, + }, + }, + "x-filter-targets": [], + "x-settings": "blockSettings:map", + "x-toolbar": "BlockSchemaToolbar", + } + `); +}); diff --git a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.tsx b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.tsx index b7da6116d..8fdae6c83 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.tsx @@ -1,9 +1,16 @@ -import { useCollection_deprecated, useCollectionManager_deprecated, useProps } from '@nocobase/client'; +import { + useCollection_deprecated, + useCollectionManager_deprecated, + useProps, + withDynamicSchemaProps, +} from '@nocobase/client'; import React, { useMemo } from 'react'; import { MapBlockComponent } from '../components'; -export const MapBlock = (props) => { +export const MapBlock = withDynamicSchemaProps((props) => { + // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema const { fieldNames } = useProps(props); + const { getCollectionJoinField } = useCollectionManager_deprecated(); const { name } = useCollection_deprecated(); const collectionField = useMemo(() => { @@ -12,4 +19,4 @@ export const MapBlock = (props) => { const fieldComponentProps = collectionField?.uiSchema?.['x-component-props']; return ; -}; +}); diff --git a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockInitializer.tsx index c019c6ec2..f12504113 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockInitializer.tsx @@ -13,7 +13,8 @@ import { } from '@nocobase/client'; import React, { useContext } from 'react'; import { useMapTranslation } from '../locale'; -import { createMapBlockSchema, findNestedOption } from './utils'; +import { findNestedOption } from './utils'; +import { createMapBlockUISchema } from './createMapBlockUISchema'; export const MapBlockInitializer = () => { const itemConfig = useSchemaInitializerItem(); @@ -84,13 +85,12 @@ export const MapBlockInitializer = () => { initialValues: {}, }); insert( - createMapBlockSchema({ - collection: item.name, + createMapBlockUISchema({ + collectionName: item.name, dataSource: item.dataSource, fieldNames: { ...values, }, - settings: 'blockSettings:map', }), ); }} diff --git a/packages/plugins/@nocobase/plugin-map/src/client/block/createMapBlockUISchema.ts b/packages/plugins/@nocobase/plugin-map/src/client/block/createMapBlockUISchema.ts new file mode 100644 index 000000000..112a40c5d --- /dev/null +++ b/packages/plugins/@nocobase/plugin-map/src/client/block/createMapBlockUISchema.ts @@ -0,0 +1,81 @@ +import { ISchema } from '@formily/react'; +import { uid } from '@formily/shared'; + +export const createMapBlockUISchema = (options: { + collectionName: string; + dataSource: string; + fieldNames: object; +}): ISchema => { + const { collectionName, fieldNames, dataSource } = options; + + return { + type: 'void', + 'x-acl-action': `${collectionName}:list`, + 'x-decorator': 'MapBlockProvider', + 'x-decorator-props': { + collection: collectionName, + dataSource, + action: 'list', + fieldNames, + params: { + paginate: false, + }, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:map', + 'x-component': 'CardItem', + // 保存当前筛选区块所能过滤的数据区块 + 'x-filter-targets': [], + properties: { + actions: { + type: 'void', + 'x-initializer': 'map:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 16, + }, + }, + }, + [uid()]: { + type: 'void', + 'x-component': 'MapBlock', + 'x-use-component-props': 'useMapBlockProps', + properties: { + drawer: { + type: 'void', + 'x-component': 'Action.Drawer', + 'x-component-props': { + className: 'nb-action-popup', + }, + title: '{{ t("View record") }}', + properties: { + tabs: { + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'TabPaneInitializers', + properties: { + tab1: { + type: 'void', + title: '{{t("Details")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:common:addBlock', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; +}; diff --git a/packages/plugins/@nocobase/plugin-map/src/client/block/utils.ts b/packages/plugins/@nocobase/plugin-map/src/client/block/utils.ts index e6d92cd1c..33aeeea54 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/block/utils.ts +++ b/packages/plugins/@nocobase/plugin-map/src/client/block/utils.ts @@ -1,86 +1,3 @@ -import { ISchema } from '@formily/react'; -import { uid } from '@formily/shared'; - -export const createMapBlockSchema = (options) => { - const { collection, resource, fieldNames, settings, ...others } = options; - const schema: ISchema = { - type: 'void', - 'x-acl-action': `${resource || collection}:list`, - 'x-decorator': 'MapBlockProvider', - 'x-decorator-props': { - collection: collection, - resource: resource || collection, - action: 'list', - fieldNames, - params: { - paginate: false, - }, - ...others, - }, - 'x-toolbar': 'BlockSchemaToolbar', - 'x-settings': settings, - 'x-component': 'CardItem', - // 保存当前筛选区块所能过滤的数据区块 - 'x-filter-targets': [], - properties: { - actions: { - type: 'void', - 'x-initializer': 'map:configureActions', - 'x-component': 'ActionBar', - 'x-component-props': { - style: { - marginBottom: 16, - }, - }, - properties: {}, - }, - [uid()]: { - type: 'void', - 'x-component': 'MapBlock', - 'x-component-props': { - useProps: '{{ useMapBlockProps }}', - }, - properties: { - drawer: { - type: 'void', - 'x-component': 'Action.Drawer', - 'x-component-props': { - className: 'nb-action-popup', - }, - title: '{{ t("View record") }}', - properties: { - tabs: { - type: 'void', - 'x-component': 'Tabs', - 'x-component-props': {}, - 'x-initializer': 'TabPaneInitializers', - properties: { - tab1: { - type: 'void', - title: '{{t("Details")}}', - 'x-component': 'Tabs.TabPane', - 'x-designer': 'Tabs.Designer', - 'x-component-props': {}, - properties: { - grid: { - type: 'void', - 'x-component': 'Grid', - 'x-initializer': 'popup:common:addBlock', - properties: {}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - return schema; -}; - export const findNestedOption = (value: string[] | string, options = []) => { if (typeof value === 'string') { value = [value]; diff --git a/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Block.tsx b/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Block.tsx index a4f18f56d..9ad47f5c4 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Block.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Block.tsx @@ -20,6 +20,7 @@ import { getSource } from '../../utils'; import { AMapComponent, AMapForwardedRefProps } from './Map'; export const AMapBlock = (props) => { + // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema const { collectionField, fieldNames, dataSource, fixedBlock, zoom, setSelectedRecordKeys, lineSort } = useProps(props); const { name, getPrimaryKey } = useCollection_deprecated(); diff --git a/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Block.tsx b/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Block.tsx index 4e43d6cf4..976fd0b54 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Block.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Block.tsx @@ -38,6 +38,7 @@ const pointClass = css` `; export const GoogleMapsBlock = (props) => { + // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema const { collectionField, fieldNames, dataSource, fixedBlock, zoom, setSelectedRecordKeys, lineSort } = useProps(props); const { getPrimaryKey } = useCollection_deprecated(); diff --git a/packages/plugins/@nocobase/plugin-map/src/client/index.tsx b/packages/plugins/@nocobase/plugin-map/src/client/index.tsx index 66c64b054..14a8e1a8e 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/index.tsx @@ -6,6 +6,7 @@ import { mapBlockSettings } from './block/MapBlock.Settings'; import { Configuration, Map } from './components'; import { fields } from './fields'; import { NAMESPACE, generateNTemplate } from './locale'; +import { useMapBlockProps } from './block/MapBlockProvider'; const MapProvider = React.memo((props) => { return ( @@ -44,6 +45,10 @@ export class MapPlugin extends Plugin { Component: Configuration, aclSnippet: 'pm.map.configuration', }); + + this.app.addScopes({ + useMapBlockProps, + }); } } diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/FormBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/FormBlockInitializer.tsx index 5ee4fc513..19bd06e43 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/FormBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/FormBlockInitializer.tsx @@ -4,7 +4,6 @@ import { CollectionProvider_deprecated, SchemaInitializerItem, SchemaInitializerItemType, - createFormBlockSchema, useRecordCollectionDataSourceItems, useSchemaInitializer, useSchemaInitializerItem, @@ -14,6 +13,7 @@ import { import { JOB_STATUS, traverseSchema } from '@nocobase/plugin-workflow/client'; import { NAMESPACE } from '../../locale'; +import { createManualFormBlockUISchema } from './createManualFormBlockUISchema'; function InternalFormBlockInitializer({ schema, ...others }) { const { getTemplateSchemaByMode } = useSchemaTemplateManager(); @@ -21,7 +21,7 @@ function InternalFormBlockInitializer({ schema, ...others }) { const items = useRecordCollectionDataSourceItems('FormItem') as SchemaInitializerItemType[]; async function onConfirm({ item }) { const template = item.template ? await getTemplateSchemaByMode(item) : null; - const result = createFormBlockSchema({ + const result = createManualFormBlockUISchema({ actionInitializers: 'workflowManual:form:configureActions', actions: { resolve: { diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/createManualFormBlockUISchema.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/createManualFormBlockUISchema.ts new file mode 100644 index 000000000..98841991f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/createManualFormBlockUISchema.ts @@ -0,0 +1,5 @@ +import { createFormBlockSchema } from '@nocobase/client'; + +export function createManualFormBlockUISchema(options) { + return createFormBlockSchema(options); +}