From 51a8ab65f3cb1a1c60392474d7b6b1fbde8a431e Mon Sep 17 00:00:00 2001
From: Rene <1595098+ReneCode@users.noreply.github.com>
Date: Thu, 9 Jul 2020 22:32:27 +0200
Subject: [PATCH] Group / ungroup should not always be present in the context
 menu (#1890)

Co-authored-by: rene_mbp <harryloveslearning@googlemail.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
---
 src/actions/actionGroup.ts                    |   4 +
 src/actions/manager.tsx                       |   8 +
 src/actions/types.ts                          |   4 +
 .../regressionTests.test.tsx.snap             | 490 ++++++++++++++++++
 src/tests/regressionTests.test.tsx            |  87 ++++
 5 files changed, 593 insertions(+)

diff --git a/src/actions/actionGroup.ts b/src/actions/actionGroup.ts
index 746c6b3d..7755a41d 100644
--- a/src/actions/actionGroup.ts
+++ b/src/actions/actionGroup.ts
@@ -90,6 +90,8 @@ export const actionGroup = register({
   },
   contextMenuOrder: 4,
   contextItemLabel: "labels.group",
+  contextItemPredicate: (elements, appState) =>
+    getSelectedElements(getNonDeletedElements(elements), appState).length > 1,
   keyTest: (event) => {
     return (
       !event.shiftKey &&
@@ -136,4 +138,6 @@ export const actionUngroup = register({
   },
   contextMenuOrder: 5,
   contextItemLabel: "labels.ungroup",
+  contextItemPredicate: (elements, appState) =>
+    getSelectedGroupIds(appState).length > 0,
 });
diff --git a/src/actions/manager.tsx b/src/actions/manager.tsx
index ece81d90..2c4350ec 100644
--- a/src/actions/manager.tsx
+++ b/src/actions/manager.tsx
@@ -82,6 +82,14 @@ export class ActionManager implements ActionsManagerInterface {
     return Object.values(this.actions)
       .filter(actionFilter)
       .filter((action) => "contextItemLabel" in action)
+      .filter((action) =>
+        action.contextItemPredicate
+          ? action.contextItemPredicate(
+              this.getElementsIncludingDeleted(),
+              this.getAppState(),
+            )
+          : true,
+      )
       .sort(
         (a, b) =>
           (a.contextMenuOrder !== undefined ? a.contextMenuOrder : 999) -
diff --git a/src/actions/types.ts b/src/actions/types.ts
index 4f160eee..f8e295bb 100644
--- a/src/actions/types.ts
+++ b/src/actions/types.ts
@@ -81,6 +81,10 @@ export interface Action {
   ) => boolean;
   contextItemLabel?: string;
   contextMenuOrder?: number;
+  contextItemPredicate?: (
+    elements: readonly ExcalidrawElement[],
+    appState: AppState,
+  ) => boolean;
 }
 
 export interface ActionsManagerInterface {
diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap
index e2f12669..f13cf401 100644
--- a/src/tests/__snapshots__/regressionTests.test.tsx.snap
+++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap
@@ -15375,6 +15375,496 @@ exports[`regression tests shift-click to multiselect, then drag: [end of test] n
 
 exports[`regression tests shift-click to multiselect, then drag: [end of test] number of renders 1`] = `17`;
 
+exports[`regression tests shows 'Group selection' in context menu for multiple selected elements: [end of test] appState 1`] = `
+Object {
+  "collaborators": Map {},
+  "currentItemBackgroundColor": "transparent",
+  "currentItemFillStyle": "hachure",
+  "currentItemFontFamily": 1,
+  "currentItemFontSize": 20,
+  "currentItemOpacity": 100,
+  "currentItemRoughness": 1,
+  "currentItemStrokeColor": "#000000",
+  "currentItemStrokeStyle": "solid",
+  "currentItemStrokeWidth": 1,
+  "currentItemTextAlign": "left",
+  "cursorButton": "up",
+  "cursorX": 0,
+  "cursorY": 0,
+  "draggingElement": null,
+  "editingElement": null,
+  "editingGroupId": null,
+  "editingLinearElement": null,
+  "elementLocked": false,
+  "elementType": "selection",
+  "errorMessage": null,
+  "exportBackground": true,
+  "gridSize": null,
+  "height": 768,
+  "isCollaborating": false,
+  "isLoading": false,
+  "isResizing": false,
+  "isRotating": false,
+  "lastPointerDownWith": "mouse",
+  "multiElement": null,
+  "name": "Untitled-201933152653",
+  "openMenu": null,
+  "previousSelectedElementIds": Object {
+    "id0": true,
+    "id2": true,
+  },
+  "resizingElement": null,
+  "scrollX": 0,
+  "scrollY": 0,
+  "scrolledOutside": false,
+  "selectedElementIds": Object {
+    "id0": true,
+    "id1": true,
+    "id2": true,
+    "id3": true,
+  },
+  "selectedGroupIds": Object {},
+  "selectionElement": null,
+  "shouldAddWatermark": false,
+  "shouldCacheIgnoreZoom": false,
+  "showShortcutsDialog": false,
+  "username": "",
+  "viewBackgroundColor": "#ffffff",
+  "width": 1024,
+  "zenModeEnabled": false,
+  "zoom": 1,
+}
+`;
+
+exports[`regression tests shows 'Group selection' in context menu for multiple selected elements: [end of test] element 0 1`] = `
+Object {
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "fillStyle": "hachure",
+  "groupIds": Array [],
+  "height": 10,
+  "id": "id0",
+  "isDeleted": false,
+  "opacity": 100,
+  "roughness": 1,
+  "seed": 337897,
+  "strokeColor": "#000000",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "rectangle",
+  "version": 2,
+  "versionNonce": 1278240551,
+  "width": 10,
+  "x": 10,
+  "y": 10,
+}
+`;
+
+exports[`regression tests shows 'Group selection' in context menu for multiple selected elements: [end of test] element 1 1`] = `
+Object {
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "fillStyle": "hachure",
+  "groupIds": Array [],
+  "height": 10,
+  "id": "id1",
+  "isDeleted": false,
+  "opacity": 100,
+  "roughness": 1,
+  "seed": 449462985,
+  "strokeColor": "#000000",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "rectangle",
+  "version": 2,
+  "versionNonce": 453191,
+  "width": 10,
+  "x": 30,
+  "y": 10,
+}
+`;
+
+exports[`regression tests shows 'Group selection' in context menu for multiple selected elements: [end of test] history 1`] = `
+Object {
+  "recording": false,
+  "redoStack": Array [],
+  "stateHistory": Array [
+    Object {
+      "appState": Object {
+        "editingGroupId": null,
+        "editingLinearElement": null,
+        "name": "Untitled-201933152653",
+        "selectedElementIds": Object {
+          "id0": true,
+        },
+        "viewBackgroundColor": "#ffffff",
+      },
+      "elements": Array [
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "fillStyle": "hachure",
+          "groupIds": Array [],
+          "height": 10,
+          "id": "id0",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 337897,
+          "strokeColor": "#000000",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 2,
+          "versionNonce": 1278240551,
+          "width": 10,
+          "x": 10,
+          "y": 10,
+        },
+      ],
+    },
+    Object {
+      "appState": Object {
+        "editingGroupId": null,
+        "editingLinearElement": null,
+        "name": "Untitled-201933152653",
+        "selectedElementIds": Object {
+          "id1": true,
+        },
+        "viewBackgroundColor": "#ffffff",
+      },
+      "elements": Array [
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "fillStyle": "hachure",
+          "groupIds": Array [],
+          "height": 10,
+          "id": "id0",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 337897,
+          "strokeColor": "#000000",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 2,
+          "versionNonce": 1278240551,
+          "width": 10,
+          "x": 10,
+          "y": 10,
+        },
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "fillStyle": "hachure",
+          "groupIds": Array [],
+          "height": 10,
+          "id": "id1",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 449462985,
+          "strokeColor": "#000000",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 2,
+          "versionNonce": 453191,
+          "width": 10,
+          "x": 30,
+          "y": 10,
+        },
+      ],
+    },
+  ],
+}
+`;
+
+exports[`regression tests shows 'Group selection' in context menu for multiple selected elements: [end of test] number of elements 1`] = `2`;
+
+exports[`regression tests shows 'Group selection' in context menu for multiple selected elements: [end of test] number of renders 1`] = `15`;
+
+exports[`regression tests shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] appState 1`] = `
+Object {
+  "collaborators": Map {},
+  "currentItemBackgroundColor": "transparent",
+  "currentItemFillStyle": "hachure",
+  "currentItemFontFamily": 1,
+  "currentItemFontSize": 20,
+  "currentItemOpacity": 100,
+  "currentItemRoughness": 1,
+  "currentItemStrokeColor": "#000000",
+  "currentItemStrokeStyle": "solid",
+  "currentItemStrokeWidth": 1,
+  "currentItemTextAlign": "left",
+  "cursorButton": "up",
+  "cursorX": 0,
+  "cursorY": 0,
+  "draggingElement": null,
+  "editingElement": null,
+  "editingGroupId": null,
+  "editingLinearElement": null,
+  "elementLocked": false,
+  "elementType": "selection",
+  "errorMessage": null,
+  "exportBackground": true,
+  "gridSize": null,
+  "height": 768,
+  "isCollaborating": false,
+  "isLoading": false,
+  "isResizing": false,
+  "isRotating": false,
+  "lastPointerDownWith": "mouse",
+  "multiElement": null,
+  "name": "Untitled-201933152653",
+  "openMenu": null,
+  "previousSelectedElementIds": Object {
+    "id0": true,
+    "id2": true,
+  },
+  "resizingElement": null,
+  "scrollX": 0,
+  "scrollY": 0,
+  "scrolledOutside": false,
+  "selectedElementIds": Object {
+    "id0": true,
+    "id1": true,
+    "id2": true,
+    "id3": true,
+  },
+  "selectedGroupIds": Object {
+    "id4": true,
+  },
+  "selectionElement": null,
+  "shouldAddWatermark": false,
+  "shouldCacheIgnoreZoom": false,
+  "showShortcutsDialog": false,
+  "username": "",
+  "viewBackgroundColor": "#ffffff",
+  "width": 1024,
+  "zenModeEnabled": false,
+  "zoom": 1,
+}
+`;
+
+exports[`regression tests shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] element 0 1`] = `
+Object {
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "fillStyle": "hachure",
+  "groupIds": Array [
+    "id4",
+  ],
+  "height": 10,
+  "id": "id0",
+  "isDeleted": false,
+  "opacity": 100,
+  "roughness": 1,
+  "seed": 337897,
+  "strokeColor": "#000000",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "rectangle",
+  "version": 3,
+  "versionNonce": 1150084233,
+  "width": 10,
+  "x": 10,
+  "y": 10,
+}
+`;
+
+exports[`regression tests shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] element 1 1`] = `
+Object {
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "fillStyle": "hachure",
+  "groupIds": Array [
+    "id4",
+  ],
+  "height": 10,
+  "id": "id1",
+  "isDeleted": false,
+  "opacity": 100,
+  "roughness": 1,
+  "seed": 449462985,
+  "strokeColor": "#000000",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "rectangle",
+  "version": 3,
+  "versionNonce": 1116226695,
+  "width": 10,
+  "x": 30,
+  "y": 10,
+}
+`;
+
+exports[`regression tests shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] history 1`] = `
+Object {
+  "recording": false,
+  "redoStack": Array [],
+  "stateHistory": Array [
+    Object {
+      "appState": Object {
+        "editingGroupId": null,
+        "editingLinearElement": null,
+        "name": "Untitled-201933152653",
+        "selectedElementIds": Object {
+          "id0": true,
+        },
+        "viewBackgroundColor": "#ffffff",
+      },
+      "elements": Array [
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "fillStyle": "hachure",
+          "groupIds": Array [],
+          "height": 10,
+          "id": "id0",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 337897,
+          "strokeColor": "#000000",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 2,
+          "versionNonce": 1278240551,
+          "width": 10,
+          "x": 10,
+          "y": 10,
+        },
+      ],
+    },
+    Object {
+      "appState": Object {
+        "editingGroupId": null,
+        "editingLinearElement": null,
+        "name": "Untitled-201933152653",
+        "selectedElementIds": Object {
+          "id1": true,
+        },
+        "viewBackgroundColor": "#ffffff",
+      },
+      "elements": Array [
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "fillStyle": "hachure",
+          "groupIds": Array [],
+          "height": 10,
+          "id": "id0",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 337897,
+          "strokeColor": "#000000",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 2,
+          "versionNonce": 1278240551,
+          "width": 10,
+          "x": 10,
+          "y": 10,
+        },
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "fillStyle": "hachure",
+          "groupIds": Array [],
+          "height": 10,
+          "id": "id1",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 449462985,
+          "strokeColor": "#000000",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 2,
+          "versionNonce": 453191,
+          "width": 10,
+          "x": 30,
+          "y": 10,
+        },
+      ],
+    },
+    Object {
+      "appState": Object {
+        "editingGroupId": null,
+        "editingLinearElement": null,
+        "name": "Untitled-201933152653",
+        "selectedElementIds": Object {
+          "id0": true,
+          "id1": true,
+          "id2": true,
+          "id3": true,
+        },
+        "viewBackgroundColor": "#ffffff",
+      },
+      "elements": Array [
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "fillStyle": "hachure",
+          "groupIds": Array [
+            "id4",
+          ],
+          "height": 10,
+          "id": "id0",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 337897,
+          "strokeColor": "#000000",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 3,
+          "versionNonce": 1150084233,
+          "width": 10,
+          "x": 10,
+          "y": 10,
+        },
+        Object {
+          "angle": 0,
+          "backgroundColor": "transparent",
+          "fillStyle": "hachure",
+          "groupIds": Array [
+            "id4",
+          ],
+          "height": 10,
+          "id": "id1",
+          "isDeleted": false,
+          "opacity": 100,
+          "roughness": 1,
+          "seed": 449462985,
+          "strokeColor": "#000000",
+          "strokeStyle": "solid",
+          "strokeWidth": 1,
+          "type": "rectangle",
+          "version": 3,
+          "versionNonce": 1116226695,
+          "width": 10,
+          "x": 30,
+          "y": 10,
+        },
+      ],
+    },
+  ],
+}
+`;
+
+exports[`regression tests shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] number of elements 1`] = `2`;
+
+exports[`regression tests shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] number of renders 1`] = `16`;
+
 exports[`regression tests shows context menu for canvas: [end of test] appState 1`] = `
 Object {
   "collaborators": Map {},
diff --git a/src/tests/regressionTests.test.tsx b/src/tests/regressionTests.test.tsx
index 9c07532d..fed5c440 100644
--- a/src/tests/regressionTests.test.tsx
+++ b/src/tests/regressionTests.test.tsx
@@ -878,6 +878,93 @@ describe("regression tests", () => {
     mouse.down(10, 10);
     mouse.up(20, 20);
     fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 });
+    const contextMenu = document.querySelector(".context-menu");
+    const options = contextMenu?.querySelectorAll(".context-menu-option");
+    const expectedOptions = [
+      "Copy styles",
+      "Paste styles",
+      "Delete",
+      "Send backward",
+      "Bring forward",
+      "Send to back",
+      "Bring to front",
+      "Duplicate",
+    ];
+
+    expect(contextMenu).not.toBeNull();
+    expect(contextMenu?.children.length).toBe(8);
+    options?.forEach((opt, i) => {
+      expect(opt.textContent).toBe(expectedOptions[i]);
+    });
+  });
+
+  it("shows 'Group selection' in context menu for multiple selected elements", () => {
+    fireEvent.change(document.querySelector(".dropdown-select__language")!, {
+      target: { value: "en" },
+    });
+
+    clickTool("rectangle");
+    mouse.down(10, 10);
+    mouse.up(10, 10);
+
+    clickTool("rectangle");
+    mouse.down(10, -10);
+    mouse.up(10, 10);
+
+    mouse.reset();
+    mouse.click(10, 10);
+    withModifierKeys({ shift: true }, () => {
+      mouse.click(20, 0);
+    });
+
+    fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 });
+
+    const contextMenu = document.querySelector(".context-menu");
+    const options = contextMenu?.querySelectorAll(".context-menu-option");
+    const expectedOptions = [
+      "Copy styles",
+      "Paste styles",
+      "Delete",
+      "Group selection",
+      "Send backward",
+      "Bring forward",
+      "Send to back",
+      "Bring to front",
+      "Duplicate",
+    ];
+
+    expect(contextMenu).not.toBeNull();
+    expect(contextMenu?.children.length).toBe(9);
+    options?.forEach((opt, i) => {
+      expect(opt.textContent).toBe(expectedOptions[i]);
+    });
+  });
+
+  it("shows 'Ungroup selection' in context menu for group inside selected elements", () => {
+    fireEvent.change(document.querySelector(".dropdown-select__language")!, {
+      target: { value: "en" },
+    });
+
+    clickTool("rectangle");
+    mouse.down(10, 10);
+    mouse.up(10, 10);
+
+    clickTool("rectangle");
+    mouse.down(10, -10);
+    mouse.up(10, 10);
+
+    mouse.reset();
+    mouse.click(10, 10);
+    withModifierKeys({ shift: true }, () => {
+      mouse.click(20, 0);
+    });
+
+    withModifierKeys({ ctrl: true }, () => {
+      keyPress("g");
+    });
+
+    fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 });
+
     const contextMenu = document.querySelector(".context-menu");
     const options = contextMenu?.querySelectorAll(".context-menu-option");
     const expectedOptions = [