mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-02 03:58:07 +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
|
export const Column = types
|
||||||
.model('Column', {
|
.model('Column', {
|
||||||
label: types.optional(types.frozen(), undefined),
|
label: types.optional(types.frozen(), undefined),
|
||||||
@ -182,6 +188,14 @@ export const Row = types
|
|||||||
lazyRender: false
|
lazyRender: false
|
||||||
})
|
})
|
||||||
.views(self => ({
|
.views(self => ({
|
||||||
|
get parent() {
|
||||||
|
return getParent(self, 2);
|
||||||
|
},
|
||||||
|
|
||||||
|
get table() {
|
||||||
|
return getParent(self, self.depth * 2);
|
||||||
|
},
|
||||||
|
|
||||||
get expandable(): boolean {
|
get expandable(): boolean {
|
||||||
let table: any;
|
let table: any;
|
||||||
return !!(
|
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 {
|
get checked(): boolean {
|
||||||
return (getParent(self, self.depth * 2) as ITableStore).isSelected(
|
return (getParent(self, self.depth * 2) as ITableStore).isSelected(
|
||||||
self as IRow
|
self as IRow
|
||||||
@ -334,10 +376,12 @@ export const Row = types
|
|||||||
}))
|
}))
|
||||||
.actions(self => ({
|
.actions(self => ({
|
||||||
toggle(checked: boolean) {
|
toggle(checked: boolean) {
|
||||||
(getParent(self, self.depth * 2) as ITableStore).toggle(
|
const table = self.table as ITableStore;
|
||||||
self as IRow,
|
const row = self as IRow;
|
||||||
checked
|
|
||||||
);
|
table.toggle(row, checked);
|
||||||
|
table.toggleAncestors(row);
|
||||||
|
table.toggleDescendants(row, checked);
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleExpanded() {
|
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) {
|
function getToggleShiftRows(row: IRow) {
|
||||||
// 如果是同一个或非 multiple 模式下就和不用 shift 一样
|
// 如果是同一个或非 multiple 模式下就和不用 shift 一样
|
||||||
if (!lastCheckedRow || row === lastCheckedRow || !self.multiple) {
|
if (!lastCheckedRow || row === lastCheckedRow || !self.multiple) {
|
||||||
@ -1816,6 +1887,8 @@ export const TableStore = iRendererStore
|
|||||||
toggleAll,
|
toggleAll,
|
||||||
getSelectedRows,
|
getSelectedRows,
|
||||||
toggle,
|
toggle,
|
||||||
|
toggleAncestors,
|
||||||
|
toggleDescendants,
|
||||||
toggleShift,
|
toggleShift,
|
||||||
getToggleShiftRows,
|
getToggleShiftRows,
|
||||||
toggleExpandAll,
|
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
|
<Checkbox
|
||||||
classPrefix={ns}
|
classPrefix={ns}
|
||||||
type={multiple ? 'checkbox' : 'radio'}
|
type={multiple ? 'checkbox' : 'radio'}
|
||||||
checked={item.checked}
|
partial={item.partial}
|
||||||
|
checked={item.checked || item.partial}
|
||||||
disabled={item.checkdisable || !item.checkable}
|
disabled={item.checkdisable || !item.checkable}
|
||||||
onChange={onCheckboxChange}
|
onChange={onCheckboxChange}
|
||||||
/>
|
/>
|
||||||
|
@ -55,6 +55,7 @@ export class TableRow extends React.PureComponent<
|
|||||||
newIndex: number;
|
newIndex: number;
|
||||||
isHover: boolean;
|
isHover: boolean;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
|
partial?: boolean;
|
||||||
modified: boolean;
|
modified: boolean;
|
||||||
moved: boolean;
|
moved: boolean;
|
||||||
depth: number;
|
depth: number;
|
||||||
@ -358,6 +359,7 @@ export default observer((props: TableRowProps) => {
|
|||||||
id={item.id}
|
id={item.id}
|
||||||
newIndex={item.newIndex}
|
newIndex={item.newIndex}
|
||||||
isHover={item.isHover}
|
isHover={item.isHover}
|
||||||
|
partial={item.partial}
|
||||||
checked={item.checked}
|
checked={item.checked}
|
||||||
modified={item.modified}
|
modified={item.modified}
|
||||||
moved={item.moved}
|
moved={item.moved}
|
||||||
|
Loading…
Reference in New Issue
Block a user