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:
parent
0269a1ff7d
commit
1c32983c00
@ -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(() => {
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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<any>({});
|
||||
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 (
|
||||
<SchemaComponentOptions scope={{ treeTable }}>
|
||||
<FormContext.Provider value={form}>
|
||||
<BlockProvider name={props.name || 'table'} {...props} params={paramsWithFilter} runWhenParamsChanged>
|
||||
<InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={paramsWithFilter} />
|
||||
<BlockProvider name={props.name || 'table'} {...props} params={params} runWhenParamsChanged>
|
||||
<InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={params} />
|
||||
</BlockProvider>
|
||||
</FormContext.Provider>
|
||||
</SchemaComponentOptions>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export const useTableBlockContext = () => {
|
||||
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);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { useCollection, useCollectionManager, useCollectionParentRecordData, useCollectionRecordData } from '../..';
|
||||
|
||||
/**
|
||||
|
@ -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',
|
||||
});
|
||||
|
@ -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",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
@ -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: '|',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
@ -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;
|
||||
}
|
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
@ -5,3 +5,5 @@ export * from './TableColumnSchemaToolbar';
|
||||
export * from './tableBlockSettings';
|
||||
export * from './tableColumnSettings';
|
||||
export * from './TableColumnInitializers';
|
||||
export * from './createTableBlockUISchema';
|
||||
export * from './hooks/useTableBlockDecoratorProps';
|
||||
|
@ -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<any[]>(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<any[]>(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 (
|
||||
<DndContext>
|
||||
<thead {...props} />
|
||||
</DndContext>
|
||||
);
|
||||
useEffect(() => {
|
||||
if (expandFlag) {
|
||||
setExpandesKeys(allIncludesChildren);
|
||||
} else {
|
||||
setExpandesKeys([]);
|
||||
}
|
||||
}, [expandFlag, allIncludesChildren]);
|
||||
|
||||
const components = useMemo(() => {
|
||||
return {
|
||||
header: {
|
||||
wrapper: (props) => {
|
||||
return (
|
||||
<DndContext>
|
||||
<thead {...props} />
|
||||
</DndContext>
|
||||
);
|
||||
},
|
||||
cell: (props) => {
|
||||
return (
|
||||
<th
|
||||
{...props}
|
||||
className={cls(
|
||||
props.className,
|
||||
css`
|
||||
max-width: 300px;
|
||||
white-space: nowrap;
|
||||
&:hover .general-schema-designer {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
cell: (props) => {
|
||||
return (
|
||||
<th
|
||||
body: {
|
||||
wrapper: (props) => {
|
||||
return (
|
||||
<DndContext
|
||||
onDragEnd={(e) => {
|
||||
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 });
|
||||
}}
|
||||
>
|
||||
<tbody {...props} />
|
||||
</DndContext>
|
||||
);
|
||||
},
|
||||
row: (props) => {
|
||||
return <SortableRow {...props}></SortableRow>;
|
||||
},
|
||||
cell: (props) => (
|
||||
<td
|
||||
{...props}
|
||||
className={cls(
|
||||
className={classNames(
|
||||
props.className,
|
||||
css`
|
||||
max-width: 300px;
|
||||
white-space: nowrap;
|
||||
&:hover .general-schema-designer {
|
||||
display: block;
|
||||
.nb-read-pretty-input-number {
|
||||
text-align: right;
|
||||
}
|
||||
.ant-color-picker-trigger {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
`,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
),
|
||||
},
|
||||
},
|
||||
body: {
|
||||
wrapper: (props) => {
|
||||
return (
|
||||
<DndContext
|
||||
onDragEnd={(e) => {
|
||||
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 });
|
||||
}}
|
||||
>
|
||||
<tbody {...props} />
|
||||
</DndContext>
|
||||
);
|
||||
},
|
||||
row: (props) => {
|
||||
return <SortableRow {...props}></SortableRow>;
|
||||
},
|
||||
cell: (props) => (
|
||||
<td
|
||||
{...props}
|
||||
className={classNames(
|
||||
props.className,
|
||||
css`
|
||||
max-width: 300px;
|
||||
white-space: nowrap;
|
||||
.nb-read-pretty-input-number {
|
||||
text-align: right;
|
||||
}
|
||||
.ant-color-picker-trigger {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
`,
|
||||
)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
};
|
||||
}, [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 (
|
||||
<div
|
||||
role="button"
|
||||
aria-label={`table-index-${index}`}
|
||||
className={classNames(
|
||||
checked ? 'checked' : null,
|
||||
css`
|
||||
position: relative;
|
||||
display: flex;
|
||||
float: left;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
padding-right: 8px;
|
||||
.nb-table-index {
|
||||
opacity: 0;
|
||||
}
|
||||
&:not(.checked) {
|
||||
.nb-table-index {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
[css`
|
||||
&:hover {
|
||||
.nb-table-index {
|
||||
opacity: 0;
|
||||
}
|
||||
.nb-origin-node {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
`]: isRowSelect,
|
||||
},
|
||||
)}
|
||||
>
|
||||
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 (
|
||||
<div
|
||||
role="button"
|
||||
aria-label={`table-index-${index}`}
|
||||
className={classNames(
|
||||
checked ? 'checked' : null,
|
||||
css`
|
||||
position: relative;
|
||||
display: flex;
|
||||
float: left;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
padding-right: 8px;
|
||||
.nb-table-index {
|
||||
opacity: 0;
|
||||
}
|
||||
&:not(.checked) {
|
||||
.nb-table-index {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
[css`
|
||||
&:hover {
|
||||
.nb-table-index {
|
||||
opacity: 0;
|
||||
}
|
||||
.nb-origin-node {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
`]: isRowSelect,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{dragSort && <SortHandle id={getRowKey(record)} />}
|
||||
{showIndex && <TableIndex index={index} />}
|
||||
</div>
|
||||
{isRowSelect && (
|
||||
<div
|
||||
className={classNames(
|
||||
'nb-origin-node',
|
||||
checked ? 'checked' : null,
|
||||
css`
|
||||
position: absolute;
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
&:not(.checked) {
|
||||
display: none;
|
||||
}
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
`,
|
||||
)}
|
||||
>
|
||||
{originNode}
|
||||
{dragSort && <SortHandle id={getRowKey(record)} />}
|
||||
{showIndex && <TableIndex index={index} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
...rowSelection,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
const SortableWrapper = useCallback<React.FC>(
|
||||
({ children }) => {
|
||||
return dragSort
|
||||
? React.createElement<Omit<SortableContextProps, 'children'>>(
|
||||
SortableContext,
|
||||
{
|
||||
items: field.value?.map?.(getRowKey) || [],
|
||||
{isRowSelect && (
|
||||
<div
|
||||
className={classNames(
|
||||
'nb-origin-node',
|
||||
checked ? 'checked' : null,
|
||||
css`
|
||||
position: absolute;
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
&:not(.checked) {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
)}
|
||||
>
|
||||
{originNode}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
children,
|
||||
)
|
||||
: React.createElement(React.Fragment, {}, children);
|
||||
},
|
||||
[field, dragSort],
|
||||
);
|
||||
const fieldSchema = useFieldSchema();
|
||||
const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock;
|
||||
...rowSelection,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
const SortableWrapper = useCallback<React.FC>(
|
||||
({ children }) => {
|
||||
return dragSort
|
||||
? React.createElement<Omit<SortableContextProps, 'children'>>(
|
||||
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 (
|
||||
<div
|
||||
className={css`
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
.ant-table-wrapper {
|
||||
const { height: tableHeight, tableSizeRefCallback } = useTableSize();
|
||||
const scroll = useMemo(() => {
|
||||
return fixedBlock
|
||||
? {
|
||||
x: 'max-content',
|
||||
y: tableHeight,
|
||||
}
|
||||
: {
|
||||
x: 'max-content',
|
||||
};
|
||||
}, [fixedBlock, tableHeight]);
|
||||
return (
|
||||
<div
|
||||
className={css`
|
||||
height: 100%;
|
||||
.ant-spin-nested-loading {
|
||||
overflow: hidden;
|
||||
.ant-table-wrapper {
|
||||
height: 100%;
|
||||
.ant-spin-container {
|
||||
.ant-spin-nested-loading {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.ant-spin-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-table {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<SortableWrapper>
|
||||
<AntdTable
|
||||
ref={tableSizeRefCallback}
|
||||
rowKey={rowKey ?? defaultRowKey}
|
||||
dataSource={dataSource}
|
||||
tableLayout="auto"
|
||||
{...others}
|
||||
{...restProps}
|
||||
pagination={paginationProps}
|
||||
components={components}
|
||||
onChange={(pagination, filters, sorter, extra) => {
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
</SortableWrapper>
|
||||
{field.errors.length > 0 && (
|
||||
<div className="ant-formily-item-error-help ant-formily-item-help ant-formily-item-help-enter ant-formily-item-help-enter-active">
|
||||
{field.errors.map((error) => {
|
||||
return error.messages.map((message) => <div key={message}>{message}</div>);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
.ant-table {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<SortableWrapper>
|
||||
<AntdTable
|
||||
ref={tableSizeRefCallback}
|
||||
rowKey={rowKey ?? defaultRowKey}
|
||||
dataSource={dataSource}
|
||||
tableLayout="auto"
|
||||
{...others}
|
||||
{...restProps}
|
||||
pagination={paginationProps}
|
||||
components={components}
|
||||
onChange={(pagination, filters, sorter, extra) => {
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
</SortableWrapper>
|
||||
{field.errors.length > 0 && (
|
||||
<div className="ant-formily-item-error-help ant-formily-item-help ant-formily-item-help-enter ant-formily-item-help-enter-active">
|
||||
{field.errors.map((error) => {
|
||||
return error.messages.map((message) => <div key={message}>{message}</div>);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
),
|
||||
{ displayName: 'Table' },
|
||||
);
|
||||
|
@ -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 (
|
||||
<SchemaInitializerItem
|
||||
icon={<TableOutlined />}
|
||||
@ -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}`,
|
||||
}),
|
||||
|
@ -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,
|
||||
|
@ -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 (
|
||||
<SchemaInitializerItem
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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: '|',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user