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

3.1 issue142 copy paste #180

Merged
merged 7 commits into from
Oct 31, 2018
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ Derived properties allow the component to expose complex state that can be usefu

## RC8 - Improve props typing

Issue: https://github.com/plotly/dash-table/issues/143
Issue: https://github.com/plotly/dash-table/issues/143

## RC9 - Sort ascending on first click

Expand All @@ -410,7 +410,11 @@ Derived properties allow the component to expose complex state that can be usefu
## RC10 - Improved props docstrings

Issue: https://github.com/plotly/dash-table/issues/163

## RC11 - Style as list view

- Fix regressions linked to the style_as_list_view feature / prop

## RC12 - Fix copy/paste behavior when copied rows larger than data

Issue: https://github.com/plotly/dash-table/issues/142
4 changes: 2 additions & 2 deletions dash_table/bundle.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions dash_table/demo.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dash_table/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dash-table",
"version": "3.1.0rc11",
"version": "3.1.0rc12",
"description": "Dash table",
"main": "build/index.js",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dash-table",
"version": "3.1.0rc11",
"version": "3.1.0rc12",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bump version

"description": "Dash table",
"main": "build/index.js",
"scripts": {
Expand Down
73 changes: 11 additions & 62 deletions src/dash-table/utils/TableClipboardHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import SheetClip from 'sheetclip';
import Clipboard from 'core/Clipboard';
import Logger from 'core/Logger';

import { ActiveCell, Columns, Data, SelectedCells, ColumnType } from 'dash-table/components/Table/props';
import isEditable from 'dash-table/derived/cell/isEditable';
import { ActiveCell, Columns, Data, SelectedCells } from 'dash-table/components/Table/props';
import applyClipboardToData from './applyClipboardToData';

export default class TableClipboardHelper {
public static toClipboard(e: any, selectedCells: SelectedCells, columns: Columns, data: Data) {
Expand Down Expand Up @@ -36,74 +36,23 @@ export default class TableClipboardHelper {
overflowColumns: boolean = true,
overflowRows: boolean = true
): { data: Data, columns: Columns } | void {
const text = Clipboard.get(ev);
const text = Clipboard.get(ev as ClipboardEvent);
Logger.trace('TableClipboard -- get clipboard data: ', text);

if (!text) {
return;
}

if (!overflowRows) {
Logger.debug(`Clipboard -- Sorting or filtering active, do not create new rows`);
}

if (!overflowColumns) {
Logger.debug(`Clipboard -- Do not create new columns`);
}

const values = SheetClip.prototype.parse(text);

let newData = data;
const newColumns = columns;

if (overflowColumns && values[0].length + activeCell[1] >= columns.length) {
for (
let i = columns.length;
i < values[0].length + activeCell[1];
i++
) {
newColumns.push({
id: `Column ${i + 1}`,
name: `Column ${i + 1}`,
type: ColumnType.Text
});
newData.forEach(row => (row[`Column ${i}`] = ''));
}
}

const realActiveRow = derived_viewport_indices[activeCell[0]];
if (overflowRows && values.length + realActiveRow >= data.length) {
const emptyRow: any = {};
columns.forEach(c => (emptyRow[c.id] = ''));
newData = R.concat(
newData,
R.repeat(
emptyRow,
values.length + realActiveRow - data.length
)
);
}

values.forEach((row: string[], i: number) =>
row.forEach((cell: string, j: number) => {
const iOffset = activeCell[0] + i;
if (derived_viewport_indices.length <= activeCell[0] + i) {
return;
}
const iRealCell = derived_viewport_indices[iOffset];

const jOffset = activeCell[1] + j;
const col = newColumns[jOffset];
if (col && isEditable(true, col)) {
newData = R.set(
R.lensPath([iRealCell, col.id]),
cell,
newData
);
}
})
return applyClipboardToData(
values,
activeCell,
derived_viewport_indices,
columns,
data,
overflowColumns,
overflowRows
);

return { data: newData, columns: newColumns };
}
}
86 changes: 86 additions & 0 deletions src/dash-table/utils/applyClipboardToData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as R from 'ramda';

import Logger from 'core/Logger';

import { ActiveCell, Columns, Data, ColumnType } from 'dash-table/components/Table/props';
import isEditable from 'dash-table/derived/cell/isEditable';

export default (
values: string[][],
activeCell: ActiveCell,
derived_viewport_indices: number[],
columns: Columns,
data: Data,
overflowColumns: boolean = true,
overflowRows: boolean = true
): { data: Data, columns: Columns } | void => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isolated the pasting logic so it's not dependent on sheetclip and the clipboard event. Makes unit testing cleaner.

if (!overflowRows) {
Logger.debug(`Clipboard -- Sorting or filtering active, do not create new rows`);
}

if (!overflowColumns) {
Logger.debug(`Clipboard -- Do not create new columns`);
}

let newData = data;
const newColumns = columns;

if (overflowColumns && values[0].length + activeCell[1] >= columns.length) {
for (
let i = columns.length;
i < values[0].length + activeCell[1];
i++
) {
newColumns.push({
id: `Column ${i + 1}`,
name: `Column ${i + 1}`,
type: ColumnType.Text
});
newData.forEach(row => (row[`Column ${i}`] = ''));
}
}

const realActiveRow = derived_viewport_indices[activeCell[0]];
if (overflowRows && values.length + realActiveRow >= data.length) {
const emptyRow: any = {};
columns.forEach(c => (emptyRow[c.id] = ''));
newData = R.concat(
newData,
R.repeat(
emptyRow,
values.length + realActiveRow - data.length
)
);
}

const lastEntry = derived_viewport_indices.slice(-1)[0] || 0;
const viewportSize = derived_viewport_indices.length;

values.forEach((row: string[], i: number) =>
row.forEach((cell: string, j: number) => {
const viewportIndex = activeCell[0] + i;

let iRealCell: number | undefined = viewportSize > viewportIndex ?
derived_viewport_indices[viewportIndex] :
overflowRows ?
lastEntry + (viewportIndex - viewportSize + 1) :
undefined;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where the main change is. If the row falls in the existing (non-appended) data, map it directly to the viewport indices, if not, map it to the last entry of the viewport + offset, if overflow is not supported return undefined instead (will be validated below to exclude those rows from being processed)


if (iRealCell === undefined) {
return;
}

const jOffset = activeCell[1] + j;
const col = newColumns[jOffset];
if (col && isEditable(true, col)) {
newData = R.set(
R.lensPath([iRealCell, col.id]),
cell,
newData
);
}
})
);

return { data: newData, columns: newColumns };
};
172 changes: 172 additions & 0 deletions tests/cypress/tests/unit/clipboard_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import * as R from 'ramda';

import applyClipboardToData from 'dash-table/utils/applyClipboardToData';

describe('clipboard', () => {
describe('with column/row overflow allowed', () => {
Copy link
Contributor Author

@Marc-Andre-Rivet Marc-Andre-Rivet Oct 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Various tests to validate behavior when copying n rows into a m rows dataframe with overflow allowed (new rows get appended)

it('pastes one line at [0, 0] in one line df', () => {
const res = applyClipboardToData(
R.range(0, 1).map(value => [`${value}`]),
[0, 0],
R.range(0, 1),
['c1'].map(id => ({ id: id, name: id })),
R.range(0, 1).map(() => ({ c1: 'c1' })),
true,
true
);

expect(res).to.not.equal(undefined);

if (res) {
expect(res.data.length).to.equal(1);
expect(res.data[0].c1).to.equal('0');
}
});

it('pastes two lines at [0, 0] in one line df', () => {
const res = applyClipboardToData(
R.range(0, 2).map(value => [`${value}`]),
[0, 0],
R.range(0, 1),
['c1'].map(id => ({ id: id, name: id })),
R.range(0, 1).map(() => ({ c1: 'c1' })),
true,
true
);

expect(res).to.not.equal(undefined);

if (res) {
expect(res.data.length).to.equal(2);
expect(res.data[0].c1).to.equal('0');
expect(res.data[1].c1).to.equal('1');
}
});

it('pastes ten lines at [0, 0] in three line df', () => {
const res = applyClipboardToData(
R.range(0, 10).map(value => [`${value}`]),
[0, 0],
R.range(0, 3),
['c1'].map(id => ({ id: id, name: id })),
R.range(0, 3).map(() => ({ c1: 'c1' })),
true,
true
);

expect(res).to.not.equal(undefined);

if (res) {
expect(res.data.length).to.equal(10);
for (let i = 0; i < 10; ++i) {
expect(res.data[i].c1).to.equal(`${i}`);
}
}
});

it('pastes ten lines at [1, 0] in three line df', () => {
const res = applyClipboardToData(
R.range(0, 10).map(value => [`${value}`]),
[1, 0],
R.range(0, 3),
['c1'].map(id => ({ id: id, name: id })),
R.range(0, 3).map(() => ({ c1: 'c1' })),
true,
true
);

expect(res).to.not.equal(undefined);

if (res) {
expect(res.data.length).to.equal(11);
expect(res.data[0].c1).to.equal('c1');
for (let i = 0; i < 10; ++i) {
expect(res.data[i + 1].c1).to.equal(`${i}`);
}
}
});
});

describe('with column overflow allowed', () => {
it('pastes one line at [0, 0] in one line df', () => {
Copy link
Contributor Author

@Marc-Andre-Rivet Marc-Andre-Rivet Oct 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Various tests to validate behavior when copying n rows into a m rows dataframe with overflow unallowed (no new rows appended)

const res = applyClipboardToData(
R.range(0, 1).map(value => [`${value}`]),
[0, 0],
R.range(0, 1),
['c1'].map(id => ({ id: id, name: id })),
R.range(0, 1).map(() => ({ c1: 'c1' })),
true,
false
);

expect(res).to.not.equal(undefined);

if (res) {
expect(res.data.length).to.equal(1);
expect(res.data[0].c1).to.equal('0');
}
});

it('pastes two lines at [0, 0] in one line df', () => {
const res = applyClipboardToData(
R.range(0, 2).map(value => [`${value}`]),
[0, 0],
R.range(0, 1),
['c1'].map(id => ({ id: id, name: id })),
R.range(0, 1).map(() => ({ c1: 'c1' })),
true,
false
);

expect(res).to.not.equal(undefined);

if (res) {
expect(res.data.length).to.equal(1);
expect(res.data[0].c1).to.equal('0');
}
});

it('pastes ten lines at [0, 0] in three line df', () => {
const res = applyClipboardToData(
R.range(0, 10).map(value => [`${value}`]),
[0, 0],
R.range(0, 3),
['c1'].map(id => ({ id: id, name: id })),
R.range(0, 3).map(() => ({ c1: 'c1' })),
true,
false
);

expect(res).to.not.equal(undefined);

if (res) {
expect(res.data.length).to.equal(3);
for (let i = 0; i < 3; ++i) {
expect(res.data[i].c1).to.equal(`${i}`);
}
}
});

it('pastes ten lines at [1, 0] in three line df', () => {
const res = applyClipboardToData(
R.range(0, 10).map(value => [`${value}`]),
[1, 0],
R.range(0, 3),
['c1'].map(id => ({ id: id, name: id })),
R.range(0, 3).map(() => ({ c1: 'c1' })),
true,
false
);

expect(res).to.not.equal(undefined);

if (res) {
expect(res.data.length).to.equal(3);
expect(res.data[0].c1).to.equal('c1');
for (let i = 0; i < 2; ++i) {
expect(res.data[i + 1].c1).to.equal(`${i}`);
}
}
});
});
});