Skip to content

Commit 3415025

Browse files
committedMar 18, 2025·
[FIX] clipboard: cross-sheet cut/paste is broken for tables/cfs
Preface: the handling of `MOVE_RANGES` is broken in multiple plugins. When calling `adaptRanges`, we don't check that the resulting range is in the same sheet as the original range. Fixing that in stable is probably not a good idea. This would mean that suddenly tables/cfs/merges could appear where they were previously not, and could break existing sheets. With the clipboard refactoring, the `MOVE_RANGES` command is now dispatched at the start of the paste handling (in `CellClipboardHandler`). At this dispatch, the CFs/Tables/Merges are moved to the wrong sheet. So in a cut, when the time comes to delete the original Table, the table is not found at the copy position, and the delete fails. This commit fixes that by dispatching the `MOVE_RANGES` command at the very end of the paste handling, after the tables/cfs/merges have been moved/deleted by their respective handlers. Note: the cut/paste of filter values does not work anymore. It somewhat was working before because of the `MOVE_RANGES`, but doesn't work anymore since `UPDATE_FILTER` is not working before the next evaluation because of dynamic tables. closes #5951 Task: 3905618 X-original-commit: 43d5192 Signed-off-by: Vincent Schippefilt (vsc) <[email protected]> Signed-off-by: Adrien Minne (adrm) <[email protected]>
1 parent 4977c01 commit 3415025

File tree

6 files changed

+84
-39
lines changed

6 files changed

+84
-39
lines changed
 

‎src/clipboard_handlers/cell_clipboard.ts

-7
Original file line numberDiff line numberDiff line change
@@ -189,13 +189,6 @@ export class CellClipboardHandler extends AbstractCellClipboardHandler<
189189
this.clearClippedZones(content);
190190
const selection = target[0];
191191
this.pasteZone(sheetId, selection.left, selection.top, content.cells, options);
192-
this.dispatch("MOVE_RANGES", {
193-
target: content.zones,
194-
sheetId: content.sheetId,
195-
targetSheetId: sheetId,
196-
col: selection.left,
197-
row: selection.top,
198-
});
199192
}
200193

201194
/**

‎src/clipboard_handlers/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ConditionalFormatClipboardHandler } from "./conditional_format_clipboar
88
import { DataValidationClipboardHandler } from "./data_validation_clipboard";
99
import { ImageClipboardHandler } from "./image_clipboard";
1010
import { MergeClipboardHandler } from "./merge_clipboard";
11+
import { ReferenceClipboardHandler } from "./references_clipboard";
1112
import { SheetClipboardHandler } from "./sheet_clipboard";
1213
import { TableClipboardHandler } from "./tables_clipboard";
1314

@@ -27,4 +28,5 @@ clipboardHandlersRegistries.cellHandlers
2728
.add("merge", MergeClipboardHandler)
2829
.add("border", BorderClipboardHandler)
2930
.add("table", TableClipboardHandler)
30-
.add("conditionalFormat", ConditionalFormatClipboardHandler);
31+
.add("conditionalFormat", ConditionalFormatClipboardHandler)
32+
.add("references", ReferenceClipboardHandler);

‎src/clipboard_handlers/merge_clipboard.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isDefined } from "../helpers";
12
import {
23
CellPosition,
34
ClipboardCellData,
@@ -11,6 +12,7 @@ import {
1112
import { AbstractCellClipboardHandler } from "./abstract_cell_clipboard_handler";
1213

1314
interface ClipboardContent {
15+
sheetId: UID;
1416
merges: Maybe<Merge>[][];
1517
}
1618

@@ -31,15 +33,16 @@ export class MergeClipboardHandler extends AbstractCellClipboardHandler<
3133
}
3234
merges.push(mergesInRow);
3335
}
34-
return { merges };
36+
return { merges, sheetId };
3537
}
3638

3739
/**
3840
* Paste the clipboard content in the given target
3941
*/
4042
paste(target: ClipboardPasteTarget, content: ClipboardContent, options: ClipboardOptions) {
4143
if (options.isCutOperation) {
42-
return;
44+
const copiedMerges = content.merges.flat().filter(isDefined);
45+
this.dispatch("REMOVE_MERGE", { sheetId: content.sheetId, target: copiedMerges });
4346
}
4447
this.pasteFromCopy(target.sheetId, target.zones, content.merges, options);
4548
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ClipboardCellData, ClipboardOptions, ClipboardPasteTarget, UID, Zone } from "../types";
2+
import { AbstractCellClipboardHandler } from "./abstract_cell_clipboard_handler";
3+
4+
interface ClipboardContent {
5+
zones: Zone[];
6+
sheetId: UID;
7+
}
8+
9+
export class ReferenceClipboardHandler extends AbstractCellClipboardHandler<ClipboardContent, {}> {
10+
copy(data: ClipboardCellData): ClipboardContent | undefined {
11+
return {
12+
zones: data.clippedZones,
13+
sheetId: data.sheetId,
14+
};
15+
}
16+
17+
paste(target: ClipboardPasteTarget, content: ClipboardContent, options: ClipboardOptions) {
18+
if (options.isCutOperation) {
19+
const selection = target.zones[0];
20+
this.dispatch("MOVE_RANGES", {
21+
target: content.zones,
22+
sheetId: content.sheetId,
23+
targetSheetId: target.sheetId,
24+
col: selection.left,
25+
row: selection.top,
26+
});
27+
}
28+
}
29+
}

‎tests/clipboard/clipboard_plugin.test.ts

+33-13
Original file line numberDiff line numberDiff line change
@@ -348,11 +348,7 @@ describe("clipboard", () => {
348348
});
349349
copy(model, "B1");
350350
paste(model, "B4");
351-
const sheetId = model.getters.getActiveSheetId();
352-
expect(model.getters.isInMerge({ sheetId, ...toCartesian("B4") })).toBe(true);
353-
expect(model.getters.isInMerge({ sheetId, ...toCartesian("B5") })).toBe(true);
354-
expect(model.getters.isInMerge({ sheetId, ...toCartesian("C4") })).toBe(true);
355-
expect(model.getters.isInMerge({ sheetId, ...toCartesian("B5") })).toBe(true);
351+
expect(model.getters.getMerges("s1")).toMatchObject([toZone("B1:C2"), toZone("B4:C5")]);
356352
});
357353

358354
test("can cut and paste merged content", () => {
@@ -361,14 +357,19 @@ describe("clipboard", () => {
361357
});
362358
cut(model, "B1:C2");
363359
paste(model, "B4");
364-
expect(model.getters.isInMerge({ sheetId: "s2", ...toCartesian("B1") })).toBe(false);
365-
expect(model.getters.isInMerge({ sheetId: "s2", ...toCartesian("B2") })).toBe(false);
366-
expect(model.getters.isInMerge({ sheetId: "s2", ...toCartesian("C1") })).toBe(false);
367-
expect(model.getters.isInMerge({ sheetId: "s2", ...toCartesian("C2") })).toBe(false);
368-
expect(model.getters.isInMerge({ sheetId: "s2", ...toCartesian("B4") })).toBe(true);
369-
expect(model.getters.isInMerge({ sheetId: "s2", ...toCartesian("B5") })).toBe(true);
370-
expect(model.getters.isInMerge({ sheetId: "s2", ...toCartesian("C4") })).toBe(true);
371-
expect(model.getters.isInMerge({ sheetId: "s2", ...toCartesian("C5") })).toBe(true);
360+
expect(model.getters.getMerges("s2")).toHaveLength(1);
361+
expect(model.getters.getMerges("s2")).toMatchObject([toZone("B4:C5")]);
362+
});
363+
364+
test("can cut and paste merged content in another sheet", () => {
365+
const model = new Model({
366+
sheets: [{ id: "s1", colNumber: 5, rowNumber: 5, merges: ["B1:C2"] }, { id: "s2" }],
367+
});
368+
cut(model, "B1:C2");
369+
activateSheet(model, "s2");
370+
paste(model, "B4");
371+
expect(model.getters.getMerges("s1")).toEqual([]);
372+
expect(model.getters.getMerges("s2")).toMatchObject([toZone("B4:C5")]);
372373
});
373374

374375
test("Pasting merge on content will remove the content", () => {
@@ -1685,6 +1686,25 @@ describe("clipboard", () => {
16851686
expect(getStyle(model, "C2")).toEqual({});
16861687
});
16871688

1689+
test("can cut and paste a conditional format in another sheet", () => {
1690+
const model = new Model();
1691+
const sheet1Id = model.getters.getActiveSheetId();
1692+
createSheet(model, { sheetId: "sheet2Id" });
1693+
const sheetId = model.getters.getActiveSheetId();
1694+
model.dispatch("ADD_CONDITIONAL_FORMAT", {
1695+
cf: createEqualCF("1", { fillColor: "#FF0000" }, "1"),
1696+
ranges: toRangesData(sheetId, "A1:A2"),
1697+
sheetId,
1698+
});
1699+
cut(model, "A1:A2");
1700+
activateSheet(model, "sheet2Id");
1701+
paste(model, "C1");
1702+
expect(model.getters.getConditionalFormats(sheet1Id)).toEqual([]);
1703+
expect(model.getters.getConditionalFormats("sheet2Id")).toMatchObject([
1704+
{ ranges: ["C1:C2"], rule: { type: "CellIsRule", style: { fillColor: "#FF0000" } } },
1705+
]);
1706+
});
1707+
16881708
test("copy cells with CF => remove origin CF => paste => it should paste with original CF", () => {
16891709
const model = new Model();
16901710
const sheetId = model.getters.getActiveSheetId();

‎tests/table/tables_plugin.test.ts

+14-16
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { CommandResult, Model } from "../../src";
22
import { toUnboundedZone, toZone, zoneToXc } from "../../src/helpers";
33
import { UID } from "../../src/types";
44
import {
5+
activateSheet,
56
addColumns,
67
addRows,
78
copy,
@@ -721,25 +722,29 @@ describe("Table plugin", () => {
721722

722723
test("Can cut and paste a whole table", () => {
723724
createTable(model, "A1:B4");
724-
updateFilter(model, "A1", ["thisIsAValue"]);
725725

726726
cut(model, "A1:B4");
727727
paste(model, "A5");
728728
expect(getTable(model, "A1")).toBeFalsy();
729729
const copiedTable = getTable(model, "A5");
730730
expect(copiedTable).toBeTruthy();
731-
expect(
732-
model.getters.getFilterHiddenValues({
733-
sheetId,
734-
col: copiedTable!.range.zone.left,
735-
row: copiedTable!.range.zone.top,
736-
})
737-
).toEqual(["thisIsAValue"]);
731+
});
732+
733+
test("Can cut and paste a whole table in another sheet", () => {
734+
const sheet1Id = model.getters.getActiveSheetId();
735+
createTable(model, "A1:B4");
736+
createSheet(model, { sheetId: "sheet2Id" });
737+
738+
cut(model, "A1:B4");
739+
activateSheet(model, "sheet2Id");
740+
paste(model, "A5");
741+
expect(model.getters.getTables(sheet1Id)).toHaveLength(0);
742+
const copiedTable = getTable(model, "A5", "sheet2Id");
743+
expect(copiedTable).toMatchObject({ range: { _zone: toZone("A5:B8") } });
738744
});
739745

740746
test("Can cut and paste multiple tables", () => {
741747
createTable(model, "A1:B4");
742-
updateFilter(model, "A1", ["thisIsAValue"]);
743748
createTable(model, "D5:D7");
744749

745750
cut(model, "A1:D7");
@@ -749,13 +754,6 @@ describe("Table plugin", () => {
749754

750755
const copiedTable = getTable(model, "A5");
751756
expect(copiedTable).toBeTruthy();
752-
expect(
753-
model.getters.getFilterHiddenValues({
754-
sheetId,
755-
col: copiedTable!.range.zone.left,
756-
row: copiedTable!.range.zone.top,
757-
})
758-
).toEqual(["thisIsAValue"]);
759757
expect(getTable(model, "D9")).toBeTruthy();
760758
});
761759

0 commit comments

Comments
 (0)
Please sign in to comment.