From b53709ceff9f9e09c7b90a481b2efc1ab03fad07 Mon Sep 17 00:00:00 2001 From: alinastarkov Date: Tue, 30 Jul 2019 09:26:54 -0400 Subject: [PATCH 01/12] add locakCopyWithoutHeaders --- src/dash-table/utils/TableClipboardHelper.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/dash-table/utils/TableClipboardHelper.ts b/src/dash-table/utils/TableClipboardHelper.ts index 54b19765d..b19641b21 100644 --- a/src/dash-table/utils/TableClipboardHelper.ts +++ b/src/dash-table/utils/TableClipboardHelper.ts @@ -5,15 +5,22 @@ 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) { 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)); - + const columnNames = R.pluck('name', columns); + // const mapIndexOuter = R.addIndex<(string[]), void>(R.map); + // const mapIndexInner = R.addIndex<(string), void>(R.map); + const transformedArray = createHeadings(columnNames, getHeaderRows(columns)); + const headers: any = transformedArray.map(row => selectedCols.map((index: number) => row[index])); const df = R.slice( R.head(selectedRows) as any, R.last(selectedRows) as any + 1, @@ -21,10 +28,10 @@ export default class TableClipboardHelper { ).map(row => R.props(selectedCols, R.props(R.pluck('id', columns) as any, row) as any) ); - - const value = SheetClip.prototype.stringify(df); - TableClipboardHelper.lastLocalCopy = df; - + const dfH = headers.concat(df); + const value = SheetClip.prototype.stringify(dfH); + TableClipboardHelper.lastLocalCopy = dfH; + TableClipboardHelper.localCopyWithoutHeaders = df; Logger.trace('TableClipboard -- set clipboard data: ', value); Clipboard.set(e, value); @@ -47,9 +54,8 @@ export default class TableClipboardHelper { } const localDf = SheetClip.prototype.stringify(TableClipboardHelper.lastLocalCopy); - const values = localDf === text ? - TableClipboardHelper.lastLocalCopy : + TableClipboardHelper.localCopyWithoutHeaders : SheetClip.prototype.parse(text); return applyClipboardToData( From 44ba00cbfe0ed2a42aeceda5c92cae9f599f2c0e Mon Sep 17 00:00:00 2001 From: alinastarkov Date: Tue, 30 Jul 2019 09:27:17 -0400 Subject: [PATCH 02/12] two tables in server test --- tests/cypress/dash/v_copy_paste.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/cypress/dash/v_copy_paste.py b/tests/cypress/dash/v_copy_paste.py index 052bcfe30..1e880e1a8 100644 --- a/tests/cypress/dash/v_copy_paste.py +++ b/tests/cypress/dash/v_copy_paste.py @@ -2,6 +2,7 @@ import dash from dash.dependencies import Input, Output, State import dash_html_components as html +import dash_core_components as dcc import os import pandas as pd import sys @@ -48,6 +49,30 @@ 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', + ), + dcc.Input(id='input-1', type='text', value=''), + dcc.Input(id='input-2', type='text', value=''), ] ) @@ -71,6 +96,5 @@ def updateData(timestamp, current, previous): return current - if __name__ == "__main__": app.run_server(port=8082, debug=False) From db99ce601156dab64ce39fdd6c927f6ad8c1a22d Mon Sep 17 00:00:00 2001 From: alinastarkov Date: Tue, 30 Jul 2019 14:48:16 -0400 Subject: [PATCH 03/12] remove input --- tests/cypress/dash/v_copy_paste.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/cypress/dash/v_copy_paste.py b/tests/cypress/dash/v_copy_paste.py index 1e880e1a8..f203a0844 100644 --- a/tests/cypress/dash/v_copy_paste.py +++ b/tests/cypress/dash/v_copy_paste.py @@ -71,8 +71,6 @@ editable=True, sort_action='native', ), - dcc.Input(id='input-1', type='text', value=''), - dcc.Input(id='input-2', type='text', value=''), ] ) From 41f2ac29ffce5baf4d61abe28be8e3016b88a698 Mon Sep 17 00:00:00 2001 From: alinastarkov Date: Tue, 30 Jul 2019 14:48:44 -0400 Subject: [PATCH 04/12] standalone test with two tables --- tests/cypress/tests/server/copy_paste_test.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/cypress/tests/server/copy_paste_test.ts b/tests/cypress/tests/server/copy_paste_test.ts index 242885bfc..f3cd2b2e4 100644 --- a/tests/cypress/tests/server/copy_paste_test.ts +++ b/tests/cypress/tests/server/copy_paste_test.ts @@ -56,6 +56,29 @@ describe('copy paste', () => { } }); + it('can copy 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', () => { @@ -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')); From eac2440e9723680fd227eedd54198dcef3140a90 Mon Sep 17 00:00:00 2001 From: alinastarkov Date: Tue, 30 Jul 2019 16:00:07 -0400 Subject: [PATCH 05/12] delete print line --- tests/integration/test_table_export_xlsx.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/test_table_export_xlsx.py b/tests/integration/test_table_export_xlsx.py index ac4799cde..33fe8b3a0 100644 --- a/tests/integration/test_table_export_xlsx.py +++ b/tests/integration/test_table_export_xlsx.py @@ -16,7 +16,6 @@ def test_tbex001_table_export(dash_duo): data=df.to_dict("records"), export_format="xlsx", ) - print(df) dash_duo.start_server(app) dash_duo.find_element(".export").click() From 07760e1b31c0cc33533d595f7a29e6231d580039 Mon Sep 17 00:00:00 2001 From: alinastarkov Date: Thu, 1 Aug 2019 10:46:16 -0400 Subject: [PATCH 06/12] remove unused component --- tests/cypress/dash/v_copy_paste.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/cypress/dash/v_copy_paste.py b/tests/cypress/dash/v_copy_paste.py index f203a0844..094274799 100644 --- a/tests/cypress/dash/v_copy_paste.py +++ b/tests/cypress/dash/v_copy_paste.py @@ -2,7 +2,6 @@ import dash from dash.dependencies import Input, Output, State import dash_html_components as html -import dash_core_components as dcc import os import pandas as pd import sys From 41d2b80434722fb0a0c3f63ade5dd16d3d408f78 Mon Sep 17 00:00:00 2001 From: alinastarkov Date: Thu, 1 Aug 2019 10:46:28 -0400 Subject: [PATCH 07/12] prettify code --- src/dash-table/utils/TableClipboardHelper.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/dash-table/utils/TableClipboardHelper.ts b/src/dash-table/utils/TableClipboardHelper.ts index b19641b21..3d1f8ba48 100644 --- a/src/dash-table/utils/TableClipboardHelper.ts +++ b/src/dash-table/utils/TableClipboardHelper.ts @@ -16,11 +16,9 @@ export default class TableClipboardHelper { public static toClipboard(e: any, selectedCells: SelectedCells, columns: Columns, data: Data) { 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)); - const columnNames = R.pluck('name', columns); - // const mapIndexOuter = R.addIndex<(string[]), void>(R.map); - // const mapIndexInner = R.addIndex<(string), void>(R.map); - const transformedArray = createHeadings(columnNames, getHeaderRows(columns)); - const headers: any = transformedArray.map(row => selectedCols.map((index: number) => row[index])); + + const transposedHeaders = createHeadings(R.pluck('name', columns), getHeaderRows(columns)); + const headers: any = R.map((row: string[]) => R.map((index: number) => row[index], selectedCols), transposedHeaders); const df = R.slice( R.head(selectedRows) as any, R.last(selectedRows) as any + 1, @@ -28,10 +26,12 @@ export default class TableClipboardHelper { ).map(row => R.props(selectedCols, R.props(R.pluck('id', columns) as any, row) as any) ); - const dfH = headers.concat(df); - const value = SheetClip.prototype.stringify(dfH); - TableClipboardHelper.lastLocalCopy = dfH; + + const dfHeaders = headers.concat(df); + const value = SheetClip.prototype.stringify(dfHeaders); + TableClipboardHelper.lastLocalCopy = dfHeaders; TableClipboardHelper.localCopyWithoutHeaders = df; + Logger.trace('TableClipboard -- set clipboard data: ', value); Clipboard.set(e, value); From 9d0f39678e546450866c07b296b1854cdbc01d32 Mon Sep 17 00:00:00 2001 From: alinastarkov Date: Thu, 1 Aug 2019 10:56:26 -0400 Subject: [PATCH 08/12] change test name --- tests/cypress/tests/server/copy_paste_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cypress/tests/server/copy_paste_test.ts b/tests/cypress/tests/server/copy_paste_test.ts index f3cd2b2e4..96d4cb1b5 100644 --- a/tests/cypress/tests/server/copy_paste_test.ts +++ b/tests/cypress/tests/server/copy_paste_test.ts @@ -56,7 +56,7 @@ describe('copy paste', () => { } }); - it('can copy from one table and paste to another', () => { + 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(); From 04910943f299b0596cc1355edaf3751674bfe0bb Mon Sep 17 00:00:00 2001 From: alinastarkov Date: Thu, 1 Aug 2019 11:05:20 -0400 Subject: [PATCH 09/12] update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09d14fdf4..53dbeec7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ 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. Headers 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 From 6ae8708d64a3df472cffcf7593342de4d3ab92e4 Mon Sep 17 00:00:00 2001 From: alinastarkov Date: Thu, 1 Aug 2019 11:11:18 -0400 Subject: [PATCH 10/12] change wordinga and linting in CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53dbeec7d..8981ebc2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). - 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. Headers are ignored when copying from the table onto itself and between two tables within the same tab. +- 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) From 25ac4ae8f025503932d1a7d00e4843e8446fb983 Mon Sep 17 00:00:00 2001 From: alinastarkov Date: Thu, 1 Aug 2019 16:37:31 -0400 Subject: [PATCH 11/12] add props for include headers on copy paste --- src/dash-table/components/Table/props.ts | 1 + src/dash-table/dash/DataTable.js | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 3905fb2e2..b2e3fd92b 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -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; diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index 21f70b1b8..562ed2a43 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -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: [], @@ -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. From 7c6b1ad26c4cc41d43b5231332a13cb4571ae940 Mon Sep 17 00:00:00 2001 From: alinastarkov Date: Thu, 1 Aug 2019 16:37:48 -0400 Subject: [PATCH 12/12] add props to table clipboard helpers --- .../components/ControlledTable/index.tsx | 12 +++++--- src/dash-table/utils/TableClipboardHelper.ts | 29 +++++++++++-------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index 6d80f0228..7d37039bb 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -557,10 +557,12 @@ export default class ControlledTable extends PureComponent 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(); } @@ -573,7 +575,8 @@ export default class ControlledTable extends PureComponent setProps, sort_by, viewport, - visibleColumns + visibleColumns, + include_headers_on_copy_paste } = this.props; if (!editable || !active_cell) { @@ -587,7 +590,8 @@ export default class ControlledTable extends PureComponent visibleColumns, data, true, - !sort_by.length || !filter_query.length + !sort_by.length || !filter_query.length, + include_headers_on_copy_paste ); if (result) { diff --git a/src/dash-table/utils/TableClipboardHelper.ts b/src/dash-table/utils/TableClipboardHelper.ts index 3d1f8ba48..91d02c58e 100644 --- a/src/dash-table/utils/TableClipboardHelper.ts +++ b/src/dash-table/utils/TableClipboardHelper.ts @@ -13,24 +13,29 @@ 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)); - const transposedHeaders = createHeadings(R.pluck('name', columns), getHeaderRows(columns)); - const headers: any = R.map((row: string[]) => R.map((index: number) => row[index], selectedCols), transposedHeaders); const df = R.slice( R.head(selectedRows) as any, 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 dfHeaders = headers.concat(df); - const value = SheetClip.prototype.stringify(dfHeaders); - TableClipboardHelper.lastLocalCopy = dfHeaders; - TableClipboardHelper.localCopyWithoutHeaders = 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); @@ -44,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); @@ -54,9 +60,8 @@ export default class TableClipboardHelper { } const localDf = SheetClip.prototype.stringify(TableClipboardHelper.lastLocalCopy); - const values = localDf === text ? - TableClipboardHelper.localCopyWithoutHeaders : - SheetClip.prototype.parse(text); + const localCopy = includeHeaders ? TableClipboardHelper.localCopyWithoutHeaders : TableClipboardHelper.lastLocalCopy; + const values = (localDf === text) ? localCopy : SheetClip.prototype.parse(text); return applyClipboardToData( values,