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
This commit is contained in:
Zeke Zhang 2024-03-25 17:35:57 +08:00 committed by GitHub
parent 0269a1ff7d
commit 1c32983c00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 915 additions and 531 deletions

View File

@ -59,31 +59,6 @@ export const useBlockResource = () => {
return useContext(BlockResourceContext) || resource; 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) => { export const MaybeCollectionProvider = (props) => {
const { collection } = props; const { collection } = props;
return collection ? ( return collection ? (
@ -218,6 +193,22 @@ export const useBlockContext = () => {
return useContext(BlockContext); 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 * @deprecated use `DataBlockProvider` instead
*/ */
@ -236,8 +227,11 @@ export const BlockProvider = (props: {
useParams?: any; useParams?: any;
}) => { }) => {
const { name, dataSource, association, useParams, parentRecord } = props; const { name, dataSource, association, useParams, parentRecord } = props;
const sourceId = useDataBlockSourceId({ association }); const sourceId = useCompatDataBlockSourceId(props);
// 新版1.0)已弃用 useParams这里之所以继续保留是为了兼容旧版的 UISchema
const paramsFromHook = useParams?.(); const paramsFromHook = useParams?.();
const { getAssociationAppends } = useAssociationNames(dataSource); const { getAssociationAppends } = useAssociationNames(dataSource);
const { appends, updateAssociationValues } = getAssociationAppends(); const { appends, updateAssociationValues } = getAssociationAppends();
const params = useMemo(() => { const params = useMemo(() => {

View File

@ -10,11 +10,13 @@ import { DetailsBlockProvider, useDetailsBlockProps } from './DetailsBlockProvid
import { FilterFormBlockProvider } from './FilterFormBlockProvider'; import { FilterFormBlockProvider } from './FilterFormBlockProvider';
import { FormBlockProvider, useFormBlockProps } from './FormBlockProvider'; import { FormBlockProvider, useFormBlockProps } from './FormBlockProvider';
import { FormFieldProvider, useFormFieldProps } from './FormFieldProvider'; 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 { TableFieldProvider, useTableFieldProps } from './TableFieldProvider';
import { TableSelectorProvider, useTableSelectorProps } from './TableSelectorProvider'; import { TableSelectorProvider, useTableSelectorProps } from './TableSelectorProvider';
import * as bp from './hooks'; import * as bp from './hooks';
import { BlockSchemaToolbar } from '../modules/blocks/BlockSchemaToolbar'; import { BlockSchemaToolbar } from '../modules/blocks/BlockSchemaToolbar';
import { useTableBlockDecoratorProps } from '../modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps';
// TODO: delete this, replaced by `BlockSchemaComponentPlugin` // TODO: delete this, replaced by `BlockSchemaComponentPlugin`
export const BlockSchemaComponentProvider: React.FC = (props) => { export const BlockSchemaComponentProvider: React.FC = (props) => {
@ -41,6 +43,7 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
useTableFieldProps, useTableFieldProps,
useTableBlockProps, useTableBlockProps,
useTableSelectorProps, useTableSelectorProps,
useTableBlockDecoratorProps,
}} }}
> >
{props.children} {props.children}
@ -84,6 +87,7 @@ export class BlockSchemaComponentPlugin extends Plugin {
useTableFieldProps, useTableFieldProps,
useTableBlockProps, useTableBlockProps,
useTableSelectorProps, useTableSelectorProps,
useTableBlockDecoratorProps,
}); });
} }
} }

View File

@ -1,12 +1,12 @@
import { ArrayField, createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { FormContext, useField, useFieldSchema } from '@formily/react'; 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 { useCollectionManager_deprecated } from '../collection-manager';
import { useFilterBlock } from '../filter-provider/FilterProvider'; import { FixedBlockWrapper, SchemaComponentOptions } from '../schema-component';
import { mergeFilter } from '../filter-provider/utils';
import { FixedBlockWrapper, SchemaComponentOptions, removeNullCondition } from '../schema-component';
import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider'; 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<any>({}); export const TableBlockContext = createContext<any>({});
TableBlockContext.displayName = 'TableBlockContext'; TableBlockContext.displayName = 'TableBlockContext';
@ -74,16 +74,37 @@ const InternalTableBlockProvider = (props: Props) => {
); );
}; };
export const TableBlockProvider = (props) => { /**
const resourceName = props.resource; * schema
const params = useMemo(() => ({ ...props.params }), [props.params]); * @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 fieldSchema = useFieldSchema();
const { getCollection, getCollectionField } = useCollectionManager_deprecated(props.dataSource); const { getCollection, getCollectionField } = useCollectionManager_deprecated(props.dataSource);
const collection = getCollection(props.collection, props.dataSource); const collection = getCollection(props.collection, props.dataSource);
const { treeTable, dragSortBy } = fieldSchema?.['x-decorator-props'] || {}; const { treeTable } = fieldSchema?.['x-decorator-props'] || {};
if (props.dragSort && dragSortBy) { const params = useTableBlockParamsCompat(props);
params['sort'] = dragSortBy;
}
let childrenColumnName = 'children'; let childrenColumnName = 'children';
if (collection?.tree && treeTable !== false) { if (collection?.tree && treeTable !== false) {
if (resourceName?.includes('.')) { if (resourceName?.includes('.')) {
@ -101,140 +122,18 @@ export const TableBlockProvider = (props) => {
} }
} }
const form = useMemo(() => createForm(), [treeTable]); const form = useMemo(() => createForm(), [treeTable]);
const { filter: parsedFilter } = useParsedFilter({
filterOption: params?.filter,
});
const paramsWithFilter = useMemo(() => {
return {
...params,
filter: parsedFilter,
};
}, [parsedFilter, params]);
return ( return (
<SchemaComponentOptions scope={{ treeTable }}> <SchemaComponentOptions scope={{ treeTable }}>
<FormContext.Provider value={form}> <FormContext.Provider value={form}>
<BlockProvider name={props.name || 'table'} {...props} params={paramsWithFilter} runWhenParamsChanged> <BlockProvider name={props.name || 'table'} {...props} params={params} runWhenParamsChanged>
<InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={paramsWithFilter} /> <InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={params} />
</BlockProvider> </BlockProvider>
</FormContext.Provider> </FormContext.Provider>
</SchemaComponentOptions> </SchemaComponentOptions>
); );
}; });
export const useTableBlockContext = () => { export const useTableBlockContext = () => {
return useContext(TableBlockContext); return useContext(TableBlockContext);
}; };
export const useTableBlockProps = () => {
const field = useField<ArrayField>();
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);
},
};
};

View File

@ -1,4 +1,3 @@
import { useFieldSchema } from '@formily/react';
import { useCollection, useCollectionManager, useCollectionParentRecordData, useCollectionRecordData } from '../..'; import { useCollection, useCollectionManager, useCollectionParentRecordData, useCollectionRecordData } from '../..';
/** /**

View File

@ -2,9 +2,9 @@ import { TableOutlined } from '@ant-design/icons';
import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application/schema-initializer/context'; import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application/schema-initializer/context';
import { useCollectionManager_deprecated } from '../../../../collection-manager/hooks/useCollectionManager_deprecated'; import { useCollectionManager_deprecated } from '../../../../collection-manager/hooks/useCollectionManager_deprecated';
import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer'; import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer';
import { createTableBlockSchema } from '../../../../schema-initializer/utils';
import React from 'react'; import React from 'react';
import { Collection, CollectionFieldOptions } from '../../../../data-source/collection/Collection'; import { Collection, CollectionFieldOptions } from '../../../../data-source/collection/Collection';
import { createTableBlockUISchema } from './createTableBlockUISchema';
export const TableBlockInitializer = ({ export const TableBlockInitializer = ({
filterCollections, filterCollections,
@ -42,8 +42,8 @@ export const TableBlockInitializer = ({
} }
const collection = getCollection(item.name, item.dataSource); const collection = getCollection(item.name, item.dataSource);
const schema = createTableBlockSchema({ const schema = createTableBlockUISchema({
collection: item.name, collectionName: item.name,
dataSource: item.dataSource, dataSource: item.dataSource,
rowKey: collection.filterTargetKey || 'id', rowKey: collection.filterTargetKey || 'id',
}); });

View File

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

View File

@ -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: '|',
},
},
},
},
},
},
},
};
};

View File

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

View File

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

View File

@ -5,3 +5,5 @@ export * from './TableColumnSchemaToolbar';
export * from './tableBlockSettings'; export * from './tableBlockSettings';
export * from './tableColumnSettings'; export * from './tableColumnSettings';
export * from './TableColumnInitializers'; export * from './TableColumnInitializers';
export * from './createTableBlockUISchema';
export * from './hooks/useTableBlockDecoratorProps';

View File

@ -25,6 +25,7 @@ import {
useTableBlockContext, useTableBlockContext,
useTableSelectorContext, useTableSelectorContext,
} from '../../../'; } from '../../../';
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
import { useACLFieldWhitelist } from '../../../acl/ACLProvider'; import { useACLFieldWhitelist } from '../../../acl/ACLProvider';
import { useToken } from '../__builtins__'; import { useToken } from '../__builtins__';
import { SubFormProvider } from '../association-field/hooks'; import { SubFormProvider } from '../association-field/hooks';
@ -211,7 +212,8 @@ const usePaginationProps = (pagination1, pagination2) => {
return result.total <= result.pageSize ? false : result; return result.total <= result.pageSize ? false : result;
}; };
export const Table: any = observer( export const Table: any = withDynamicSchemaProps(
observer(
(props: { (props: {
useProps?: () => any; useProps?: () => any;
onChange?: (pagination, filters, sorter, extra) => void; onChange?: (pagination, filters, sorter, extra) => void;
@ -229,7 +231,10 @@ export const Table: any = observer(
}) => { }) => {
const { token } = useToken(); const { token } = useToken();
const { pagination: pagination1, useProps, onChange, ...others1 } = props; const { pagination: pagination1, useProps, onChange, ...others1 } = props;
const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {};
// 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { pagination: pagination2, ...others2 } = useProps?.() || {};
const { const {
dragSort = false, dragSort = false,
showIndex = true, showIndex = true,
@ -239,6 +244,7 @@ export const Table: any = observer(
rowKey, rowKey,
required, required,
onExpand, onExpand,
onClickRow,
...others ...others
} = { ...others1, ...others2 } as any; } = { ...others1, ...others2 } as any;
const field = useArrayField(others); const field = useArrayField(others);
@ -591,5 +597,6 @@ export const Table: any = observer(
</div> </div>
); );
}, },
),
{ displayName: 'Table' }, { displayName: 'Table' },
); );

View File

@ -3,8 +3,9 @@ import { TableOutlined } from '@ant-design/icons';
import { useCollectionManager_deprecated } from '../../collection-manager'; import { useCollectionManager_deprecated } from '../../collection-manager';
import { useSchemaTemplateManager } from '../../schema-templates'; import { useSchemaTemplateManager } from '../../schema-templates';
import { createTableBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; import { useRecordCollectionDataSourceItems } from '../utils';
import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application';
import { createTableBlockUISchema } from '../../modules/blocks/data-blocks/table/createTableBlockUISchema';
/** /**
* @deprecated * @deprecated
@ -17,7 +18,7 @@ export const RecordAssociationBlockInitializer = () => {
const { getCollection } = useCollectionManager_deprecated(); const { getCollection } = useCollectionManager_deprecated();
const field = itemConfig.field; const field = itemConfig.field;
const collection = getCollection(field.target); const collection = getCollection(field.target);
const resource = `${field.collectionName}.${field.name}`; const association = `${field.collectionName}.${field.name}`;
return ( return (
<SchemaInitializerItem <SchemaInitializerItem
icon={<TableOutlined />} icon={<TableOutlined />}
@ -28,17 +29,15 @@ export const RecordAssociationBlockInitializer = () => {
insert(s); insert(s);
} else { } else {
insert( insert(
createTableBlockSchema({ createTableBlockUISchema({
rowKey: collection.filterTargetKey, rowKey: collection.filterTargetKey,
collection: field.target,
dataSource: collection.dataSource, dataSource: collection.dataSource,
resource, association: association,
association: resource,
}), }),
); );
} }
}} }}
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); const collection = getCollection(field.target);
insert( insert(
createTableBlockSchema({ createTableBlockUISchema({
rowKey: collection.filterTargetKey, rowKey: collection.filterTargetKey,
collection: field.target,
dataSource: collection.dataSource, dataSource: collection.dataSource,
association: `${field.collectionName}.${field.name}`, association: `${field.collectionName}.${field.name}`,
}), }),

View File

@ -1181,8 +1181,6 @@ export const createFormBlockSchema = (options) => {
resource: resourceName, resource: resourceName,
collection, collection,
association, association,
// action: 'get',
// useParams: '{{ useParamsFromRecord }}',
}, },
'x-toolbar': 'BlockSchemaToolbar', 'x-toolbar': 'BlockSchemaToolbar',
...(settings ? { 'x-settings': settings } : { 'x-designer': designer }), ...(settings ? { 'x-settings': settings } : { 'x-designer': designer }),
@ -1358,6 +1356,12 @@ export const createReadPrettyFormBlockSchema = (options) => {
return schema; return schema;
}; };
/**
* @deprecated
* 使 createTableBlockUISchema
* @param options
* @returns
*/
export const createTableBlockSchema = (options) => { export const createTableBlockSchema = (options) => {
const { const {
collection, collection,

View File

@ -1,28 +1,16 @@
import { TableOutlined } from '@ant-design/icons'; import { TableOutlined } from '@ant-design/icons';
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '@nocobase/client';
createTableBlockSchema,
SchemaInitializerItem,
useSchemaInitializer,
useSchemaInitializerItem,
} from '@nocobase/client';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { createAuditLogsBlockSchema } from './createAuditLogsBlockSchema';
export const AuditLogsBlockInitializer = () => { export const AuditLogsBlockInitializer = () => {
const { insert } = useSchemaInitializer(); const { insert } = useSchemaInitializer();
const { t } = useTranslation(); const { t } = useTranslation();
const itemConfig = useSchemaInitializerItem(); const itemConfig = useSchemaInitializerItem();
const schema = createTableBlockSchema({ const schema = createAuditLogsBlockSchema();
collection: 'auditLogs',
rowKey: 'id',
tableActionInitializers: 'auditLogsTable:configureActions',
tableColumnInitializers: 'auditLogsTable:configureColumns',
tableActionColumnInitializers: 'auditLogsTable:configureItemActions',
tableBlockProvider: 'AuditLogsBlockProvider',
disableTemplate: true,
});
return ( return (
<SchemaInitializerItem <SchemaInitializerItem

View File

@ -0,0 +1,77 @@
import { ISchema } from '@formily/react';
import { createAuditLogsBlockSchema } from '../createAuditLogsBlockSchema';
describe('createAuditLogsBlockSchema', () => {
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');
});
});
});
});

View File

@ -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: '|',
},
},
},
},
},
},
},
};
}