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;
|
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(() => {
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useFieldSchema } from '@formily/react';
|
|
||||||
import { useCollection, useCollectionManager, useCollectionParentRecordData, useCollectionRecordData } from '../..';
|
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 { 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',
|
||||||
});
|
});
|
||||||
|
@ -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 './tableBlockSettings';
|
||||||
export * from './tableColumnSettings';
|
export * from './tableColumnSettings';
|
||||||
export * from './TableColumnInitializers';
|
export * from './TableColumnInitializers';
|
||||||
|
export * from './createTableBlockUISchema';
|
||||||
|
export * from './hooks/useTableBlockDecoratorProps';
|
||||||
|
@ -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,385 +212,391 @@ 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(
|
||||||
(props: {
|
observer(
|
||||||
useProps?: () => any;
|
(props: {
|
||||||
onChange?: (pagination, filters, sorter, extra) => void;
|
useProps?: () => any;
|
||||||
onRowSelectionChange?: (selectedRowKeys: any[], selectedRows: any[]) => void;
|
onChange?: (pagination, filters, sorter, extra) => void;
|
||||||
onRowDragEnd?: (e: { from: any; to: any }) => void;
|
onRowSelectionChange?: (selectedRowKeys: any[], selectedRows: any[]) => void;
|
||||||
onClickRow?: (record: any, setSelectedRow: (selectedRow: any[]) => void, selectedRow: any[]) => void;
|
onRowDragEnd?: (e: { from: any; to: any }) => void;
|
||||||
pagination?: any;
|
onClickRow?: (record: any, setSelectedRow: (selectedRow: any[]) => void, selectedRow: any[]) => void;
|
||||||
showIndex?: boolean;
|
pagination?: any;
|
||||||
dragSort?: boolean;
|
showIndex?: boolean;
|
||||||
rowKey?: string | ((record: any) => string);
|
dragSort?: boolean;
|
||||||
rowSelection?: any;
|
rowKey?: string | ((record: any) => string);
|
||||||
required?: boolean;
|
rowSelection?: any;
|
||||||
onExpand?: (flag: boolean, record: any) => void;
|
required?: boolean;
|
||||||
isSubTable?: boolean;
|
onExpand?: (flag: boolean, record: any) => void;
|
||||||
}) => {
|
isSubTable?: boolean;
|
||||||
const { token } = useToken();
|
}) => {
|
||||||
const { pagination: pagination1, useProps, onChange, ...others1 } = props;
|
const { token } = useToken();
|
||||||
const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {};
|
const { pagination: pagination1, useProps, onChange, ...others1 } = props;
|
||||||
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 = '';
|
|
||||||
|
|
||||||
if (onClickRow) {
|
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
|
||||||
onRow = (record) => {
|
const { pagination: pagination2, ...others2 } = useProps?.() || {};
|
||||||
return {
|
|
||||||
onClick: (e) => {
|
const {
|
||||||
if (isPortalInBody(e.target)) {
|
dragSort = false,
|
||||||
return;
|
showIndex = true,
|
||||||
}
|
onRowSelectionChange,
|
||||||
onClickRow(record, setSelectedRow, selectedRow);
|
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`
|
||||||
highlightRow = css`
|
& > td {
|
||||||
& > td {
|
background-color: ${token.controlItemBgActiveHover} !important;
|
||||||
background-color: ${token.controlItemBgActiveHover} !important;
|
}
|
||||||
}
|
&:hover > td {
|
||||||
&:hover > td {
|
background-color: ${token.controlItemBgActiveHover} !important;
|
||||||
background-color: ${token.controlItemBgActiveHover} !important;
|
}
|
||||||
}
|
`;
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (expandFlag) {
|
|
||||||
setExpandesKeys(allIncludesChildren);
|
|
||||||
} else {
|
|
||||||
setExpandesKeys([]);
|
|
||||||
}
|
}
|
||||||
}, [expandFlag, allIncludesChildren]);
|
|
||||||
|
|
||||||
const components = useMemo(() => {
|
useEffect(() => {
|
||||||
return {
|
if (expandFlag) {
|
||||||
header: {
|
setExpandesKeys(allIncludesChildren);
|
||||||
wrapper: (props) => {
|
} else {
|
||||||
return (
|
setExpandesKeys([]);
|
||||||
<DndContext>
|
}
|
||||||
<thead {...props} />
|
}, [expandFlag, allIncludesChildren]);
|
||||||
</DndContext>
|
|
||||||
);
|
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) => {
|
body: {
|
||||||
return (
|
wrapper: (props) => {
|
||||||
<th
|
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}
|
{...props}
|
||||||
className={cls(
|
className={classNames(
|
||||||
props.className,
|
props.className,
|
||||||
css`
|
css`
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
&:hover .general-schema-designer {
|
.nb-read-pretty-input-number {
|
||||||
display: block;
|
text-align: right;
|
||||||
|
}
|
||||||
|
.ant-color-picker-trigger {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
),
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
body: {
|
}, [field, onRowDragEnd, dragSort]);
|
||||||
wrapper: (props) => {
|
|
||||||
return (
|
/**
|
||||||
<DndContext
|
* 为没有设置 key 属性的表格行生成一个唯一的 key
|
||||||
onDragEnd={(e) => {
|
* 1. rowKey 的默认值是 “key”,所以先判断有没有 record.key;
|
||||||
if (!e.active || !e.over) {
|
* 2. 如果没有就生成一个唯一的 key,并以 record 的值作为索引;
|
||||||
console.warn('move cancel');
|
* 3. 这样下次就能取到对应的 key 的值;
|
||||||
return;
|
*
|
||||||
}
|
* 这里有效的前提是:数组中对应的 record 的引用不会发生改变。
|
||||||
const fromIndex = e.active?.data.current?.sortable?.index;
|
*
|
||||||
const toIndex = e.over?.data.current?.sortable?.index;
|
* @param record
|
||||||
const from = field.value[fromIndex] || e.active;
|
* @returns
|
||||||
const to = field.value[toIndex] || e.over;
|
*/
|
||||||
void field.move(fromIndex, toIndex);
|
const defaultRowKey = (record: any) => {
|
||||||
onRowDragEnd({ from, to });
|
if (record.key) {
|
||||||
}}
|
return record.key;
|
||||||
>
|
}
|
||||||
<tbody {...props} />
|
|
||||||
</DndContext>
|
if (defaultRowKeyMap.current.has(record)) {
|
||||||
);
|
return defaultRowKeyMap.current.get(record);
|
||||||
},
|
}
|
||||||
row: (props) => {
|
|
||||||
return <SortableRow {...props}></SortableRow>;
|
const key = uid();
|
||||||
},
|
defaultRowKeyMap.current.set(record, key);
|
||||||
cell: (props) => (
|
return key;
|
||||||
<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]);
|
|
||||||
|
|
||||||
/**
|
const getRowKey = (record: any) => {
|
||||||
* 为没有设置 key 属性的表格行生成一个唯一的 key
|
if (typeof rowKey === 'string') {
|
||||||
* 1. rowKey 的默认值是 “key”,所以先判断有没有 record.key;
|
return record[rowKey]?.toString();
|
||||||
* 2. 如果没有就生成一个唯一的 key,并以 record 的值作为索引;
|
} else {
|
||||||
* 3. 这样下次就能取到对应的 key 的值;
|
return (rowKey ?? defaultRowKey)(record)?.toString();
|
||||||
*
|
}
|
||||||
* 这里有效的前提是:数组中对应的 record 的引用不会发生改变。
|
};
|
||||||
*
|
|
||||||
* @param record
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const defaultRowKey = (record: any) => {
|
|
||||||
if (record.key) {
|
|
||||||
return record.key;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defaultRowKeyMap.current.has(record)) {
|
const restProps = {
|
||||||
return defaultRowKeyMap.current.get(record);
|
rowSelection: rowSelection
|
||||||
}
|
? {
|
||||||
|
type: 'checkbox',
|
||||||
const key = uid();
|
selectedRowKeys: selectedRowKeys,
|
||||||
defaultRowKeyMap.current.set(record, key);
|
onChange(selectedRowKeys: any[], selectedRows: any[]) {
|
||||||
return key;
|
field.data = field.data || {};
|
||||||
};
|
field.data.selectedRowKeys = selectedRowKeys;
|
||||||
|
setSelectedRowKeys(selectedRowKeys);
|
||||||
const getRowKey = (record: any) => {
|
onRowSelectionChange?.(selectedRowKeys, selectedRows);
|
||||||
if (typeof rowKey === 'string') {
|
},
|
||||||
return record[rowKey]?.toString();
|
getCheckboxProps(record) {
|
||||||
} else {
|
return {
|
||||||
return (rowKey ?? defaultRowKey)(record)?.toString();
|
'aria-label': `checkbox`,
|
||||||
}
|
};
|
||||||
};
|
},
|
||||||
|
renderCell: (checked, record, index, originNode) => {
|
||||||
const restProps = {
|
if (!dragSort && !showIndex) {
|
||||||
rowSelection: rowSelection
|
return originNode;
|
||||||
? {
|
}
|
||||||
type: 'checkbox',
|
const current = props?.pagination?.current;
|
||||||
selectedRowKeys: selectedRowKeys,
|
const pageSize = props?.pagination?.pageSize || 20;
|
||||||
onChange(selectedRowKeys: any[], selectedRows: any[]) {
|
if (current) {
|
||||||
field.data = field.data || {};
|
index = index + (current - 1) * pageSize + 1;
|
||||||
field.data.selectedRowKeys = selectedRowKeys;
|
} else {
|
||||||
setSelectedRowKeys(selectedRowKeys);
|
index = index + 1;
|
||||||
onRowSelectionChange?.(selectedRowKeys, selectedRows);
|
}
|
||||||
},
|
if (record.__index) {
|
||||||
getCheckboxProps(record) {
|
index = extractIndex(record.__index);
|
||||||
return {
|
}
|
||||||
'aria-label': `checkbox`,
|
return (
|
||||||
};
|
|
||||||
},
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
|
role="button"
|
||||||
|
aria-label={`table-index-${index}`}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
checked ? 'checked' : null,
|
checked ? 'checked' : null,
|
||||||
css`
|
css`
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
float: left;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-evenly;
|
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
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'nb-origin-node',
|
|
||||||
checked ? 'checked' : null,
|
checked ? 'checked' : null,
|
||||||
css`
|
css`
|
||||||
position: absolute;
|
position: relative;
|
||||||
right: 50%;
|
display: flex;
|
||||||
transform: translateX(50%);
|
align-items: center;
|
||||||
&:not(.checked) {
|
justify-content: space-evenly;
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{originNode}
|
{dragSort && <SortHandle id={getRowKey(record)} />}
|
||||||
|
{showIndex && <TableIndex index={index} />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
{isRowSelect && (
|
||||||
</div>
|
<div
|
||||||
);
|
className={classNames(
|
||||||
},
|
'nb-origin-node',
|
||||||
...rowSelection,
|
checked ? 'checked' : null,
|
||||||
}
|
css`
|
||||||
: undefined,
|
position: absolute;
|
||||||
};
|
right: 50%;
|
||||||
const SortableWrapper = useCallback<React.FC>(
|
transform: translateX(50%);
|
||||||
({ children }) => {
|
&:not(.checked) {
|
||||||
return dragSort
|
display: none;
|
||||||
? React.createElement<Omit<SortableContextProps, 'children'>>(
|
}
|
||||||
SortableContext,
|
`,
|
||||||
{
|
)}
|
||||||
items: field.value?.map?.(getRowKey) || [],
|
>
|
||||||
|
{originNode}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
children,
|
...rowSelection,
|
||||||
)
|
}
|
||||||
: React.createElement(React.Fragment, {}, children);
|
: undefined,
|
||||||
},
|
};
|
||||||
[field, dragSort],
|
const SortableWrapper = useCallback<React.FC>(
|
||||||
);
|
({ children }) => {
|
||||||
const fieldSchema = useFieldSchema();
|
return dragSort
|
||||||
const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock;
|
? 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 { height: tableHeight, tableSizeRefCallback } = useTableSize();
|
||||||
const scroll = useMemo(() => {
|
const scroll = useMemo(() => {
|
||||||
return fixedBlock
|
return fixedBlock
|
||||||
? {
|
? {
|
||||||
x: 'max-content',
|
x: 'max-content',
|
||||||
y: tableHeight,
|
y: tableHeight,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
x: 'max-content',
|
x: 'max-content',
|
||||||
};
|
};
|
||||||
}, [fixedBlock, tableHeight]);
|
}, [fixedBlock, tableHeight]);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={css`
|
className={css`
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
.ant-table-wrapper {
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
.ant-spin-nested-loading {
|
overflow: hidden;
|
||||||
|
.ant-table-wrapper {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
.ant-spin-container {
|
.ant-spin-nested-loading {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
.ant-spin-container {
|
||||||
flex-direction: column;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.ant-table {
|
||||||
.ant-table {
|
overflow-x: auto;
|
||||||
overflow-x: auto;
|
overflow-y: hidden;
|
||||||
overflow-y: hidden;
|
}
|
||||||
}
|
`}
|
||||||
`}
|
>
|
||||||
>
|
<SortableWrapper>
|
||||||
<SortableWrapper>
|
<AntdTable
|
||||||
<AntdTable
|
ref={tableSizeRefCallback}
|
||||||
ref={tableSizeRefCallback}
|
rowKey={rowKey ?? defaultRowKey}
|
||||||
rowKey={rowKey ?? defaultRowKey}
|
dataSource={dataSource}
|
||||||
dataSource={dataSource}
|
tableLayout="auto"
|
||||||
tableLayout="auto"
|
{...others}
|
||||||
{...others}
|
{...restProps}
|
||||||
{...restProps}
|
pagination={paginationProps}
|
||||||
pagination={paginationProps}
|
components={components}
|
||||||
components={components}
|
onChange={(pagination, filters, sorter, extra) => {
|
||||||
onChange={(pagination, filters, sorter, extra) => {
|
onTableChange?.(pagination, filters, sorter, extra);
|
||||||
onTableChange?.(pagination, filters, sorter, extra);
|
}}
|
||||||
}}
|
onRow={onRow}
|
||||||
onRow={onRow}
|
rowClassName={(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : '')}
|
||||||
rowClassName={(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : '')}
|
scroll={scroll}
|
||||||
scroll={scroll}
|
columns={columns}
|
||||||
columns={columns}
|
expandable={{
|
||||||
expandable={{
|
onExpand: (flag, record) => {
|
||||||
onExpand: (flag, record) => {
|
const newKeys = flag
|
||||||
const newKeys = flag
|
? [...expandedKeys, record[collection.getPrimaryKey()]]
|
||||||
? [...expandedKeys, record[collection.getPrimaryKey()]]
|
: expandedKeys.filter((i) => record[collection.getPrimaryKey()] !== i);
|
||||||
: expandedKeys.filter((i) => record[collection.getPrimaryKey()] !== i);
|
setExpandesKeys(newKeys);
|
||||||
setExpandesKeys(newKeys);
|
onExpand?.(flag, record);
|
||||||
onExpand?.(flag, record);
|
},
|
||||||
},
|
expandedRowKeys: expandedKeys,
|
||||||
expandedRowKeys: expandedKeys,
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</SortableWrapper>
|
||||||
</SortableWrapper>
|
{field.errors.length > 0 && (
|
||||||
{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">
|
||||||
<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) => {
|
||||||
{field.errors.map((error) => {
|
return error.messages.map((message) => <div key={message}>{message}</div>);
|
||||||
return error.messages.map((message) => <div key={message}>{message}</div>);
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
{ displayName: 'Table' },
|
{ displayName: 'Table' },
|
||||||
);
|
);
|
||||||
|
@ -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}`,
|
||||||
}),
|
}),
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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