From 1c32983c0087826863f23e48a79a2ce268e12826 Mon Sep 17 00:00:00 2001 From: Zeke Zhang <958414905@qq.com> Date: Mon, 25 Mar 2024 17:35:57 +0800 Subject: [PATCH] refactor(DataBlock): table block (#3748) * refactor: remove useless code * chore: remove useless code * feat: add createTableBlockSchema * refactor: use createTableBlockUISchema * refactor: extract useTableBlockParams * refactor: extract useTableBlockSourceId * refactor: compat * refactor: fix typo in createTableBlockUISchema file * refactor: should not get collection on getting association in UISchema * refactor: use x-use-component-props instead of useProps * chore: fix unit tests * fix: fix errors * refactor: refactor data block source ID hooks --- .../src/block-provider/BlockProvider.tsx | 46 +- .../BlockSchemaComponentProvider.tsx | 6 +- .../src/block-provider/TableBlockProvider.tsx | 175 +---- .../hooks/useDataBlockSourceId.tsx | 1 - .../table/TableBlockInitializer.tsx | 6 +- .../__tests__/createTableBLockSchema.test.ts | 85 +++ .../table/createTableBlockUISchema.ts | 84 +++ .../hooks/useTableBlockDecoratorProps.ts | 51 ++ .../table/hooks/useTableBlockProps.tsx | 121 ++++ .../modules/blocks/data-blocks/table/index.ts | 2 + .../schema-component/antd/table-v2/Table.tsx | 679 +++++++++--------- .../RecordAssociationBlockInitializer.tsx | 16 +- .../client/src/schema-initializer/utils.ts | 8 +- .../src/client/AuditLogsBlockInitializer.tsx | 18 +- .../createAuditLogsBlockSchema.test.ts | 77 ++ .../src/client/createAuditLogsBlockSchema.tsx | 71 ++ 16 files changed, 915 insertions(+), 531 deletions(-) create mode 100644 packages/core/client/src/modules/blocks/data-blocks/table/__tests__/createTableBLockSchema.test.ts create mode 100644 packages/core/client/src/modules/blocks/data-blocks/table/createTableBlockUISchema.ts create mode 100644 packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps.ts create mode 100644 packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx create mode 100644 packages/plugins/@nocobase/plugin-audit-logs/src/client/__tests__/createAuditLogsBlockSchema.test.ts create mode 100644 packages/plugins/@nocobase/plugin-audit-logs/src/client/createAuditLogsBlockSchema.tsx diff --git a/packages/core/client/src/block-provider/BlockProvider.tsx b/packages/core/client/src/block-provider/BlockProvider.tsx index 80c9f344e9..11040c5f0e 100644 --- a/packages/core/client/src/block-provider/BlockProvider.tsx +++ b/packages/core/client/src/block-provider/BlockProvider.tsx @@ -59,31 +59,6 @@ export const useBlockResource = () => { return useContext(BlockResourceContext) || resource; }; -interface UseResourceProps { - resource: any; - association?: any; - useSourceId?: any; - collection?: any; - dataSource?: any; - block?: any; -} - -const useAssociation = (props) => { - const { association } = props; - const { getCollectionField } = useCollectionManager_deprecated(); - if (typeof association === 'string') { - return getCollectionField(association); - } else if (association?.collectionName && association?.name) { - return getCollectionField(`${association?.collectionName}.${association?.name}`); - } -}; - -const useActionParams = (props) => { - const { useParams } = props; - const params = useParams?.() || {}; - return { ...props.params, ...params }; -}; - export const MaybeCollectionProvider = (props) => { const { collection } = props; return collection ? ( @@ -218,6 +193,22 @@ export const useBlockContext = () => { return useContext(BlockContext); }; +/** + * 用于兼容旧版本 Schema + */ +const useCompatDataBlockSourceId = (props) => { + const fieldSchema = useFieldSchema(); + + // 如果存在 x-use-decorator-props,说明是新版 Schema + if (fieldSchema['x-use-decorator-props']) { + return props.sourceId; + } else { + // 是否存在 x-use-decorator-props 是固定不变的,所以这里可以使用 hooks + // eslint-disable-next-line react-hooks/rules-of-hooks + return useDataBlockSourceId(props); + } +}; + /** * @deprecated use `DataBlockProvider` instead */ @@ -236,8 +227,11 @@ export const BlockProvider = (props: { useParams?: any; }) => { const { name, dataSource, association, useParams, parentRecord } = props; - const sourceId = useDataBlockSourceId({ association }); + const sourceId = useCompatDataBlockSourceId(props); + + // 新版(1.0)已弃用 useParams,这里之所以继续保留是为了兼容旧版的 UISchema const paramsFromHook = useParams?.(); + const { getAssociationAppends } = useAssociationNames(dataSource); const { appends, updateAssociationValues } = getAssociationAppends(); const params = useMemo(() => { diff --git a/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx b/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx index fc08fc1e1e..8e3c7c1ebe 100644 --- a/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx +++ b/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx @@ -10,11 +10,13 @@ import { DetailsBlockProvider, useDetailsBlockProps } from './DetailsBlockProvid import { FilterFormBlockProvider } from './FilterFormBlockProvider'; import { FormBlockProvider, useFormBlockProps } from './FormBlockProvider'; import { FormFieldProvider, useFormFieldProps } from './FormFieldProvider'; -import { TableBlockProvider, useTableBlockProps } from './TableBlockProvider'; +import { TableBlockProvider } from './TableBlockProvider'; +import { useTableBlockProps } from '../modules/blocks/data-blocks/table/hooks/useTableBlockProps'; 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'; // TODO: delete this, replaced by `BlockSchemaComponentPlugin` export const BlockSchemaComponentProvider: React.FC = (props) => { @@ -41,6 +43,7 @@ export const BlockSchemaComponentProvider: React.FC = (props) => { useTableFieldProps, useTableBlockProps, useTableSelectorProps, + useTableBlockDecoratorProps, }} > {props.children} @@ -84,6 +87,7 @@ export class BlockSchemaComponentPlugin extends Plugin { useTableFieldProps, useTableBlockProps, useTableSelectorProps, + useTableBlockDecoratorProps, }); } } diff --git a/packages/core/client/src/block-provider/TableBlockProvider.tsx b/packages/core/client/src/block-provider/TableBlockProvider.tsx index 1963b52250..252e396a14 100644 --- a/packages/core/client/src/block-provider/TableBlockProvider.tsx +++ b/packages/core/client/src/block-provider/TableBlockProvider.tsx @@ -1,12 +1,12 @@ -import { ArrayField, createForm } from '@formily/core'; +import { createForm } from '@formily/core'; import { FormContext, useField, useFieldSchema } from '@formily/react'; -import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; +import React, { createContext, useContext, useMemo, useState } from 'react'; import { useCollectionManager_deprecated } from '../collection-manager'; -import { useFilterBlock } from '../filter-provider/FilterProvider'; -import { mergeFilter } from '../filter-provider/utils'; -import { FixedBlockWrapper, SchemaComponentOptions, removeNullCondition } from '../schema-component'; +import { FixedBlockWrapper, SchemaComponentOptions } from '../schema-component'; import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider'; -import { findFilterTargets, useParsedFilter } from './hooks'; +import { useParsedFilter } from './hooks'; +import { useTableBlockParams } from '../modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps'; +import { withDynamicSchemaProps } from '../application/hoc/withDynamicSchemaProps'; export const TableBlockContext = createContext({}); TableBlockContext.displayName = 'TableBlockContext'; @@ -74,16 +74,37 @@ const InternalTableBlockProvider = (props: Props) => { ); }; -export const TableBlockProvider = (props) => { - const resourceName = props.resource; - const params = useMemo(() => ({ ...props.params }), [props.params]); +/** + * 用于兼容旧版本的 schema,当不需要兼容时可直接移除该方法 + * @param props + * @returns + */ +const useTableBlockParamsCompat = (props) => { + const fieldSchema = useFieldSchema(); + + let params; + // 1. 新版本的 schema 存在 x-use-decorator-props 属性 + if (fieldSchema['x-use-decorator-props']) { + params = props.params; + } else { + // 2. 旧版本的 schema 不存在 x-use-decorator-props 属性 + // 因为 schema 中是否存在 x-use-decorator-props 是固定不变的,所以这里可以使用 hooks + // eslint-disable-next-line react-hooks/rules-of-hooks + params = useTableBlockParams(props); + } + + return params; +}; + +export const TableBlockProvider = withDynamicSchemaProps((props) => { + const resourceName = props.resource || props.association; + const fieldSchema = useFieldSchema(); const { getCollection, getCollectionField } = useCollectionManager_deprecated(props.dataSource); const collection = getCollection(props.collection, props.dataSource); - const { treeTable, dragSortBy } = fieldSchema?.['x-decorator-props'] || {}; - if (props.dragSort && dragSortBy) { - params['sort'] = dragSortBy; - } + const { treeTable } = fieldSchema?.['x-decorator-props'] || {}; + const params = useTableBlockParamsCompat(props); + let childrenColumnName = 'children'; if (collection?.tree && treeTable !== false) { if (resourceName?.includes('.')) { @@ -101,140 +122,18 @@ export const TableBlockProvider = (props) => { } } const form = useMemo(() => createForm(), [treeTable]); - const { filter: parsedFilter } = useParsedFilter({ - filterOption: params?.filter, - }); - const paramsWithFilter = useMemo(() => { - return { - ...params, - filter: parsedFilter, - }; - }, [parsedFilter, params]); return ( - - + + ); -}; +}); export const useTableBlockContext = () => { return useContext(TableBlockContext); }; - -export const useTableBlockProps = () => { - const field = useField(); - const fieldSchema = useFieldSchema(); - const ctx = useTableBlockContext(); - const globalSort = fieldSchema.parent?.['x-decorator-props']?.['params']?.['sort']; - const { getDataBlocks } = useFilterBlock(); - - useEffect(() => { - if (!ctx?.service?.loading) { - field.value = []; - field.value = ctx?.service?.data?.data; - field?.setInitialValue(ctx?.service?.data?.data); - field.data = field.data || {}; - field.data.selectedRowKeys = ctx?.field?.data?.selectedRowKeys; - field.componentProps.pagination = field.componentProps.pagination || {}; - field.componentProps.pagination.pageSize = ctx?.service?.data?.meta?.pageSize; - field.componentProps.pagination.total = ctx?.service?.data?.meta?.count; - field.componentProps.pagination.current = ctx?.service?.data?.meta?.page; - } - }, [ctx?.service?.data, ctx?.service?.loading]); // 这里如果依赖了 ctx?.field?.data?.selectedRowKeys 的话,会导致这个问题: - return { - childrenColumnName: ctx.childrenColumnName, - loading: ctx?.service?.loading, - showIndex: ctx.showIndex, - dragSort: ctx.dragSort && ctx.dragSortBy, - rowKey: ctx.rowKey || 'id', - pagination: - ctx?.params?.paginate !== false - ? { - defaultCurrent: ctx?.params?.page || 1, - defaultPageSize: ctx?.params?.pageSize, - } - : false, - onRowSelectionChange(selectedRowKeys) { - ctx.field.data = ctx?.field?.data || {}; - ctx.field.data.selectedRowKeys = selectedRowKeys; - ctx?.field?.onRowSelect?.(selectedRowKeys); - }, - async onRowDragEnd({ from, to }) { - await ctx.resource.move({ - sourceId: from[ctx.rowKey || 'id'], - targetId: to[ctx.rowKey || 'id'], - sortField: ctx.dragSort && ctx.dragSortBy, - }); - ctx.service.refresh(); - }, - onChange({ current, pageSize }, filters, sorter) { - const sort = sorter.order ? (sorter.order === `ascend` ? [sorter.field] : [`-${sorter.field}`]) : globalSort; - ctx.service.run({ ...ctx.service.params?.[0], page: current, pageSize, sort }); - }, - onClickRow(record, setSelectedRow, selectedRow) { - const { targets, uid } = findFilterTargets(fieldSchema); - const dataBlocks = getDataBlocks(); - - // 如果是之前创建的区块是没有 x-filter-targets 属性的,所以这里需要判断一下避免报错 - if (!targets || !targets.some((target) => dataBlocks.some((dataBlock) => dataBlock.uid === target.uid))) { - // 当用户已经点击过某一行,如果此时再把相连接的区块给删除的话,行的高亮状态就会一直保留。 - // 这里暂时没有什么比较好的方法,只是在用户再次点击的时候,把高亮状态给清除掉。 - setSelectedRow((prev) => (prev.length ? [] : prev)); - return; - } - - const value = [record[ctx.rowKey]]; - - dataBlocks.forEach((block) => { - const target = targets.find((target) => target.uid === block.uid); - if (!target) return; - - const param = block.service.params?.[0] || {}; - // 保留原有的 filter - const storedFilter = block.service.params?.[1]?.filters || {}; - - if (selectedRow.includes(record[ctx.rowKey])) { - if (block.dataLoadingMode === 'manual') { - return block.clearData(); - } - delete storedFilter[uid]; - } else { - storedFilter[uid] = { - $and: [ - { - [target.field || ctx.rowKey]: { - [target.field ? '$in' : '$eq']: value, - }, - }, - ], - }; - } - - const mergedFilter = mergeFilter([ - ...Object.values(storedFilter).map((filter) => removeNullCondition(filter)), - block.defaultFilter, - ]); - - return block.doFilter( - { - ...param, - page: 1, - filter: mergedFilter, - }, - { filters: storedFilter }, - ); - }); - - // 更新表格的选中状态 - setSelectedRow((prev) => (prev?.includes(record[ctx.rowKey]) ? [] : [...value])); - }, - onExpand(expanded, record) { - ctx?.field.onExpandClick?.(expanded, record); - }, - }; -}; diff --git a/packages/core/client/src/block-provider/hooks/useDataBlockSourceId.tsx b/packages/core/client/src/block-provider/hooks/useDataBlockSourceId.tsx index 5089b848b0..755694776f 100644 --- a/packages/core/client/src/block-provider/hooks/useDataBlockSourceId.tsx +++ b/packages/core/client/src/block-provider/hooks/useDataBlockSourceId.tsx @@ -1,4 +1,3 @@ -import { useFieldSchema } from '@formily/react'; import { useCollection, useCollectionManager, useCollectionParentRecordData, useCollectionRecordData } from '../..'; /** diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/TableBlockInitializer.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/TableBlockInitializer.tsx index 56d38dfc9e..374293f46a 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/TableBlockInitializer.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/TableBlockInitializer.tsx @@ -2,9 +2,9 @@ import { TableOutlined } from '@ant-design/icons'; import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application/schema-initializer/context'; import { useCollectionManager_deprecated } from '../../../../collection-manager/hooks/useCollectionManager_deprecated'; import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer'; -import { createTableBlockSchema } from '../../../../schema-initializer/utils'; import React from 'react'; import { Collection, CollectionFieldOptions } from '../../../../data-source/collection/Collection'; +import { createTableBlockUISchema } from './createTableBlockUISchema'; export const TableBlockInitializer = ({ filterCollections, @@ -42,8 +42,8 @@ export const TableBlockInitializer = ({ } const collection = getCollection(item.name, item.dataSource); - const schema = createTableBlockSchema({ - collection: item.name, + const schema = createTableBlockUISchema({ + collectionName: item.name, dataSource: item.dataSource, rowKey: collection.filterTargetKey || 'id', }); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__tests__/createTableBLockSchema.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__tests__/createTableBLockSchema.test.ts new file mode 100644 index 0000000000..7d0cd5d96a --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__tests__/createTableBLockSchema.test.ts @@ -0,0 +1,85 @@ +import { createTableBlockUISchema } from '../createTableBlockUISchema'; + +vi.mock('@formily/shared', () => { + return { + uid: () => 'mocked-uid', + }; +}); + +describe('createTableBLockSchemaV2', () => { + it('should create a default table block schema with minimum options', () => { + const options = { dataSource: 'abc', collectionName: 'users', association: 'users.roles', rowKey: 'rowKey' }; + const schema = createTableBlockUISchema(options); + + expect(schema).toMatchInlineSnapshot(` + { + "properties": { + "actions": { + "properties": {}, + "type": "void", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)", + }, + }, + "x-initializer": "table:configureActions", + }, + "mocked-uid": { + "properties": { + "actions": { + "properties": { + "mocked-uid": { + "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": { + "rowKey": "id", + "rowSelection": { + "type": "checkbox", + }, + }, + "x-initializer": "table:configureColumns", + "x-use-component-props": "useTableBlockProps", + }, + }, + "type": "void", + "x-acl-action": "users:list", + "x-component": "CardItem", + "x-decorator": "TableBlockProvider", + "x-decorator-props": { + "action": "list", + "association": "users.roles", + "collection": "users", + "dataSource": "abc", + "dragSort": false, + "params": { + "pageSize": 20, + }, + "rowKey": "rowKey", + "showIndex": true, + }, + "x-filter-targets": [], + "x-settings": "blockSettings:table", + "x-toolbar": "BlockSchemaToolbar", + "x-use-decorator-props": "useTableBlockDecoratorProps", + } + `); + }); +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/createTableBlockUISchema.ts b/packages/core/client/src/modules/blocks/data-blocks/table/createTableBlockUISchema.ts new file mode 100644 index 0000000000..ca5da9dbdc --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/table/createTableBlockUISchema.ts @@ -0,0 +1,84 @@ +import { ISchema } from '@formily/react'; +import { uid } from '@formily/shared'; + +export const createTableBlockUISchema = (options: { + dataSource: string; + collectionName?: string; + rowKey?: string; + association?: string; +}): ISchema => { + const { collectionName, dataSource, rowKey, association } = options; + + if (!dataSource) { + throw new Error('dataSource is required'); + } + + return { + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': `${collectionName}:list`, + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: collectionName, + association, + dataSource, + action: 'list', + params: { + pageSize: 20, + }, + rowKey, + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + properties: { + actions: { + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + properties: {}, + }, + [uid()]: { + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + properties: { + actions: { + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-designer': 'TableV2.ActionColumnDesigner', + 'x-initializer': 'table:configureItemActions', + properties: { + [uid()]: { + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + }, + }, + }, + }, + }, + }, + }; +}; diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps.ts b/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps.ts new file mode 100644 index 0000000000..55650748ab --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps.ts @@ -0,0 +1,51 @@ +import { useFieldSchema } from '@formily/react'; +import { useParsedFilter } from '../../../../../block-provider/hooks/useParsedFilter'; +import { useMemo } from 'react'; +import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId'; + +export const useTableBlockDecoratorProps = (props) => { + const params = useTableBlockParams(props); + const sourceId = useTableBlockSourceId(props); + + return { + params, + sourceId, + }; +}; + +export function useTableBlockParams(props) { + const fieldSchema = useFieldSchema(); + const { filter: parsedFilter } = useParsedFilter({ + filterOption: props.params?.filter, + }); + + return useMemo(() => { + const params = props.params || {}; + + // 1. sort + const { dragSortBy } = fieldSchema?.['x-decorator-props'] || {}; + if (props.dragSort && dragSortBy) { + params['sort'] = dragSortBy; + } + + // 2. filter + const paramsWithFilter = { + ...params, + filter: parsedFilter, + }; + + return paramsWithFilter; + }, [fieldSchema, parsedFilter, props.dragSort, props.params]); +} + +function useTableBlockSourceId(props) { + let sourceId: string | undefined; + + // 因为 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/hooks/useTableBlockProps.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx new file mode 100644 index 0000000000..d30dfe2f60 --- /dev/null +++ b/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx @@ -0,0 +1,121 @@ +import { ArrayField } from '@formily/core'; +import { useField, useFieldSchema } from '@formily/react'; +import { useEffect } from 'react'; +import { useFilterBlock } from '../../../../../filter-provider/FilterProvider'; +import { mergeFilter } from '../../../../../filter-provider/utils'; +import { removeNullCondition } from '../../../../../schema-component'; +import { findFilterTargets } from '../../../../../block-provider/hooks'; +import { useTableBlockContext } from '../../../../../block-provider/TableBlockProvider'; + +export const useTableBlockProps = () => { + const field = useField(); + const fieldSchema = useFieldSchema(); + const ctx = useTableBlockContext(); + const globalSort = fieldSchema.parent?.['x-decorator-props']?.['params']?.['sort']; + const { getDataBlocks } = useFilterBlock(); + + useEffect(() => { + if (!ctx?.service?.loading) { + field.value = []; + field.value = ctx?.service?.data?.data; + field?.setInitialValue(ctx?.service?.data?.data); + field.data = field.data || {}; + field.data.selectedRowKeys = ctx?.field?.data?.selectedRowKeys; + field.componentProps.pagination = field.componentProps.pagination || {}; + field.componentProps.pagination.pageSize = ctx?.service?.data?.meta?.pageSize; + field.componentProps.pagination.total = ctx?.service?.data?.meta?.count; + field.componentProps.pagination.current = ctx?.service?.data?.meta?.page; + } + }, [ctx?.service?.data, ctx?.service?.loading]); // 这里如果依赖了 ctx?.field?.data?.selectedRowKeys 的话,会导致这个问题: + return { + childrenColumnName: ctx.childrenColumnName, + loading: ctx?.service?.loading, + showIndex: ctx.showIndex, + dragSort: ctx.dragSort && ctx.dragSortBy, + rowKey: ctx.rowKey || 'id', + pagination: + ctx?.params?.paginate !== false + ? { + defaultCurrent: ctx?.params?.page || 1, + defaultPageSize: ctx?.params?.pageSize, + } + : false, + onRowSelectionChange(selectedRowKeys) { + ctx.field.data = ctx?.field?.data || {}; + ctx.field.data.selectedRowKeys = selectedRowKeys; + ctx?.field?.onRowSelect?.(selectedRowKeys); + }, + async onRowDragEnd({ from, to }) { + await ctx.resource.move({ + sourceId: from[ctx.rowKey || 'id'], + targetId: to[ctx.rowKey || 'id'], + sortField: ctx.dragSort && ctx.dragSortBy, + }); + ctx.service.refresh(); + }, + onChange({ current, pageSize }, filters, sorter) { + const sort = sorter.order ? (sorter.order === `ascend` ? [sorter.field] : [`-${sorter.field}`]) : globalSort; + ctx.service.run({ ...ctx.service.params?.[0], page: current, pageSize, sort }); + }, + onClickRow(record, setSelectedRow, selectedRow) { + const { targets, uid } = findFilterTargets(fieldSchema); + const dataBlocks = getDataBlocks(); + + // 如果是之前创建的区块是没有 x-filter-targets 属性的,所以这里需要判断一下避免报错 + if (!targets || !targets.some((target) => dataBlocks.some((dataBlock) => dataBlock.uid === target.uid))) { + // 当用户已经点击过某一行,如果此时再把相连接的区块给删除的话,行的高亮状态就会一直保留。 + // 这里暂时没有什么比较好的方法,只是在用户再次点击的时候,把高亮状态给清除掉。 + setSelectedRow((prev) => (prev.length ? [] : prev)); + return; + } + + const value = [record[ctx.rowKey]]; + + dataBlocks.forEach((block) => { + const target = targets.find((target) => target.uid === block.uid); + if (!target) return; + + const param = block.service.params?.[0] || {}; + // 保留原有的 filter + const storedFilter = block.service.params?.[1]?.filters || {}; + + if (selectedRow.includes(record[ctx.rowKey])) { + if (block.dataLoadingMode === 'manual') { + return block.clearData(); + } + delete storedFilter[uid]; + } else { + storedFilter[uid] = { + $and: [ + { + [target.field || ctx.rowKey]: { + [target.field ? '$in' : '$eq']: value, + }, + }, + ], + }; + } + + const mergedFilter = mergeFilter([ + ...Object.values(storedFilter).map((filter) => removeNullCondition(filter)), + block.defaultFilter, + ]); + + return block.doFilter( + { + ...param, + page: 1, + filter: mergedFilter, + }, + { filters: storedFilter }, + ); + }); + + // 更新表格的选中状态 + setSelectedRow((prev) => (prev?.includes(record[ctx.rowKey]) ? [] : [...value])); + }, + onExpand(expanded, record) { + ctx?.field.onExpandClick?.(expanded, record); + }, + }; +}; diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/index.ts b/packages/core/client/src/modules/blocks/data-blocks/table/index.ts index 643720b497..5b35a76b89 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/index.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/index.ts @@ -5,3 +5,5 @@ export * from './TableColumnSchemaToolbar'; export * from './tableBlockSettings'; export * from './tableColumnSettings'; export * from './TableColumnInitializers'; +export * from './createTableBlockUISchema'; +export * from './hooks/useTableBlockDecoratorProps'; diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx index 0b2f4accba..6069dc657f 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx @@ -25,6 +25,7 @@ import { useTableBlockContext, useTableSelectorContext, } from '../../../'; +import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps'; import { useACLFieldWhitelist } from '../../../acl/ACLProvider'; import { useToken } from '../__builtins__'; import { SubFormProvider } from '../association-field/hooks'; @@ -211,385 +212,391 @@ const usePaginationProps = (pagination1, pagination2) => { return result.total <= result.pageSize ? false : result; }; -export const Table: any = observer( - (props: { - useProps?: () => any; - onChange?: (pagination, filters, sorter, extra) => void; - onRowSelectionChange?: (selectedRowKeys: any[], selectedRows: any[]) => void; - onRowDragEnd?: (e: { from: any; to: any }) => void; - onClickRow?: (record: any, setSelectedRow: (selectedRow: any[]) => void, selectedRow: any[]) => void; - pagination?: any; - showIndex?: boolean; - dragSort?: boolean; - rowKey?: string | ((record: any) => string); - rowSelection?: any; - required?: boolean; - onExpand?: (flag: boolean, record: any) => void; - isSubTable?: boolean; - }) => { - const { token } = useToken(); - const { pagination: pagination1, useProps, onChange, ...others1 } = props; - const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {}; - const { - dragSort = false, - showIndex = true, - onRowSelectionChange, - onChange: onTableChange, - rowSelection, - rowKey, - required, - onExpand, - ...others - } = { ...others1, ...others2 } as any; - const field = useArrayField(others); - const columns = useTableColumns(others); - const schema = useFieldSchema(); - const collection = useCollection_deprecated(); - const isTableSelector = schema?.parent?.['x-decorator'] === 'TableSelectorProvider'; - const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext(); - const { expandFlag, allIncludesChildren } = ctx; - const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {})); - const paginationProps = usePaginationProps(pagination1, pagination2); - const [expandedKeys, setExpandesKeys] = useState([]); - const [selectedRowKeys, setSelectedRowKeys] = useState(field?.data?.selectedRowKeys || []); - const [selectedRow, setSelectedRow] = useState([]); - const dataSource = field?.value?.slice?.()?.filter?.(Boolean) || []; - const isRowSelect = rowSelection?.type !== 'none'; - const defaultRowKeyMap = useRef(new Map()); - let onRow = null, - highlightRow = ''; +export const Table: any = withDynamicSchemaProps( + observer( + (props: { + useProps?: () => any; + onChange?: (pagination, filters, sorter, extra) => void; + onRowSelectionChange?: (selectedRowKeys: any[], selectedRows: any[]) => void; + onRowDragEnd?: (e: { from: any; to: any }) => void; + onClickRow?: (record: any, setSelectedRow: (selectedRow: any[]) => void, selectedRow: any[]) => void; + pagination?: any; + showIndex?: boolean; + dragSort?: boolean; + rowKey?: string | ((record: any) => string); + rowSelection?: any; + required?: boolean; + onExpand?: (flag: boolean, record: any) => void; + isSubTable?: boolean; + }) => { + const { token } = useToken(); + const { pagination: pagination1, useProps, onChange, ...others1 } = props; - if (onClickRow) { - onRow = (record) => { - return { - onClick: (e) => { - if (isPortalInBody(e.target)) { - return; - } - onClickRow(record, setSelectedRow, selectedRow); - }, + // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema + const { pagination: pagination2, ...others2 } = useProps?.() || {}; + + const { + dragSort = false, + showIndex = true, + onRowSelectionChange, + onChange: onTableChange, + rowSelection, + rowKey, + required, + onExpand, + onClickRow, + ...others + } = { ...others1, ...others2 } as any; + const field = useArrayField(others); + const columns = useTableColumns(others); + const schema = useFieldSchema(); + const collection = useCollection_deprecated(); + const isTableSelector = schema?.parent?.['x-decorator'] === 'TableSelectorProvider'; + const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext(); + const { expandFlag, allIncludesChildren } = ctx; + const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {})); + const paginationProps = usePaginationProps(pagination1, pagination2); + const [expandedKeys, setExpandesKeys] = useState([]); + const [selectedRowKeys, setSelectedRowKeys] = useState(field?.data?.selectedRowKeys || []); + const [selectedRow, setSelectedRow] = useState([]); + const dataSource = field?.value?.slice?.()?.filter?.(Boolean) || []; + const isRowSelect = rowSelection?.type !== 'none'; + const defaultRowKeyMap = useRef(new Map()); + let onRow = null, + highlightRow = ''; + + if (onClickRow) { + onRow = (record) => { + return { + onClick: (e) => { + if (isPortalInBody(e.target)) { + return; + } + onClickRow(record, setSelectedRow, selectedRow); + }, + }; }; - }; - highlightRow = css` - & > td { - background-color: ${token.controlItemBgActiveHover} !important; - } - &:hover > td { - background-color: ${token.controlItemBgActiveHover} !important; - } - `; - } - - useEffect(() => { - if (expandFlag) { - setExpandesKeys(allIncludesChildren); - } else { - setExpandesKeys([]); + highlightRow = css` + & > td { + background-color: ${token.controlItemBgActiveHover} !important; + } + &:hover > td { + background-color: ${token.controlItemBgActiveHover} !important; + } + `; } - }, [expandFlag, allIncludesChildren]); - const components = useMemo(() => { - return { - header: { - wrapper: (props) => { - return ( - - - - ); + useEffect(() => { + if (expandFlag) { + setExpandesKeys(allIncludesChildren); + } else { + setExpandesKeys([]); + } + }, [expandFlag, allIncludesChildren]); + + const components = useMemo(() => { + return { + header: { + wrapper: (props) => { + return ( + + + + ); + }, + cell: (props) => { + return ( + + ); + }, }, - cell: (props) => { - return ( - { + return ( + { + if (!e.active || !e.over) { + console.warn('move cancel'); + return; + } + const fromIndex = e.active?.data.current?.sortable?.index; + const toIndex = e.over?.data.current?.sortable?.index; + const from = field.value[fromIndex] || e.active; + const to = field.value[toIndex] || e.over; + void field.move(fromIndex, toIndex); + onRowDragEnd({ from, to }); + }} + > + + + ); + }, + row: (props) => { + return ; + }, + cell: (props) => ( + - ); + ), }, - }, - body: { - wrapper: (props) => { - return ( - { - if (!e.active || !e.over) { - console.warn('move cancel'); - return; - } - const fromIndex = e.active?.data.current?.sortable?.index; - const toIndex = e.over?.data.current?.sortable?.index; - const from = field.value[fromIndex] || e.active; - const to = field.value[toIndex] || e.over; - void field.move(fromIndex, toIndex); - onRowDragEnd({ from, to }); - }} - > - - - ); - }, - row: (props) => { - return ; - }, - cell: (props) => ( - - ), - }, + }; + }, [field, onRowDragEnd, dragSort]); + + /** + * 为没有设置 key 属性的表格行生成一个唯一的 key + * 1. rowKey 的默认值是 “key”,所以先判断有没有 record.key; + * 2. 如果没有就生成一个唯一的 key,并以 record 的值作为索引; + * 3. 这样下次就能取到对应的 key 的值; + * + * 这里有效的前提是:数组中对应的 record 的引用不会发生改变。 + * + * @param record + * @returns + */ + const defaultRowKey = (record: any) => { + if (record.key) { + return record.key; + } + + if (defaultRowKeyMap.current.has(record)) { + return defaultRowKeyMap.current.get(record); + } + + const key = uid(); + defaultRowKeyMap.current.set(record, key); + return key; }; - }, [field, onRowDragEnd, dragSort]); - /** - * 为没有设置 key 属性的表格行生成一个唯一的 key - * 1. rowKey 的默认值是 “key”,所以先判断有没有 record.key; - * 2. 如果没有就生成一个唯一的 key,并以 record 的值作为索引; - * 3. 这样下次就能取到对应的 key 的值; - * - * 这里有效的前提是:数组中对应的 record 的引用不会发生改变。 - * - * @param record - * @returns - */ - const defaultRowKey = (record: any) => { - if (record.key) { - return record.key; - } + const getRowKey = (record: any) => { + if (typeof rowKey === 'string') { + return record[rowKey]?.toString(); + } else { + return (rowKey ?? defaultRowKey)(record)?.toString(); + } + }; - if (defaultRowKeyMap.current.has(record)) { - return defaultRowKeyMap.current.get(record); - } - - const key = uid(); - defaultRowKeyMap.current.set(record, key); - return key; - }; - - const getRowKey = (record: any) => { - if (typeof rowKey === 'string') { - return record[rowKey]?.toString(); - } else { - return (rowKey ?? defaultRowKey)(record)?.toString(); - } - }; - - const restProps = { - rowSelection: rowSelection - ? { - type: 'checkbox', - selectedRowKeys: selectedRowKeys, - onChange(selectedRowKeys: any[], selectedRows: any[]) { - field.data = field.data || {}; - field.data.selectedRowKeys = selectedRowKeys; - setSelectedRowKeys(selectedRowKeys); - onRowSelectionChange?.(selectedRowKeys, selectedRows); - }, - getCheckboxProps(record) { - return { - 'aria-label': `checkbox`, - }; - }, - renderCell: (checked, record, index, originNode) => { - if (!dragSort && !showIndex) { - return originNode; - } - const current = props?.pagination?.current; - const pageSize = props?.pagination?.pageSize || 20; - if (current) { - index = index + (current - 1) * pageSize + 1; - } else { - index = index + 1; - } - if (record.__index) { - index = extractIndex(record.__index); - } - return ( -
+ const restProps = { + rowSelection: rowSelection + ? { + type: 'checkbox', + selectedRowKeys: selectedRowKeys, + onChange(selectedRowKeys: any[], selectedRows: any[]) { + field.data = field.data || {}; + field.data.selectedRowKeys = selectedRowKeys; + setSelectedRowKeys(selectedRowKeys); + onRowSelectionChange?.(selectedRowKeys, selectedRows); + }, + getCheckboxProps(record) { + return { + 'aria-label': `checkbox`, + }; + }, + renderCell: (checked, record, index, originNode) => { + if (!dragSort && !showIndex) { + return originNode; + } + const current = props?.pagination?.current; + const pageSize = props?.pagination?.pageSize || 20; + if (current) { + index = index + (current - 1) * pageSize + 1; + } else { + index = index + 1; + } + if (record.__index) { + index = extractIndex(record.__index); + } + return (
- {dragSort && } - {showIndex && } -
- {isRowSelect && (
- {originNode} + {dragSort && } + {showIndex && }
- )} -
- ); - }, - ...rowSelection, - } - : undefined, - }; - const SortableWrapper = useCallback( - ({ children }) => { - return dragSort - ? React.createElement>( - SortableContext, - { - items: field.value?.map?.(getRowKey) || [], + {isRowSelect && ( +
+ {originNode} +
+ )} + + ); }, - children, - ) - : React.createElement(React.Fragment, {}, children); - }, - [field, dragSort], - ); - const fieldSchema = useFieldSchema(); - const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock; + ...rowSelection, + } + : undefined, + }; + const SortableWrapper = useCallback( + ({ children }) => { + return dragSort + ? React.createElement>( + SortableContext, + { + items: field.value?.map?.(getRowKey) || [], + }, + children, + ) + : React.createElement(React.Fragment, {}, children); + }, + [field, dragSort], + ); + const fieldSchema = useFieldSchema(); + const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock; - const { height: tableHeight, tableSizeRefCallback } = useTableSize(); - const scroll = useMemo(() => { - return fixedBlock - ? { - x: 'max-content', - y: tableHeight, - } - : { - x: 'max-content', - }; - }, [fixedBlock, tableHeight]); - return ( -
{ + return fixedBlock + ? { + x: 'max-content', + y: tableHeight, + } + : { + x: 'max-content', + }; + }, [fixedBlock, tableHeight]); + return ( +
- - { - onTableChange?.(pagination, filters, sorter, extra); - }} - onRow={onRow} - rowClassName={(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : '')} - scroll={scroll} - columns={columns} - expandable={{ - onExpand: (flag, record) => { - const newKeys = flag - ? [...expandedKeys, record[collection.getPrimaryKey()]] - : expandedKeys.filter((i) => record[collection.getPrimaryKey()] !== i); - setExpandesKeys(newKeys); - onExpand?.(flag, record); - }, - expandedRowKeys: expandedKeys, - }} - /> - - {field.errors.length > 0 && ( -
- {field.errors.map((error) => { - return error.messages.map((message) =>
{message}
); - })} -
- )} -
- ); - }, + .ant-table { + overflow-x: auto; + overflow-y: hidden; + } + `} + > + + { + onTableChange?.(pagination, filters, sorter, extra); + }} + onRow={onRow} + rowClassName={(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : '')} + scroll={scroll} + columns={columns} + expandable={{ + onExpand: (flag, record) => { + const newKeys = flag + ? [...expandedKeys, record[collection.getPrimaryKey()]] + : expandedKeys.filter((i) => record[collection.getPrimaryKey()] !== i); + setExpandesKeys(newKeys); + onExpand?.(flag, record); + }, + expandedRowKeys: expandedKeys, + }} + /> + + {field.errors.length > 0 && ( +
+ {field.errors.map((error) => { + return error.messages.map((message) =>
{message}
); + })} +
+ )} +
+ ); + }, + ), { displayName: 'Table' }, ); diff --git a/packages/core/client/src/schema-initializer/items/RecordAssociationBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordAssociationBlockInitializer.tsx index 2d6e0d5ff6..7f3525453a 100644 --- a/packages/core/client/src/schema-initializer/items/RecordAssociationBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordAssociationBlockInitializer.tsx @@ -3,8 +3,9 @@ import { TableOutlined } from '@ant-design/icons'; import { useCollectionManager_deprecated } from '../../collection-manager'; import { useSchemaTemplateManager } from '../../schema-templates'; -import { createTableBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; +import { useRecordCollectionDataSourceItems } from '../utils'; import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import { createTableBlockUISchema } from '../../modules/blocks/data-blocks/table/createTableBlockUISchema'; /** * @deprecated @@ -17,7 +18,7 @@ export const RecordAssociationBlockInitializer = () => { const { getCollection } = useCollectionManager_deprecated(); const field = itemConfig.field; const collection = getCollection(field.target); - const resource = `${field.collectionName}.${field.name}`; + const association = `${field.collectionName}.${field.name}`; return ( } @@ -28,17 +29,15 @@ export const RecordAssociationBlockInitializer = () => { insert(s); } else { insert( - createTableBlockSchema({ + createTableBlockUISchema({ rowKey: collection.filterTargetKey, - collection: field.target, dataSource: collection.dataSource, - resource, - association: resource, + association: association, }), ); } }} - items={useRecordCollectionDataSourceItems('Table', itemConfig, field.target, resource)} + items={useRecordCollectionDataSourceItems('Table', itemConfig, field.target, association)} /> ); }; @@ -53,9 +52,8 @@ export function useCreateAssociationTableBlock() { const collection = getCollection(field.target); insert( - createTableBlockSchema({ + createTableBlockUISchema({ rowKey: collection.filterTargetKey, - collection: field.target, dataSource: collection.dataSource, association: `${field.collectionName}.${field.name}`, }), diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts index 6b7adf2c0d..4a9fed7d81 100644 --- a/packages/core/client/src/schema-initializer/utils.ts +++ b/packages/core/client/src/schema-initializer/utils.ts @@ -1181,8 +1181,6 @@ export const createFormBlockSchema = (options) => { resource: resourceName, collection, association, - // action: 'get', - // useParams: '{{ useParamsFromRecord }}', }, 'x-toolbar': 'BlockSchemaToolbar', ...(settings ? { 'x-settings': settings } : { 'x-designer': designer }), @@ -1358,6 +1356,12 @@ export const createReadPrettyFormBlockSchema = (options) => { return schema; }; +/** + * @deprecated + * 已弃用,可以使用 createTableBlockUISchema 替换 + * @param options + * @returns + */ export const createTableBlockSchema = (options) => { const { collection, diff --git a/packages/plugins/@nocobase/plugin-audit-logs/src/client/AuditLogsBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-audit-logs/src/client/AuditLogsBlockInitializer.tsx index 6752dcf102..9607e1581f 100644 --- a/packages/plugins/@nocobase/plugin-audit-logs/src/client/AuditLogsBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-audit-logs/src/client/AuditLogsBlockInitializer.tsx @@ -1,28 +1,16 @@ import { TableOutlined } from '@ant-design/icons'; import { ISchema } from '@formily/react'; -import { - createTableBlockSchema, - SchemaInitializerItem, - useSchemaInitializer, - useSchemaInitializerItem, -} from '@nocobase/client'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '@nocobase/client'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import { createAuditLogsBlockSchema } from './createAuditLogsBlockSchema'; export const AuditLogsBlockInitializer = () => { const { insert } = useSchemaInitializer(); const { t } = useTranslation(); const itemConfig = useSchemaInitializerItem(); - const schema = createTableBlockSchema({ - collection: 'auditLogs', - rowKey: 'id', - tableActionInitializers: 'auditLogsTable:configureActions', - tableColumnInitializers: 'auditLogsTable:configureColumns', - tableActionColumnInitializers: 'auditLogsTable:configureItemActions', - tableBlockProvider: 'AuditLogsBlockProvider', - disableTemplate: true, - }); + const schema = createAuditLogsBlockSchema(); return ( { + let schema: ISchema; + + beforeAll(() => { + schema = createAuditLogsBlockSchema(); + }); + + it('should return a valid schema object', () => { + expect(schema).toBeDefined(); + expect(typeof schema).toBe('object'); + }); + + it('should have specific top-level properties with expected values', () => { + expect(schema).toMatchObject({ + type: 'void', + 'x-decorator': 'AuditLogsBlockProvider', + 'x-acl-action': 'auditLogs:list', + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': expect.arrayContaining([]), + }); + }); + + describe('x-decorator-props section', () => { + it('should have a collection named "auditLogs"', () => { + expect(schema['x-decorator-props'].collection).toEqual('auditLogs'); + }); + + it('should have a list action', () => { + expect(schema['x-decorator-props'].action).toEqual('list'); + }); + + it('should set pageSize to 20', () => { + expect(schema['x-decorator-props'].params.pageSize).toEqual(20); + }); + + it('contains expected boolean flags', () => { + expect(schema['x-decorator-props'].showIndex).toBeTruthy(); + expect(schema['x-decorator-props'].dragSort).toBeFalsy(); + expect(schema['x-decorator-props'].disableTemplate).toBeTruthy(); + }); + }); + + describe('actions property inside schema.properties', () => { + let actions; + + beforeAll(() => { + // @ts-ignore + actions = schema.properties.actions; + }); + + it('should be defined and have a specific structure', () => { + expect(actions).toBeDefined(); + expect(actions.type).toBe('void'); + expect(actions['x-initializer']).toBe('auditLogsTable:configureActions'); + expect(actions['x-component']).toBe('ActionBar'); + }); + + it('has x-component-props with expected style', () => { + expect(actions['x-component-props'].style.marginBottom).toBe('var(--nb-spacing)'); + }); + }); + + describe('properties should include dynamically keyed objects', () => { + it('keyed objects should match the expected structure for tables', () => { + const propertyKeys = Object.keys(schema.properties).filter((key) => key !== 'actions'); + propertyKeys.forEach((key) => { + expect(schema.properties[key].type).toBe('array'); + expect(schema.properties[key]['x-component']).toBe('TableV2'); + }); + }); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-audit-logs/src/client/createAuditLogsBlockSchema.tsx b/packages/plugins/@nocobase/plugin-audit-logs/src/client/createAuditLogsBlockSchema.tsx new file mode 100644 index 0000000000..2f7e1ef6be --- /dev/null +++ b/packages/plugins/@nocobase/plugin-audit-logs/src/client/createAuditLogsBlockSchema.tsx @@ -0,0 +1,71 @@ +import { ISchema } from '@formily/react'; +import { uid } from '@nocobase/utils/client'; + +export function createAuditLogsBlockSchema(): ISchema { + return { + type: 'void', + 'x-decorator': 'AuditLogsBlockProvider', + 'x-acl-action': `auditLogs:list`, + 'x-decorator-props': { + collection: 'auditLogs', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + disableTemplate: true, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + properties: { + actions: { + type: 'void', + 'x-initializer': 'auditLogsTable:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + properties: {}, + }, + [uid()]: { + type: 'array', + 'x-initializer': 'auditLogsTable:configureColumns', + 'x-component': 'TableV2', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + useProps: '{{ useTableBlockProps }}', + }, + 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': 'auditLogsTable:configureItemActions', + properties: { + [uid()]: { + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + }, + }, + }, + }, + }, + }, + }; +}