Skip to content
This repository was archived by the owner on Jun 4, 2024. It is now read-only.

Commit 187f652

Browse files
Marc-Andre-RivetShammamah Hossain
authored and
Shammamah Hossain
committed
Issue 591 - Row and column selection with multiple tables (#594)
1 parent 3469aa3 commit 187f652

File tree

10 files changed

+150
-85
lines changed

10 files changed

+150
-85
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
88
- The `datestartswith` relational operator now supports number comparison
99
- Fixed a bug where the implicit operator for columns was `equal` instead of the expected default for the column type
1010

11+
[#591](https://github.com/plotly/dash-table/issues/591)
12+
- Fixed row and column selection when multiple tables are present
13+
1114
## [4.3.0] - 2019-09-17
1215
### Added
1316
[#566](https://github.com/plotly/dash-table/pull/566)

demo/App.tsx

+11-10
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import Environment from 'core/environment';
66
import { memoizeOne } from 'core/memoizer';
77
import Logger from 'core/Logger';
88
import AppState, { AppMode, AppFlavor } from './AppMode';
9-
import memoizerCache from 'core/cache/memoizer';
109

1110
import './style.less';
1211

@@ -50,14 +49,18 @@ class App extends Component<any, any> {
5049
}} />
5150
</div>);
5251
} else if (mode === AppMode.TaleOfTwoTables) {
53-
const props: any = {};
54-
Object.entries(this.state.tableProps).forEach(([key, value]) => {
55-
props[key] = this.propCache.get(key)(value);
56-
});
52+
if (!this.state.tableProps2) {
53+
this.setState({
54+
tableProps2: R.clone(this.state.tableProps)
55+
});
56+
}
5757

58-
return (<DataTable
59-
{...props}
60-
/>);
58+
const baseId = this.state.tableProps2 && this.state.tableProps2.id;
59+
60+
return (this.state.tableProps2 ? <DataTable
61+
{...this.state.tableProps2}
62+
id={baseId ? 'table2' : baseId}
63+
/> : null);
6164
}
6265
}
6366

@@ -71,8 +74,6 @@ class App extends Component<any, any> {
7174
</div>);
7275
}
7376

74-
private propCache = memoizerCache<[string]>()(R.clone);
75-
7677
private setProps = memoizeOne(() => {
7778
return (newProps: any) => {
7879
Logger.debug('--->', newProps);

demo/AppMode.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export enum AppFlavor {
3636
FixedColumnPlus1 = 'fixed_columns={ "headers": true, "data": 1 }',
3737
FixedRow = 'fixed_rows={ "headers": true }',
3838
FixedRowPlus1 = 'fixed_rows={ "headers": true, "data": 1 }',
39-
Merged = 'merge_duplicate_headers=true'
39+
Merged = 'merge_duplicate_headers=true',
40+
NoId = 'id=null'
4041
}
4142

4243
export const ReadWriteModes = [
@@ -346,6 +347,7 @@ function getModeState(mode: string | null) {
346347
case AppMode.SingleHeaders:
347348
return getSingleHeaderState();
348349
case AppMode.TaleOfTwoTables:
350+
return getActionableState();
349351
case AppMode.Default:
350352
default:
351353
return getDefaultState();

src/dash-table/components/CellFactory.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export default class CellFactory {
4242
dropdown,
4343
data,
4444
dropdown_data,
45+
id,
4546
is_focused,
4647
row_deletable,
4748
row_selectable,
@@ -94,6 +95,7 @@ export default class CellFactory {
9495
);
9596

9697
const operations = this.cellOperations(
98+
id,
9799
data,
98100
virtualized.data,
99101
virtualized.indices,

src/dash-table/components/HeaderFactory.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export default class HeaderFactory {
4040
columns,
4141
data,
4242
hidden_columns,
43+
id,
4344
map,
4445
merge_duplicate_headers,
4546
page_action,
@@ -93,6 +94,7 @@ export default class HeaderFactory {
9394
);
9495

9596
const contents = this.headerContent(
97+
id,
9698
visibleColumns,
9799
columns,
98100
hidden_columns,

src/dash-table/derived/cell/operations.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function deleteRow(idx: number, data: Data, selectedRows: number[]) {
3333
return newProps;
3434
}
3535

36-
function rowSelectCell(idx: number, rowSelectable: Selection, selectedRows: number[], setProps: SetProps, data: Data) {
36+
function rowSelectCell(id: string, idx: number, rowSelectable: Selection, selectedRows: number[], setProps: SetProps, data: Data) {
3737
return (<td
3838
key='select'
3939
className='dash-select-cell'
@@ -42,7 +42,7 @@ function rowSelectCell(idx: number, rowSelectable: Selection, selectedRows: numb
4242
<input
4343
type={rowSelectable === 'single' ? 'radio' : 'checkbox'}
4444
style={{ verticalAlign: 'middle' }}
45-
name='row-select'
45+
name={`row-select-${id}`}
4646
checked={R.includes(idx, selectedRows)}
4747
onChange={() => {
4848
const newSelectedRows = rowSelectable === 'single' ?
@@ -73,6 +73,7 @@ function rowDeleteCell(doDelete: () => any) {
7373
}
7474

7575
const getter = (
76+
id: string,
7677
data: Data,
7778
viewportData: Data,
7879
viewportIndices: Indices,
@@ -83,7 +84,7 @@ const getter = (
8384
): JSX.Element[][] => R.addIndex<Datum, JSX.Element[]>(R.map)(
8485
(_, rowIndex) => [
8586
...(rowDeletable ? [rowDeleteCell(() => setProps(deleteRow(viewportIndices[rowIndex], data, selectedRows)))] : []),
86-
...(rowSelectable ? [rowSelectCell(viewportIndices[rowIndex], rowSelectable, selectedRows, setProps, data)] : [])
87+
...(rowSelectable ? [rowSelectCell(id, viewportIndices[rowIndex], rowSelectable, selectedRows, setProps, data)] : [])
8788
],
8889
viewportData
8990
);

src/dash-table/derived/header/content.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ function getSortingIcon(columnId: ColumnId, sortBy: SortBy) {
150150
}
151151

152152
function getter(
153+
id: string,
153154
visibleColumns: Columns,
154155
columns: Columns,
155156
hiddenColumns: string[] | undefined,
@@ -230,7 +231,7 @@ function getter(
230231
column_selectable === 'single',
231232
!allSelected
232233
)}
233-
name='column-header--select'
234+
name={`column-select-${id}`}
234235
type={column_selectable === 'single' ?
235236
'radio' :
236237
'checkbox'

tests/cypress/src/DashTable.ts

+44-38
Original file line numberDiff line numberDiff line change
@@ -19,104 +19,110 @@ const getSelector = (state: State) => {
1919
}
2020
};
2121

22-
export default class DashTable {
23-
static getCell(row: number, column: number, editable: State = State.Ready) {
24-
return cy.get(`#table ${getSelector(editable)} tbody tr td.column-${column}`).eq(row);
22+
export class DashTableHelper {
23+
constructor(private readonly id) {
24+
25+
}
26+
27+
public getCell(row: number, column: number, editable: State = State.Ready) {
28+
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr td.column-${column}`).eq(row);
2529
}
2630

27-
static getCellById(row: number, column: string, editable: State = State.Ready) {
28-
return cy.get(`#table ${getSelector(editable)} tbody tr td[data-dash-column="${column}"]`).eq(row);
31+
public getCellById(row: number, column: string, editable: State = State.Ready) {
32+
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr td[data-dash-column="${column}"]`).eq(row);
2933
}
3034

31-
static getFilter(column: number, editable: State = State.Ready) {
32-
return cy.get(`#table ${getSelector(editable)} tbody tr th.dash-filter.column-${column}`);
35+
public getFilter(column: number, editable: State = State.Ready) {
36+
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-filter.column-${column}`);
3337
}
3438

35-
static getFilterById(column: string, editable: State = State.Ready) {
36-
return cy.get(`#table ${getSelector(editable)} tbody tr th.dash-filter[data-dash-column="${column}"]`);
39+
public getFilterById(column: string, editable: State = State.Ready) {
40+
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-filter[data-dash-column="${column}"]`);
3741
}
3842

39-
static getHeader(row: number, column: number, editable: State = State.Ready) {
40-
return cy.get(`#table ${getSelector(editable)} tbody tr th.dash-header.column-${column}`).eq(row);
43+
public getHeader(row: number, column: number, editable: State = State.Ready) {
44+
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-header.column-${column}`).eq(row);
4145
}
4246

43-
static getHeaderById(row: number, column: string, editable: State = State.Ready) {
44-
return cy.get(`#table ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"]`).eq(row);
47+
public getHeaderById(row: number, column: string, editable: State = State.Ready) {
48+
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"]`).eq(row);
4549
}
4650

47-
static focusCell(row: number, column: number) {
51+
public focusCell(row: number, column: number) {
4852
// somehow we need to scrollIntoView AFTER click, or it doesn't
4953
// work right. Why?
5054
return this.getCell(row, column).click().scrollIntoView();
5155
}
5256

53-
static focusCellById(row: number, column: string) {
57+
public focusCellById(row: number, column: string) {
5458
return this.getCellById(row, column).click().scrollIntoView();
5559
}
5660

57-
static clearColumnById(row: number, column: string, editable: State = State.Ready) {
58-
return cy.get(`#table ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"] .column-header--clear`).eq(row).click();
61+
public clearColumnById(row: number, column: string, editable: State = State.Ready) {
62+
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"] .column-header--clear`).eq(row).click();
5963
}
6064

61-
static deleteColumnById(row: number, column: string, editable: State = State.Ready) {
62-
return cy.get(`#table ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"] .column-header--delete`).eq(row).click();
65+
public deleteColumnById(row: number, column: string, editable: State = State.Ready) {
66+
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"] .column-header--delete`).eq(row).click();
6367
}
6468

65-
static hideColumnById(row: number, column: string, editable: State = State.Ready) {
66-
return cy.get(`#table ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"] .column-header--hide`).eq(row).click();
69+
public hideColumnById(row: number, column: string, editable: State = State.Ready) {
70+
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"] .column-header--hide`).eq(row).click();
6771
}
6872

69-
static getSelectColumnById(row: number, column: string, editable: State = State.Ready) {
70-
return cy.get(`#table ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"] .column-header--select input`).eq(row);
73+
public getSelectColumnById(row: number, column: string, editable: State = State.Ready) {
74+
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-header[data-dash-column="${column}"] .column-header--select input`).eq(row);
7175
}
7276

73-
static selectColumnById(row: number, column: string) {
74-
return DashTable.getSelectColumnById(row, column).click();
77+
public selectColumnById(row: number, column: string) {
78+
return this.getSelectColumnById(row, column).click();
7579
}
7680

77-
static getDelete(row: number, editable: State = State.Ready) {
78-
return cy.get(`#table ${getSelector(editable)} tbody tr td.dash-delete-cell`).eq(row);
81+
public getDelete(row: number, editable: State = State.Ready) {
82+
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr td.dash-delete-cell`).eq(row);
7983
}
8084

81-
static getSelect(row: number, editable: State = State.Ready) {
82-
return cy.get(`#table ${getSelector(editable)} tbody tr td.dash-select-cell`).eq(row);
85+
public getSelect(row: number, editable: State = State.Ready) {
86+
return cy.get(`#${this.id} ${getSelector(editable)} tbody tr td.dash-select-cell`).eq(row);
8387
}
8488

85-
static getActiveCell(editable: State = State.Ready) {
86-
return cy.get(`#table ${getSelector(editable)} tbody td.focused`);
89+
public getActiveCell(editable: State = State.Ready) {
90+
return cy.get(`#${this.id} ${getSelector(editable)} tbody td.focused`);
8791
}
8892

89-
static getSelectedCells(editable: State = State.Ready) {
90-
return cy.get(`#table ${getSelector(editable)} tbody td.cell--selected`);
93+
public getSelectedCells(editable: State = State.Ready) {
94+
return cy.get(`#${this.id} ${getSelector(editable)} tbody td.cell--selected`);
9195
}
9296

93-
static scrollToTop() {
97+
public scrollToTop() {
9498
cy.get(`.cell.cell-1-1.dash-fixed-content`).invoke(`outerHeight`).then(height => {
9599
cy.scrollTo(0, -height);
96100
});
97101
}
98102

99-
static scrollToBottom() {
103+
public scrollToBottom() {
100104
cy.get(`.cell.cell-1-1.dash-fixed-content`).invoke(`outerHeight`).then(height => {
101105
cy.scrollTo(0, height);
102106
});
103107
}
104108

105-
static getCellInLastRowOfColumn(column: number) {
109+
public getCellInLastRowOfColumn(column: number) {
106110
const cellInLastRow = cy.get(`td.dash-cell.column-${column}`).last().then(elem => {
107111
const lastRow = elem ? elem.attr(`data-dash-row`) : undefined;
108112
return lastRow ? cy.get(`td.dash-cell.column-${column}[data-dash-row="${lastRow}"`) : undefined;
109113
});
110114
return cellInLastRow;
111115
}
112116

113-
static getCellFromDataDash(row: number, column: number) {
117+
public getCellFromDataDash(row: number, column: number) {
114118
return cy.get(`td.column-${column}[data-dash-row="${row}"]`);
115119
}
116120

117-
static toggleScroll(toggled: boolean) {
121+
public toggleScroll(toggled: boolean) {
118122
cy.get('.row-1').then($el => {
119123
$el[0].style.overflow = toggled ? '' : 'unset';
120124
});
121125
}
122126
}
127+
128+
export default new DashTableHelper('table');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import DashTable, { DashTableHelper } from 'cypress/DashTable';
2+
3+
import { BasicModes, AppMode, AppFlavor } from 'demo/AppMode';
4+
5+
Object.values(BasicModes).forEach(mode => {
6+
describe(`select row, mode=${mode}`, () => {
7+
beforeEach(() => {
8+
cy.visit(`http://localhost:8080?mode=${mode}`);
9+
DashTable.toggleScroll(false);
10+
});
11+
12+
describe('fe pagination & sort', () => {
13+
it('can select row', () => {
14+
DashTable.getSelect(0).within(() => cy.get('input').click());
15+
DashTable.getSelect(0).within(() => cy.get('input').should('be.checked'));
16+
});
17+
18+
it('can select row when sorted', () => {
19+
cy.get('tr th.column-0 .column-header--sort').last().click({ force: true });
20+
DashTable.getSelect(0).within(() => cy.get('input').click());
21+
DashTable.getSelect(0).within(() => cy.get('input').should('be.checked'));
22+
});
23+
24+
it('select, sort, new row is not selected', () => {
25+
DashTable.getSelect(0).within(() => cy.get('input').click());
26+
DashTable.getSelect(0).within(() => cy.get('input').should('be.checked'));
27+
cy.get('tr th.column-0 .column-header--sort').last().click({ force: true }).click({ force: true });
28+
DashTable.getSelect(0).within(() => cy.get('input').should('not.be.checked'));
29+
});
30+
});
31+
});
32+
});
33+
34+
describe('select rows & columns in multiple tables without id', () => {
35+
let table1: DashTableHelper;
36+
let table2: DashTableHelper;
37+
38+
beforeEach(() => {
39+
cy.visit(`http://localhost:8080?mode=${AppMode.TaleOfTwoTables}&flavor=${[AppFlavor.ColumnSelectableSingle,AppFlavor.NoId].join(';')}`).then(() => {
40+
const ids: string[] = [];
41+
return cy.get('.dash-spreadsheet-container').parent().each(el => {
42+
const id = el.attr('id');
43+
if (id) {
44+
ids.push(id);
45+
}
46+
}).then(() => ids);
47+
}).then(ids => {
48+
cy.log('table ids', ids);
49+
expect(ids.length).to.equal(2);
50+
51+
table1 = new DashTableHelper(ids[0]);
52+
table2 = new DashTableHelper(ids[1]);
53+
});
54+
55+
DashTable.toggleScroll(false);
56+
});
57+
58+
it('can select a row in both tables', () => {
59+
table1.getSelect(0).within(() => cy.get('input').click());
60+
table1.getSelect(0).within(() => cy.get('input').should('be.checked'));
61+
62+
table2.getSelect(1).within(() => cy.get('input').click());
63+
table2.getSelect(1).within(() => cy.get('input').should('be.checked'));
64+
65+
table1.getSelect(0).within(() => cy.get('input').should('be.checked'));
66+
table1.getSelect(1).within(() => cy.get('input').should('not.be.checked'));
67+
});
68+
69+
it('can select a column in both tables', () => {
70+
table1.selectColumnById(0, 'ccc');
71+
table1.getSelectColumnById(2, 'ccc').should('be.checked');
72+
73+
table2.selectColumnById(0, 'rows');
74+
table2.getSelectColumnById(2, 'rows').should('be.checked');
75+
76+
table1.getSelectColumnById(2, 'ccc').should('be.checked');
77+
table1.getSelectColumnById(2, 'rows').should('not.be.checked');
78+
});
79+
});

0 commit comments

Comments
 (0)