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

Commit 3b7c4f8

Browse files
alinastarkovMarc-Andre-Rivet
authored andcommitted
Copy paste headers (#523)
1 parent ea3a78b commit 3b7c4f8

File tree

7 files changed

+88
-14
lines changed

7 files changed

+88
-14
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
2525
row of `data`.
2626
- Additionally clearing the column will reset the filter for the affected column(s)
2727

28+
[#318](https://github.com/plotly/dash-table/issues/318)
29+
- Headers are included when copying from the table to different
30+
tabs and elsewhere. They are ignored when copying from the table onto itself and
31+
between two tables within the same tab.
32+
2833
### Changed
2934
[#497](https://github.com/plotly/dash-table/pull/497)
3035
- Like for clearing above, deleting through the `x` action will also

src/dash-table/components/ControlledTable/index.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -557,10 +557,12 @@ export default class ControlledTable extends PureComponent<ControlledTableProps>
557557
const {
558558
selected_cells,
559559
viewport,
560-
visibleColumns
560+
columns,
561+
visibleColumns,
562+
include_headers_on_copy_paste
561563
} = this.props;
562564

563-
TableClipboardHelper.toClipboard(e, selected_cells, visibleColumns, viewport.data);
565+
TableClipboardHelper.toClipboard(e, selected_cells, columns, visibleColumns, viewport.data, include_headers_on_copy_paste);
564566
this.$el.focus();
565567
}
566568

@@ -573,7 +575,8 @@ export default class ControlledTable extends PureComponent<ControlledTableProps>
573575
setProps,
574576
sort_by,
575577
viewport,
576-
visibleColumns
578+
visibleColumns,
579+
include_headers_on_copy_paste
577580
} = this.props;
578581

579582
if (!editable || !active_cell) {
@@ -587,7 +590,8 @@ export default class ControlledTable extends PureComponent<ControlledTableProps>
587590
visibleColumns,
588591
data,
589592
true,
590-
!sort_by.length || !filter_query.length
593+
!sort_by.length || !filter_query.length,
594+
include_headers_on_copy_paste
591595
);
592596

593597
if (result) {

src/dash-table/components/Table/props.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ interface IDefaultProps {
319319
fill_width: boolean;
320320
filter_query: string;
321321
filter_action: TableAction;
322+
include_headers_on_copy_paste: boolean;
322323
merge_duplicate_headers: boolean;
323324
fixed_columns: Fixed;
324325
fixed_rows: Fixed;

src/dash-table/dash/DataTable.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export const defaultProps = {
8888
columns: [],
8989
editable: false,
9090
export_format: 'none',
91+
include_headers_on_copy_paste: false,
9192
selected_cells: [],
9293
selected_rows: [],
9394
selected_row_ids: [],
@@ -285,6 +286,14 @@ export const propTypes = {
285286
* The `id` is not visible in the table.
286287
*/
287288
id: PropTypes.string.isRequired,
289+
290+
/**
291+
* If true, headers are included when copying from the table to different
292+
* tabs and elsewhere. Note that headers are ignored when copying from the table onto itself and
293+
* between two tables within the same tab.
294+
*/
295+
include_headers_on_copy_paste: PropTypes.bool,
296+
288297
/**
289298
* The `name` of the column,
290299
* as it appears in the column header.

src/dash-table/utils/TableClipboardHelper.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ import Clipboard from 'core/Clipboard';
55
import Logger from 'core/Logger';
66

77
import { ICellCoordinates, Columns, Data, SelectedCells } from 'dash-table/components/Table/props';
8+
import { createHeadings } from 'dash-table/components/Export/utils';
89
import applyClipboardToData from './applyClipboardToData';
10+
import getHeaderRows from 'dash-table/derived/header/headerRows';
911

1012
export default class TableClipboardHelper {
1113
private static lastLocalCopy: any[][] = [[]];
14+
private static localCopyWithoutHeaders: any[][] = [[]];
1215

13-
public static toClipboard(e: any, selectedCells: SelectedCells, columns: Columns, data: Data) {
16+
public static toClipboard(e: any, selectedCells: SelectedCells, columns: Columns, visibleColumns: Columns, data: Data, includeHeaders: boolean) {
1417
const selectedRows = R.uniq(R.pluck('row', selectedCells).sort((a, b) => a - b));
1518
const selectedCols: any = R.uniq(R.pluck('column', selectedCells).sort((a, b) => a - b));
1619

@@ -19,12 +22,21 @@ export default class TableClipboardHelper {
1922
R.last(selectedRows) as any + 1,
2023
data
2124
).map(row =>
22-
R.props(selectedCols, R.props(R.pluck('id', columns) as any, row) as any)
25+
R.props(selectedCols, R.props(R.pluck('id', visibleColumns) as any, row) as any)
2326
);
2427

25-
const value = SheetClip.prototype.stringify(df);
28+
let value = SheetClip.prototype.stringify(df);
2629
TableClipboardHelper.lastLocalCopy = df;
2730

31+
if (includeHeaders) {
32+
const transposedHeaders = createHeadings(R.pluck('name', visibleColumns), getHeaderRows(columns));
33+
const headers: any = R.map((row: string[]) => R.map((index: number) => row[index], selectedCols), transposedHeaders);
34+
const dfHeaders = headers.concat(df);
35+
value = SheetClip.prototype.stringify(dfHeaders);
36+
TableClipboardHelper.lastLocalCopy = dfHeaders;
37+
TableClipboardHelper.localCopyWithoutHeaders = df;
38+
}
39+
2840
Logger.trace('TableClipboard -- set clipboard data: ', value);
2941

3042
Clipboard.set(e, value);
@@ -37,7 +49,8 @@ export default class TableClipboardHelper {
3749
columns: Columns,
3850
data: Data,
3951
overflowColumns: boolean = true,
40-
overflowRows: boolean = true
52+
overflowRows: boolean = true,
53+
includeHeaders: boolean
4154
): { data: Data, columns: Columns } | void {
4255
const text = Clipboard.get(ev);
4356
Logger.trace('TableClipboard -- get clipboard data: ', text);
@@ -47,10 +60,8 @@ export default class TableClipboardHelper {
4760
}
4861

4962
const localDf = SheetClip.prototype.stringify(TableClipboardHelper.lastLocalCopy);
50-
51-
const values = localDf === text ?
52-
TableClipboardHelper.lastLocalCopy :
53-
SheetClip.prototype.parse(text);
63+
const localCopy = includeHeaders ? TableClipboardHelper.localCopyWithoutHeaders : TableClipboardHelper.lastLocalCopy;
64+
const values = (localDf === text) ? localCopy : SheetClip.prototype.parse(text);
5465

5566
return applyClipboardToData(
5667
values,

tests/cypress/dash/v_copy_paste.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,28 @@
4848
editable=True,
4949
sort_action='native',
5050
),
51+
dash_table.DataTable(
52+
id="table2",
53+
data=df[0:10],
54+
columns=[
55+
{"id": 0, "name": "Complaint ID"},
56+
{"id": 1, "name": "Product"},
57+
{"id": 2, "name": "Sub-product"},
58+
{"id": 3, "name": "Issue"},
59+
{"id": 4, "name": "Sub-issue"},
60+
{"id": 5, "name": "State"},
61+
{"id": 6, "name": "ZIP"},
62+
{"id": 7, "name": "code"},
63+
{"id": 8, "name": "Date received"},
64+
{"id": 9, "name": "Date sent to company"},
65+
{"id": 10, "name": "Company"},
66+
{"id": 11, "name": "Company response"},
67+
{"id": 12, "name": "Timely response?"},
68+
{"id": 13, "name": "Consumer disputed?"},
69+
],
70+
editable=True,
71+
sort_action='native',
72+
),
5173
]
5274
)
5375

@@ -71,6 +93,5 @@ def updateData(timestamp, current, previous):
7193

7294
return current
7395

74-
7596
if __name__ == "__main__":
7697
app.run_server(port=8082, debug=False)

tests/cypress/tests/server/copy_paste_test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,29 @@ describe('copy paste', () => {
5656
}
5757
});
5858

59+
it('can copy multiple rows and columns from one table and paste to another', () => {
60+
DashTable.getCell(10, 0).click();
61+
DOM.focused.type(Key.Shift, { release: false });
62+
DashTable.getCell(13, 3).click();
63+
64+
DOM.focused.type(`${Key.Meta}c`);
65+
cy.get(`#table2 tbody tr td.column-${0}`).eq(0).click();
66+
DOM.focused.type(`${Key.Meta}v`);
67+
cy.get(`#table2 tbody tr td.column-${3}`).eq(3).click();
68+
69+
DashTable.getCell(14, 0).click();
70+
DOM.focused.type(Key.Shift, { release: false });
71+
72+
for (let row = 9; row <= 13; ++row) {
73+
for (let column = 0; column <= 3; ++column) {
74+
let initialValue: string;
75+
76+
DashTable.getCell(row, column).within(() => cy.get('.dash-cell-value').then($cells => initialValue = $cells[0].innerHTML));
77+
cy.get(`#table2 tbody tr td.column-${column}`).eq(row - 10).within(() => cy.get('.dash-cell-value').should('have.html', initialValue));
78+
}
79+
}
80+
});
81+
5982
// Commenting this test as Cypress team is having issues with the copy/paste scenario
6083
// LINK: https://github.com/cypress-io/cypress/issues/2386
6184
describe('BE roundtrip on copy-paste', () => {
@@ -98,7 +121,7 @@ describe('copy paste', () => {
98121
});
99122

100123
it('BE rountrip with sorted, unfiltered data', () => {
101-
cy.get('tr th.column-2 .sort').last().click();
124+
cy.get('#table tr th.column-2 .sort').last().click();
102125

103126
DashTable.getCell(0, 0).click();
104127
DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.value', '11'));

0 commit comments

Comments
 (0)