Skip to content

Commit 0130936

Browse files
committed
m
1 parent d6030bf commit 0130936

File tree

9 files changed

+159
-14
lines changed

9 files changed

+159
-14
lines changed

packages/o-spreadsheet-engine/src/plugins/ui_stateful/clipboard.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,21 +95,53 @@ export class ClipboardPlugin extends UIPlugin {
9595
if (zones.length > 1 || (zones[0].top === 0 && zones[0].bottom === 0)) {
9696
return CommandResult.InvalidCopyPasteSelection;
9797
}
98-
break;
98+
const zone = this.getters.getSelectedZone();
99+
const multipleRowsInSelection = zone.top !== zone.bottom;
100+
const copyTarget = {
101+
...zone,
102+
bottom: multipleRowsInSelection ? zone.top : zone.top - 1,
103+
top: multipleRowsInSelection ? zone.top : zone.top - 1,
104+
};
105+
this.originSheetId = this.getters.getActiveSheetId();
106+
const copiedData = this.copy([copyTarget]);
107+
return this.isPasteAllowed(zones, copiedData, {
108+
isCutOperation: false,
109+
});
99110
}
100111
case "COPY_PASTE_CELLS_ON_LEFT": {
101112
const zones = this.getters.getSelectedZones();
102113
if (zones.length > 1 || (zones[0].left === 0 && zones[0].right === 0)) {
103114
return CommandResult.InvalidCopyPasteSelection;
104115
}
105-
break;
116+
const zone = this.getters.getSelectedZone();
117+
const multipleColsInSelection = zone.left !== zone.right;
118+
const copyTarget = {
119+
...zone,
120+
right: multipleColsInSelection ? zone.left : zone.left - 1,
121+
left: multipleColsInSelection ? zone.left : zone.left - 1,
122+
};
123+
this.originSheetId = this.getters.getActiveSheetId();
124+
const copiedData = this.copy([copyTarget]);
125+
return this.isPasteAllowed(zones, copiedData, {
126+
isCutOperation: false,
127+
});
106128
}
107129
case "COPY_PASTE_CELLS_ON_ZONE": {
108130
const zones = this.getters.getSelectedZones();
109131
if (zones.length > 1) {
110132
return CommandResult.InvalidCopyPasteSelection;
111133
}
112-
break;
134+
const zone = this.getters.getSelectedZone();
135+
const copyTarget = {
136+
...zone,
137+
right: zone.left,
138+
bottom: zone.top,
139+
};
140+
this.originSheetId = this.getters.getActiveSheetId();
141+
const copiedData = this.copy([copyTarget]);
142+
return this.isPasteAllowed(zones, copiedData, {
143+
isCutOperation: false,
144+
});
113145
}
114146
case "INSERT_CELL": {
115147
const { cut, paste } = this.getInsertCellsTargets(cmd.zone, cmd.shiftDimension);

src/components/grid/grid.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ import {
2828
import { canUngroupHeaders } from "../../actions/view_actions";
2929
import { isInside } from "../../helpers/index";
3030
import { interactiveCut } from "../../helpers/ui/cut_interactive";
31-
import { interactivePaste, interactivePasteFromOS } from "../../helpers/ui/paste_interactive";
31+
import {
32+
handleCopyPasteResult,
33+
interactivePaste,
34+
interactivePasteFromOS,
35+
} from "../../helpers/ui/paste_interactive";
3236
import { cellMenuRegistry } from "../../registries/menus/cell_menu_registry";
3337
import { colMenuRegistry } from "../../registries/menus/col_menu_registry";
3438
import {
@@ -360,9 +364,15 @@ export class Grid extends Component<Props, SpreadsheetChildEnv> {
360364
const position = this.env.model.getters.getActivePosition();
361365
this.env.model.selection.selectZone({ cell: position, zone: newZone });
362366
},
363-
"Ctrl+D": async () => this.env.model.dispatch("COPY_PASTE_CELLS_ABOVE"),
364-
"Ctrl+R": async () => this.env.model.dispatch("COPY_PASTE_CELLS_ON_LEFT"),
365-
"Ctrl+Enter": async () => this.env.model.dispatch("COPY_PASTE_CELLS_ON_ZONE"),
367+
"Ctrl+D": () => {
368+
handleCopyPasteResult(this.env, { type: "COPY_PASTE_CELLS_ABOVE" });
369+
},
370+
"Ctrl+R": () => {
371+
handleCopyPasteResult(this.env, { type: "COPY_PASTE_CELLS_ON_LEFT" });
372+
},
373+
"Ctrl+Enter": () => {
374+
handleCopyPasteResult(this.env, { type: "COPY_PASTE_CELLS_ON_ZONE" });
375+
},
366376
"Ctrl+H": () => this.sidePanel.open("FindAndReplace", {}),
367377
"Ctrl+F": () => this.sidePanel.open("FindAndReplace", {}),
368378
"Ctrl+Shift+E": () => this.setHorizontalAlign("center"),

src/helpers/ui/paste_interactive.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,32 @@
1-
import { RemoveDuplicateTerms } from "@odoo/o-spreadsheet-engine/components/translations_terms";
1+
import {
2+
MergeErrorMessage,
3+
RemoveDuplicateTerms,
4+
} from "@odoo/o-spreadsheet-engine/components/translations_terms";
25
import { getCurrentVersion } from "@odoo/o-spreadsheet-engine/migrations/data";
36
import { _t } from "@odoo/o-spreadsheet-engine/translation";
47
import { SpreadsheetChildEnv } from "@odoo/o-spreadsheet-engine/types/spreadsheet_env";
58
import {
69
ClipboardPasteOptions,
710
CommandResult,
11+
CopyPasteCellsAboveCommand,
12+
CopyPasteCellsOnLeftCommand,
13+
CopyPasteCellsOnZoneCommand,
814
DispatchResult,
915
ParsedOSClipboardContent,
1016
ParsedOsClipboardContentWithImageData,
1117
Zone,
1218
} from "../../types";
1319

20+
export const handleCopyPasteResult = (
21+
env: SpreadsheetChildEnv,
22+
command: CopyPasteCellsAboveCommand | CopyPasteCellsOnLeftCommand | CopyPasteCellsOnZoneCommand
23+
) => {
24+
const result = env.model.dispatch(command.type);
25+
if (result.isCancelledBecause(CommandResult.WillRemoveExistingMerge)) {
26+
env.raiseError(MergeErrorMessage);
27+
}
28+
};
29+
1430
export const PasteInteractiveContent = {
1531
wrongPasteSelection: _t("This operation is not allowed with multiple selections."),
1632
willRemoveExistingMerge: RemoveDuplicateTerms.Errors.WillRemoveExistingMerge,

tests/clipboard/clipboard_plugin.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
copy,
3535
copyPasteAboveCells,
3636
copyPasteCellsOnLeft,
37+
copyPasteCellsOnZone,
3738
createDynamicTable,
3839
createImage,
3940
createSheet,
@@ -2412,6 +2413,33 @@ describe("clipboard: pasting outside of sheet", () => {
24122413
expect(getCellContent(model, "D2")).toBe("d1");
24132414
});
24142415

2416+
test("do not fill down if filling down would unmerge cells", () => {
2417+
const model = new Model();
2418+
setCellContent(model, "A1", "a1");
2419+
merge(model, "A2:A3");
2420+
setSelection(model, ["A1:A3"]);
2421+
const result = copyPasteAboveCells(model);
2422+
expect(result).toBeCancelledBecause(CommandResult.WillRemoveExistingMerge);
2423+
});
2424+
2425+
test("do not fill right if filling right would unmerge cells", () => {
2426+
const model = new Model();
2427+
setCellContent(model, "A1", "a1");
2428+
merge(model, "B1:C1");
2429+
setSelection(model, ["A1:C1"]);
2430+
const result = copyPasteCellsOnLeft(model);
2431+
expect(result).toBeCancelledBecause(CommandResult.WillRemoveExistingMerge);
2432+
});
2433+
2434+
test("do not fill if filling would unmerge cells", () => {
2435+
const model = new Model();
2436+
setCellContent(model, "A1", "a1");
2437+
merge(model, "A2:A3");
2438+
setSelection(model, ["A1:A3"]);
2439+
const result = copyPasteCellsOnZone(model);
2440+
expect(result).toBeCancelledBecause(CommandResult.WillRemoveExistingMerge);
2441+
});
2442+
24152443
test("fill right selection with single column -> for each cell, replicates the cell on its left", async () => {
24162444
const model = new Model();
24172445
setCellContent(model, "B1", "b1");

tests/grid/grid_component.test.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ import { resetTimeoutDuration } from "../../src/components/helpers/touch_scroll_
2424
import { PaintFormatStore } from "../../src/components/paint_format_button/paint_format_store";
2525
import { CellPopoverStore } from "../../src/components/popover";
2626
import { buildSheetLink, toCartesian, toZone, zoneToXc } from "../../src/helpers";
27+
import { handleCopyPasteResult } from "../../src/helpers/ui/paste_interactive";
2728
import { Store } from "../../src/store_engine";
2829
import { ClientFocusStore } from "../../src/stores/client_focus_store";
2930
import { HighlightStore } from "../../src/stores/highlight_store";
31+
import { NotificationStore } from "../../src/stores/notification_store";
3032
import { Align, ClipboardMIMEType } from "../../src/types";
3133
import { FileStore } from "../__mocks__/mock_file_store";
3234
import { MockTransportService } from "../__mocks__/transport_service";
@@ -127,6 +129,14 @@ let composerFocusStore: Store<ComposerFocusStore>;
127129

128130
jest.useFakeTimers();
129131
mockChart();
132+
jest.mock("../../src/actions/menu_items_actions.ts", () => {
133+
const originalModule = jest.requireActual("../../src/actions/menu_items_actions.ts");
134+
return {
135+
__esModule: true,
136+
...originalModule,
137+
INSERT_TABLE: jest.fn(originalModule.INSERT_TABLE),
138+
};
139+
});
130140

131141
describe("Grid component", () => {
132142
beforeEach(async () => {
@@ -942,8 +952,13 @@ describe("Grid component", () => {
942952
expect(model.getters.getActiveSheetId()).toBe("third");
943953
});
944954

945-
// test("Pressing Shift+F11 insert a new sheet", () => {
946-
// });
955+
test("Pressing Shift+F11 insert a new sheet", () => {
956+
expect(model.getters.getSheetIds()).toHaveLength(1);
957+
keyDown({ key: "F11", shiftKey: true });
958+
const sheetIds = model.getters.getSheetIds();
959+
expect(sheetIds).toHaveLength(2);
960+
expect(model.getters.getActiveSheetId()).toBe(sheetIds[1]);
961+
});
947962

948963
test("pressing Ctrl+K opens the link editor", async () => {
949964
await keyDown({ key: "k", ctrlKey: true });
@@ -1794,7 +1809,7 @@ describe("Copy paste keyboard shortcut", () => {
17941809
const fileStore = new FileStore();
17951810
beforeEach(async () => {
17961811
clipboardData = new MockClipboardData();
1797-
({ parent, model, fixture } = await mountSpreadsheet({
1812+
({ parent, model, fixture, env } = await mountSpreadsheet({
17981813
model: new Model({}, { external: { fileStore } }),
17991814
}));
18001815
sheetId = model.getters.getActiveSheetId();
@@ -1967,6 +1982,16 @@ describe("Copy paste keyboard shortcut", () => {
19671982
expect(getCell(model, "D2")?.content).toBe("d1");
19681983
});
19691984

1985+
test("banane", () => {
1986+
setCellContent(model, "A1", "a1");
1987+
merge(model, "A2:A3");
1988+
setSelection(model, ["A1:A3"]);
1989+
handleCopyPasteResult(env, { type: "COPY_PASTE_CELLS_ON_ZONE" });
1990+
// @ts-ignore
1991+
const notificationStore = env.__spreadsheet_stores__.get(NotificationStore);
1992+
expect(notificationStore.raiseError).toHaveBeenCalled();
1993+
});
1994+
19701995
test("can copy and paste cell(s) on left using CTRL+R", async () => {
19711996
setCellContent(model, "A2", "a2");
19721997
setCellContent(model, "B2", "b2");
@@ -1983,6 +2008,26 @@ describe("Copy paste keyboard shortcut", () => {
19832008
expect(getCell(model, "B4")?.content).toBe("a4");
19842009
});
19852010

2011+
test("can copy and paste cell(s) on zone using CTRL+ENTER", async () => {
2012+
setCellContent(model, "A1", "a1");
2013+
setSelection(model, ["A1:B2"]);
2014+
keyDown({ key: "Enter", ctrlKey: true });
2015+
expect(getCell(model, "A1")?.content).toBe("a1");
2016+
expect(getCell(model, "A2")?.content).toBe("a1");
2017+
expect(getCell(model, "B1")?.content).toBe("a1");
2018+
expect(getCell(model, "B2")?.content).toBe("a1");
2019+
});
2020+
2021+
test("Alt+T -> Table", async () => {
2022+
setSelection(model, ["A1:A5"]);
2023+
await keyDown({ key: "T", altKey: true });
2024+
expect(model.getters.getTable({ sheetId, row: 0, col: 0 })).toMatchObject({
2025+
range: { zone: toZone("A1:A5") },
2026+
});
2027+
const { INSERT_TABLE } = require("../../src/actions/menu_items_actions");
2028+
expect(INSERT_TABLE as jest.Mock).toHaveBeenCalled();
2029+
});
2030+
19862031
test("Clipboard visible zones (copy) will be cleaned after hitting esc", async () => {
19872032
setCellContent(model, "A1", "things");
19882033
selectCell(model, "A1");

tests/link/link_display_component.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,15 @@ describe("link display component", () => {
9797
setCellContent(model, "A1", "HELLO");
9898
await rightClickCell(model, "A1");
9999
expect(
100-
fixture.querySelector(".o-menu .o-menu-item[data-name='insert_link']")?.textContent
100+
fixture.querySelector(".o-menu .o-menu-item[data-name='insert_link'] .o-menu-item-name")
101+
?.textContent
101102
).toBe("Insert link");
102103

103104
setCellContent(model, "A1", "[label](url.com)");
104105
await rightClickCell(model, "A1");
105106
expect(
106-
fixture.querySelector(".o-menu .o-menu-item[data-name='insert_link']")?.textContent
107+
fixture.querySelector(".o-menu .o-menu-item[data-name='insert_link'] .o-menu-item-name")
108+
?.textContent
107109
).toBe("Edit link");
108110
});
109111

tests/menus/__snapshots__/context_menu_component.test.ts.snap

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,11 @@ exports[`Context MenuPopover integration tests context menu simple rendering 1`]
435435
>
436436
Insert link
437437
</div>
438+
<div
439+
class="o-menu-item-description ms-auto text-truncate"
440+
>
441+
Ctrl+K
442+
</div>
438443
439444
440445

tests/test_helpers/commands_helpers.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,13 @@ export function copyPasteCellsOnLeft(model: Model): DispatchResult {
544544
return model.dispatch("COPY_PASTE_CELLS_ON_LEFT");
545545
}
546546

547+
/**
548+
* Copy cell and paste on zone
549+
*/
550+
export function copyPasteCellsOnZone(model: Model): DispatchResult {
551+
return model.dispatch("COPY_PASTE_CELLS_ON_ZONE");
552+
}
553+
547554
/**
548555
* Clean clipboard highlight selection.
549556
*/

tests/test_helpers/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ export function makeTestEnv(
202202
const notificationStore = container.get(NotificationStore);
203203
notificationStore.updateNotificationCallbacks({
204204
notifyUser: mockEnv.notifyUser || (() => {}),
205-
raiseError: mockEnv.raiseError || (() => {}),
205+
raiseError: mockEnv.raiseError || (jest.fn() as unknown as (message: string) => void),
206206
askConfirmation: mockEnv.askConfirmation || (() => {}),
207207
});
208208

0 commit comments

Comments
 (0)