mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-01 19:47:56 +08:00
Merge pull request #8642 from ranwawa/feat/table-expandme-checkme-toggle-entire-tree
feat(amis): table嵌套批量操作时自动选择父/子级复选框
This commit is contained in:
commit
b5fd4819e6
113
packages/amis-core/__tests__/store/table.test.ts
Normal file
113
packages/amis-core/__tests__/store/table.test.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import {TableStore, SELECTED_STATUS} from '../../src/store/table';
|
||||
|
||||
import type {IRow, ITableStore} from '../../src/store/table';
|
||||
|
||||
let table: ITableStore;
|
||||
let firstLevel: IRow;
|
||||
let secondLevel: IRow;
|
||||
let secondLevel2: IRow;
|
||||
let thirdLevel: IRow;
|
||||
let thirdLevel2: IRow;
|
||||
let thirdLevel3: IRow;
|
||||
|
||||
beforeEach(() => {
|
||||
table = TableStore.create({id: 'mock-id', storeType: 'table'});
|
||||
table.initRows([
|
||||
{
|
||||
name: '手机维修',
|
||||
children: [
|
||||
{
|
||||
name: '苹果',
|
||||
children: [
|
||||
{
|
||||
name: 'iphone系列'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '华为',
|
||||
children: [
|
||||
{
|
||||
name: 'mate系列'
|
||||
},
|
||||
{
|
||||
name: 'p系列'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
firstLevel = table.rows[0];
|
||||
secondLevel = table.rows[0].children[0];
|
||||
secondLevel2 = table.rows[0].children[1];
|
||||
thirdLevel = table.rows[0].children[0].children[0];
|
||||
thirdLevel2 = table.rows[0].children[1].children[0];
|
||||
thirdLevel3 = table.rows[0].children[1].children[1];
|
||||
});
|
||||
|
||||
describe('Row', () => {
|
||||
it('通过parent快速拿到父节点', () => {
|
||||
expect(secondLevel.parent).toEqual(firstLevel);
|
||||
});
|
||||
|
||||
it('通过table快速拿到根节点', () => {
|
||||
expect(thirdLevel.table).toEqual(table);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TableStore', () => {
|
||||
it('选中父节点,所有子节点自动选中', () => {
|
||||
firstLevel.toggle(true);
|
||||
|
||||
const selectedRows = table.selectedRows.map(row => row.data.name);
|
||||
|
||||
expect(selectedRows).toEqual(
|
||||
expect.arrayContaining([
|
||||
'苹果',
|
||||
'iphone系列',
|
||||
'华为',
|
||||
'mate系列',
|
||||
'p系列'
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('选中所有子节点,所有父节点自动选中', () => {
|
||||
thirdLevel.toggle(true);
|
||||
thirdLevel2.toggle(true);
|
||||
thirdLevel3.toggle(true);
|
||||
|
||||
const selectedRows = table.selectedRows.map(row => row.data.name);
|
||||
|
||||
expect(selectedRows).toEqual(
|
||||
expect.arrayContaining(['苹果', '华为', '手机维修'])
|
||||
);
|
||||
});
|
||||
|
||||
it('默认情况下,父节点是未选中状态,选中一个子节点切换到部分选中状态,选中所有2个子节点切换到全部选中状态', () => {
|
||||
expect(firstLevel.childrenSelected()).toBe(SELECTED_STATUS.NONE);
|
||||
expect(firstLevel.partial).toBe(false);
|
||||
|
||||
secondLevel.toggle(true);
|
||||
|
||||
expect(firstLevel.childrenSelected()).toBe(SELECTED_STATUS.PARTIAL);
|
||||
expect(firstLevel.partial).toBe(true);
|
||||
|
||||
secondLevel2.toggle(true);
|
||||
|
||||
expect(firstLevel.childrenSelected()).toBe(SELECTED_STATUS.ALL);
|
||||
expect(firstLevel.partial).toBe(false);
|
||||
});
|
||||
|
||||
it('多个子节点只选中一个,祖先节点不会被选中,但会被标记为部分选中状态', () => {
|
||||
thirdLevel2.toggle(true);
|
||||
|
||||
expect(secondLevel2.checked).toBe(false);
|
||||
expect(secondLevel2.partial).toBe(true);
|
||||
|
||||
expect(firstLevel.checked).toBe(false);
|
||||
expect(firstLevel.partial).toBe(true);
|
||||
});
|
||||
});
|
@ -78,6 +78,12 @@ function initChildren(
|
||||
});
|
||||
}
|
||||
|
||||
export enum SELECTED_STATUS {
|
||||
ALL,
|
||||
PARTIAL,
|
||||
NONE
|
||||
}
|
||||
|
||||
export const Column = types
|
||||
.model('Column', {
|
||||
label: types.optional(types.frozen(), undefined),
|
||||
@ -182,6 +188,14 @@ export const Row = types
|
||||
lazyRender: false
|
||||
})
|
||||
.views(self => ({
|
||||
get parent() {
|
||||
return getParent(self, 2);
|
||||
},
|
||||
|
||||
get table() {
|
||||
return getParent(self, self.depth * 2);
|
||||
},
|
||||
|
||||
get expandable(): boolean {
|
||||
let table: any;
|
||||
return !!(
|
||||
@ -193,6 +207,34 @@ export const Row = types
|
||||
);
|
||||
},
|
||||
|
||||
childrenSelected() {
|
||||
const {children, table} = self as IRow;
|
||||
|
||||
const selectedLength = children.filter((child: IRow) =>
|
||||
(table as ITableStore).isSelected(child)
|
||||
).length;
|
||||
|
||||
if (!selectedLength) {
|
||||
return SELECTED_STATUS.NONE;
|
||||
}
|
||||
|
||||
if (selectedLength === children.length) {
|
||||
return SELECTED_STATUS.ALL;
|
||||
}
|
||||
|
||||
return SELECTED_STATUS.PARTIAL;
|
||||
},
|
||||
|
||||
get partial(): boolean {
|
||||
const childrenSelected =
|
||||
this.childrenSelected() === SELECTED_STATUS.PARTIAL;
|
||||
const childrenPartial = (self as IRow).children.some(
|
||||
(child: IRow) => child.partial
|
||||
);
|
||||
|
||||
return childrenSelected || childrenPartial;
|
||||
},
|
||||
|
||||
get checked(): boolean {
|
||||
return (getParent(self, self.depth * 2) as ITableStore).isSelected(
|
||||
self as IRow
|
||||
@ -334,10 +376,12 @@ export const Row = types
|
||||
}))
|
||||
.actions(self => ({
|
||||
toggle(checked: boolean) {
|
||||
(getParent(self, self.depth * 2) as ITableStore).toggle(
|
||||
self as IRow,
|
||||
checked
|
||||
);
|
||||
const table = self.table as ITableStore;
|
||||
const row = self as IRow;
|
||||
|
||||
table.toggle(row, checked);
|
||||
table.toggleAncestors(row);
|
||||
table.toggleDescendants(row, checked);
|
||||
},
|
||||
|
||||
toggleExpanded() {
|
||||
@ -1552,6 +1596,33 @@ export const TableStore = iRendererStore
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAncestors(row: IRow) {
|
||||
const parent = row.parent as IRow;
|
||||
|
||||
if (!parent.depth) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedStatus = parent.childrenSelected();
|
||||
|
||||
toggle(parent, selectedStatus === SELECTED_STATUS.ALL);
|
||||
|
||||
toggleAncestors(parent);
|
||||
}
|
||||
|
||||
function toggleDescendants(row: IRow, checked: boolean) {
|
||||
const {children} = row;
|
||||
|
||||
if (!children?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
children.forEach((child: IRow) => {
|
||||
toggle(child, checked);
|
||||
toggleDescendants(child, checked);
|
||||
});
|
||||
}
|
||||
|
||||
function getToggleShiftRows(row: IRow) {
|
||||
// 如果是同一个或非 multiple 模式下就和不用 shift 一样
|
||||
if (!lastCheckedRow || row === lastCheckedRow || !self.multiple) {
|
||||
@ -1816,6 +1887,8 @@ export const TableStore = iRendererStore
|
||||
toggleAll,
|
||||
getSelectedRows,
|
||||
toggle,
|
||||
toggleAncestors,
|
||||
toggleDescendants,
|
||||
toggleShift,
|
||||
getToggleShiftRows,
|
||||
toggleExpandAll,
|
||||
|
179
packages/amis/__tests__/renderers/Table/Cell.test.tsx
Normal file
179
packages/amis/__tests__/renderers/Table/Cell.test.tsx
Normal file
@ -0,0 +1,179 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
waitFor,
|
||||
screen,
|
||||
within
|
||||
} from '@testing-library/react';
|
||||
import {render as amisRender} from '../../../src';
|
||||
import {makeEnv} from '../../helper';
|
||||
|
||||
const items = [
|
||||
{
|
||||
name: '手机维修',
|
||||
children: [
|
||||
{
|
||||
name: '苹果',
|
||||
children: [
|
||||
{
|
||||
name: 'iphone系列'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '华为',
|
||||
children: [
|
||||
{
|
||||
name: 'mate系列'
|
||||
},
|
||||
{
|
||||
name: 'p系列'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const getExpandBtnByText = (text: string) => {
|
||||
const tdElement = screen.getByText(text).parentElement as HTMLElement;
|
||||
|
||||
return tdElement.querySelector('.cxd-Table-expandBtn2') as HTMLAnchorElement;
|
||||
};
|
||||
|
||||
const getCheckMeBtnByText = (text: string) => {
|
||||
const trElement = screen.getByText(text).parentElement!
|
||||
.parentElement as HTMLElement;
|
||||
|
||||
return within(trElement!).getByRole('checkbox');
|
||||
};
|
||||
|
||||
const renderTable = () =>
|
||||
render(
|
||||
amisRender(
|
||||
{
|
||||
type: 'crud',
|
||||
headerToolbar: ['bulkActions'],
|
||||
bulkActions: [
|
||||
{
|
||||
type: 'button',
|
||||
label: '批量操作'
|
||||
}
|
||||
],
|
||||
footable: {
|
||||
expand: 'first'
|
||||
},
|
||||
data: {
|
||||
items
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'name'
|
||||
}
|
||||
]
|
||||
},
|
||||
{},
|
||||
makeEnv({})
|
||||
)
|
||||
);
|
||||
|
||||
describe('层级选择', () => {
|
||||
it('选择根节点,所有后代节点都会自动选中', async () => {
|
||||
renderTable();
|
||||
const checkMeFirst = getCheckMeBtnByText('手机维修');
|
||||
|
||||
expect(checkMeFirst).toBeInTheDocument();
|
||||
|
||||
// 选中一级节点
|
||||
fireEvent.click(checkMeFirst);
|
||||
|
||||
// 一级节点选中
|
||||
await waitFor(() => {
|
||||
expect(checkMeFirst).toBeChecked();
|
||||
});
|
||||
|
||||
const checkMeSecond1 = getCheckMeBtnByText('苹果');
|
||||
const checkMeSecond2 = getCheckMeBtnByText('华为');
|
||||
|
||||
// 二级节点选中
|
||||
expect(checkMeSecond1).toBeChecked();
|
||||
expect(checkMeSecond2).toBeChecked();
|
||||
|
||||
const expandSecond1 = getExpandBtnByText('苹果');
|
||||
|
||||
expect(expandSecond1).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(expandSecond1);
|
||||
|
||||
const checkMeThird1 = getCheckMeBtnByText('iphone系列');
|
||||
|
||||
// 三级节点选中
|
||||
await waitFor(() => {
|
||||
expect(checkMeThird1).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('选择所有子节点,所有祖先点选中', async () => {
|
||||
renderTable();
|
||||
const expandSecond1 = getExpandBtnByText('苹果');
|
||||
|
||||
// 展开三级节点
|
||||
fireEvent.click(expandSecond1);
|
||||
const checkMeThird1 = getCheckMeBtnByText('iphone系列');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(checkMeThird1).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(checkMeThird1);
|
||||
|
||||
const checkMeSecond1 = getCheckMeBtnByText('苹果');
|
||||
const checkMeFirst = getCheckMeBtnByText('手机维修');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(checkMeSecond1).toBeChecked();
|
||||
expect(checkMeSecond1.parentElement!.classList).toContain(
|
||||
'cxd-Checkbox--full'
|
||||
);
|
||||
});
|
||||
|
||||
// 根节点下包含苹果和华为两个节点
|
||||
// 华为节点是未选中,所以根节点应该显示部分选中的样式
|
||||
expect(checkMeFirst).toBeChecked();
|
||||
expect(checkMeFirst.parentElement!.classList).not.toContain(
|
||||
'cxd-Checkbox--full'
|
||||
);
|
||||
});
|
||||
|
||||
it('选择部分子节点,所有祖先点选中,但选中状态是部分选中', async () => {
|
||||
renderTable();
|
||||
const expandSecond2 = getExpandBtnByText('华为');
|
||||
|
||||
// 展开三级节点
|
||||
fireEvent.click(expandSecond2);
|
||||
const checkMeThird1 = getCheckMeBtnByText('mate系列');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(checkMeThird1).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(checkMeThird1);
|
||||
|
||||
const checkMeSecond2 = getCheckMeBtnByText('华为');
|
||||
const checkMeFirst = getCheckMeBtnByText('手机维修');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(checkMeSecond2).toBeChecked();
|
||||
expect(checkMeSecond2.parentElement!.classList).not.toContain(
|
||||
'cxd-Checkbox--full'
|
||||
);
|
||||
});
|
||||
|
||||
expect(checkMeFirst).toBeChecked();
|
||||
expect(checkMeFirst.parentElement!.classList).not.toContain(
|
||||
'cxd-Checkbox--full'
|
||||
);
|
||||
});
|
||||
});
|
@ -82,7 +82,8 @@ export default function Cell({
|
||||
<Checkbox
|
||||
classPrefix={ns}
|
||||
type={multiple ? 'checkbox' : 'radio'}
|
||||
checked={item.checked}
|
||||
partial={item.partial}
|
||||
checked={item.checked || item.partial}
|
||||
disabled={item.checkdisable || !item.checkable}
|
||||
onChange={onCheckboxChange}
|
||||
/>
|
||||
|
@ -55,6 +55,7 @@ export class TableRow extends React.PureComponent<
|
||||
newIndex: number;
|
||||
isHover: boolean;
|
||||
checked: boolean;
|
||||
partial?: boolean;
|
||||
modified: boolean;
|
||||
moved: boolean;
|
||||
depth: number;
|
||||
@ -358,6 +359,7 @@ export default observer((props: TableRowProps) => {
|
||||
id={item.id}
|
||||
newIndex={item.newIndex}
|
||||
isHover={item.isHover}
|
||||
partial={item.partial}
|
||||
checked={item.checked}
|
||||
modified={item.modified}
|
||||
moved={item.moved}
|
||||
|
Loading…
Reference in New Issue
Block a user