Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
row of `data`.
- Additionally clearing the column will reset the filter for the affected column(s)

[#318](https://github.com/plotly/dash-table/issues/318)
- Headers are included when copying from the table to different
tabs and elsewhere. They are ignored when copying from the table onto itself and
between two tables within the same tab.

### Changed
[#497](https://github.com/plotly/dash-table/pull/497)
- Like for clearing above, deleting through the `x` action will also
Expand Down
12 changes: 8 additions & 4 deletions src/dash-table/components/ControlledTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -557,10 +557,12 @@ export default class ControlledTable extends PureComponent<ControlledTableProps>
const {
selected_cells,
viewport,
visibleColumns
columns,
visibleColumns,
include_headers_on_copy_paste
} = this.props;

TableClipboardHelper.toClipboard(e, selected_cells, visibleColumns, viewport.data);
TableClipboardHelper.toClipboard(e, selected_cells, columns, visibleColumns, viewport.data, include_headers_on_copy_paste);
this.$el.focus();
}

Expand All @@ -573,7 +575,8 @@ export default class ControlledTable extends PureComponent<ControlledTableProps>
setProps,
sort_by,
viewport,
visibleColumns
visibleColumns,
include_headers_on_copy_paste
} = this.props;

if (!editable || !active_cell) {
Expand All @@ -587,7 +590,8 @@ export default class ControlledTable extends PureComponent<ControlledTableProps>
visibleColumns,
data,
true,
!sort_by.length || !filter_query.length
!sort_by.length || !filter_query.length,
include_headers_on_copy_paste
);

if (result) {
Expand Down
1 change: 1 addition & 0 deletions src/dash-table/components/Table/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ interface IDefaultProps {
fill_width: boolean;
filter_query: string;
filter_action: TableAction;
include_headers_on_copy_paste: boolean;
merge_duplicate_headers: boolean;
fixed_columns: Fixed;
fixed_rows: Fixed;
Expand Down
9 changes: 9 additions & 0 deletions src/dash-table/dash/DataTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const defaultProps = {
columns: [],
editable: false,
export_format: 'none',
include_headers_on_copy_paste: false,
selected_cells: [],
selected_rows: [],
selected_row_ids: [],
Expand Down Expand Up @@ -285,6 +286,14 @@ export const propTypes = {
* The `id` is not visible in the table.
*/
id: PropTypes.string.isRequired,

/**
* If true, headers are included when copying from the table to different
* tabs and elsewhere. Note that headers are ignored when copying from the table onto itself and
* between two tables within the same tab.
*/
include_headers_on_copy_paste: PropTypes.bool,

/**
* The `name` of the column,
* as it appears in the column header.
Expand Down
27 changes: 19 additions & 8 deletions src/dash-table/utils/TableClipboardHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import Clipboard from 'core/Clipboard';
import Logger from 'core/Logger';

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

export default class TableClipboardHelper {
private static lastLocalCopy: any[][] = [[]];
private static localCopyWithoutHeaders: any[][] = [[]];

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

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

const value = SheetClip.prototype.stringify(df);
let value = SheetClip.prototype.stringify(df);
TableClipboardHelper.lastLocalCopy = df;

if (includeHeaders) {
const transposedHeaders = createHeadings(R.pluck('name', visibleColumns), getHeaderRows(columns));
const headers: any = R.map((row: string[]) => R.map((index: number) => row[index], selectedCols), transposedHeaders);
const dfHeaders = headers.concat(df);
value = SheetClip.prototype.stringify(dfHeaders);
TableClipboardHelper.lastLocalCopy = dfHeaders;
TableClipboardHelper.localCopyWithoutHeaders = df;
}

Logger.trace('TableClipboard -- set clipboard data: ', value);

Clipboard.set(e, value);
Expand All @@ -37,7 +49,8 @@ export default class TableClipboardHelper {
columns: Columns,
data: Data,
overflowColumns: boolean = true,
overflowRows: boolean = true
overflowRows: boolean = true,
includeHeaders: boolean
): { data: Data, columns: Columns } | void {
const text = Clipboard.get(ev);
Logger.trace('TableClipboard -- get clipboard data: ', text);
Expand All @@ -47,10 +60,8 @@ export default class TableClipboardHelper {
}

const localDf = SheetClip.prototype.stringify(TableClipboardHelper.lastLocalCopy);

const values = localDf === text ?
TableClipboardHelper.lastLocalCopy :
SheetClip.prototype.parse(text);
const localCopy = includeHeaders ? TableClipboardHelper.localCopyWithoutHeaders : TableClipboardHelper.lastLocalCopy;
const values = (localDf === text) ? localCopy : SheetClip.prototype.parse(text);

return applyClipboardToData(
values,
Expand Down
23 changes: 22 additions & 1 deletion tests/cypress/dash/v_copy_paste.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,28 @@
editable=True,
sort_action='native',
),
dash_table.DataTable(
id="table2",
data=df[0:10],
columns=[
{"id": 0, "name": "Complaint ID"},
{"id": 1, "name": "Product"},
{"id": 2, "name": "Sub-product"},
{"id": 3, "name": "Issue"},
{"id": 4, "name": "Sub-issue"},
{"id": 5, "name": "State"},
{"id": 6, "name": "ZIP"},
{"id": 7, "name": "code"},
{"id": 8, "name": "Date received"},
{"id": 9, "name": "Date sent to company"},
{"id": 10, "name": "Company"},
{"id": 11, "name": "Company response"},
{"id": 12, "name": "Timely response?"},
{"id": 13, "name": "Consumer disputed?"},
],
editable=True,
sort_action='native',
),
]
)

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

return current


if __name__ == "__main__":
app.run_server(port=8082, debug=False)
25 changes: 24 additions & 1 deletion tests/cypress/tests/server/copy_paste_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,29 @@ describe('copy paste', () => {
}
});

it('can copy multiple rows and columns from one table and paste to another', () => {
DashTable.getCell(10, 0).click();
DOM.focused.type(Key.Shift, { release: false });
DashTable.getCell(13, 3).click();

DOM.focused.type(`${Key.Meta}c`);
cy.get(`#table2 tbody tr td.column-${0}`).eq(0).click();
DOM.focused.type(`${Key.Meta}v`);
cy.get(`#table2 tbody tr td.column-${3}`).eq(3).click();

DashTable.getCell(14, 0).click();
DOM.focused.type(Key.Shift, { release: false });

for (let row = 9; row <= 13; ++row) {
for (let column = 0; column <= 3; ++column) {
let initialValue: string;

DashTable.getCell(row, column).within(() => cy.get('.dash-cell-value').then($cells => initialValue = $cells[0].innerHTML));
cy.get(`#table2 tbody tr td.column-${column}`).eq(row - 10).within(() => cy.get('.dash-cell-value').should('have.html', initialValue));
}
}
});

// Commenting this test as Cypress team is having issues with the copy/paste scenario
// LINK: https://github.com/cypress-io/cypress/issues/2386
describe('BE roundtrip on copy-paste', () => {
Expand Down Expand Up @@ -98,7 +121,7 @@ describe('copy paste', () => {
});

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

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