Merge pull request #8642 from ranwawa/feat/table-expandme-checkme-toggle-entire-tree

feat(amis): table嵌套批量操作时自动选择父/子级复选框
This commit is contained in:
hsm-lv 2023-11-17 10:19:52 +08:00 committed by GitHub
commit b5fd4819e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 373 additions and 5 deletions

View 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);
});
});

View File

@ -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,

View 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'
);
});
});

View File

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

View File

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