From 7fe61c02f1900c5c8564020a85cfe7f7c17cee18 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Wed, 15 Oct 2025 12:25:54 +0200 Subject: [PATCH 1/8] feat(data-modeling): add keyboard controls for DM experience COMPASS-9779 Add support for keyboard shortcuts for deleting items, blurring items (i.e. removing focus), confirming edits in inputs with Enter, and undo/redo. --- .../src/hooks/use-throttled-props.tsx | 2 +- packages/compass-components/src/index.ts | 1 + .../src/components/diagram-editor.tsx | 214 +++++++++++++++--- .../drawer/diagram-editor-side-panel.spec.tsx | 30 +++ .../components/drawer/use-change-on-blur.tsx | 6 + .../src/store/diagram.ts | 9 +- .../tests/data-modeling-tab.test.ts | 12 +- 7 files changed, 245 insertions(+), 29 deletions(-) diff --git a/packages/compass-components/src/hooks/use-throttled-props.tsx b/packages/compass-components/src/hooks/use-throttled-props.tsx index 4dab1e44797..32b05f58ce6 100644 --- a/packages/compass-components/src/hooks/use-throttled-props.tsx +++ b/packages/compass-components/src/hooks/use-throttled-props.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'; const DEFAULT_REFRESH_RATE_MS = 250; -export const useThrottledProps = >( +export const useThrottledProps = ( props: T, refreshRate: number = DEFAULT_REFRESH_RATE_MS ): T => { diff --git a/packages/compass-components/src/index.ts b/packages/compass-components/src/index.ts index 420e9aec119..6baf8fd443a 100644 --- a/packages/compass-components/src/index.ts +++ b/packages/compass-components/src/index.ts @@ -242,6 +242,7 @@ export { export type { EdgeProps, NodeProps, + DiagramProps, DiagramInstance, NodeField, NodeGlyph, diff --git a/packages/compass-data-modeling/src/components/diagram-editor.tsx b/packages/compass-data-modeling/src/components/diagram-editor.tsx index af12bcd04ea..eaf9ce444f4 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor.tsx @@ -19,8 +19,17 @@ import { createNewRelationship, addCollection, selectField, + deleteCollection, + deleteRelationship, + removeField, + undoEdit, + redoEdit, } from '../store/diagram'; -import type { EdgeProps, NodeProps } from '@mongodb-js/compass-components'; +import type { + EdgeProps, + NodeProps, + DiagramProps, +} from '@mongodb-js/compass-components'; import { Banner, CancelLoader, @@ -120,6 +129,20 @@ const ZOOM_OPTIONS = { minZoom: 0.25, }; +function anyParentNodeIsDataModelingDiagramOrDiagramToolbar( + element: HTMLElement | null +): boolean { + while (element) { + if ( + (element as any)._isDataModelingDiagram || + (element as any)._isDiagramToolbar + ) + return true; + element = element.parentElement; + } + return false; +} + type SelectedItems = NonNullable['selectedItems']; const DiagramContent: React.FunctionComponent<{ @@ -135,7 +158,12 @@ const DiagramContent: React.FunctionComponent<{ onCollectionSelect: (namespace: string) => void; onRelationshipSelect: (rId: string) => void; onFieldSelect: (namespace: string, fieldPath: FieldPath) => void; - onDiagramBackgroundClicked: () => void; + onDiagramBackgroundClicked: (keepFieldSelection?: boolean) => void; + onDeleteCollection: (ns: string) => void; + onDeleteRelationship: (rId: string) => void; + onDeleteField: (ns: string, fieldPath: FieldPath) => void; + onUndoClick: () => void; + onRedoClick: () => void; selectedItems: SelectedItems; onCreateNewRelationship: ({ localNamespace, @@ -162,6 +190,11 @@ const DiagramContent: React.FunctionComponent<{ onDiagramBackgroundClicked, onCreateNewRelationship, onRelationshipDrawn, + onDeleteCollection, + onDeleteRelationship, + onDeleteField, + onUndoClick, + onRedoClick, selectedItems, DiagramComponent = Diagram, }) => { @@ -177,6 +210,7 @@ const DiagramContent: React.FunctionComponent<{ if (ref) { // For debugging purposes, we attach the diagram to the ref. (ref as any)._diagram = diagram.current; + (ref as any)._isDataModelingDiagram = true; } }, []); @@ -272,7 +306,7 @@ const DiagramContent: React.FunctionComponent<{ ); const onNodeClick = useCallback( - (_evt: React.MouseEvent, node: NodeProps) => { + (_evt: React.MouseEvent | null, node: NodeProps) => { if (node.type !== 'collection') { return; } @@ -283,7 +317,7 @@ const DiagramContent: React.FunctionComponent<{ ); const onEdgeClick = useCallback( - (_evt: React.MouseEvent, edge: EdgeProps) => { + (_evt: React.MouseEvent | null, edge: EdgeProps) => { onRelationshipSelect(edge.id); openDrawer(DATA_MODELING_DRAWER_ID); }, @@ -333,21 +367,119 @@ const DiagramContent: React.FunctionComponent<{ [onAddFieldToObjectField] ); - const diagramProps = useMemo( - () => ({ - isDarkMode, - title: diagramLabel, - edges, - nodes, - onAddFieldToNodeClick: onClickAddFieldToCollection, - onAddFieldToObjectFieldClick: onClickAddFieldToObjectField, - onNodeClick, - onPaneClick, - onEdgeClick, - onFieldClick, - onNodeDragStop, - onConnect, - }), + const onSelectionChange = useCallback( + ({ nodes, edges }: { nodes: NodeProps[]; edges: EdgeProps[] }) => { + // Select the first selected item (if any) in the diagram + // since unlike react-flow, we only support single item selections + if (nodes.length > 0) { + onNodeClick(null, nodes[0]); + return; + } + if (edges.length > 0) { + onEdgeClick(null, edges[0]); + return; + } + // If nothing is selected, clear the selection + onDiagramBackgroundClicked(true); + }, + [onDiagramBackgroundClicked, onNodeClick, onEdgeClick] + ); + + const onKeyDown = useCallback( + (event: React.KeyboardEvent | KeyboardEvent) => { + // If no element is focused, some inputs should still be treated as targeting the diagram + // (e.g. undo/redo) + const isUnfocusedEvent = + event.target === document.body || + event.target === document.documentElement; + if ( + !isUnfocusedEvent && + !anyParentNodeIsDataModelingDiagramOrDiagramToolbar( + event.target as HTMLElement + ) + ) { + return; + } + const markHandled = () => { + event.preventDefault(); + event.stopPropagation(); + }; + + // Backspace and Delete keys delete selected item + if ( + !isUnfocusedEvent && + (event.key === 'Backspace' || event.key === 'Delete') + ) { + markHandled(); + switch (selectedItems?.type) { + case 'collection': + onDeleteCollection(selectedItems.id); + break; + case 'relationship': + onDeleteRelationship(selectedItems.id); + break; + case 'field': + onDeleteField(selectedItems.namespace, selectedItems.fieldPath); + break; + default: + break; + } + } + + // Escape key clears selection + else if (!isUnfocusedEvent && event.key === 'Escape') { + markHandled(); + onDiagramBackgroundClicked(true); + // Undo/Redo should also trigger if there is no currently focused element + // macOS: Cmd+Shift+Z = Redo, Cmd+Z = Undo + } else if (event.metaKey && event.key.toLowerCase() === 'z') { + markHandled(); + if (event.shiftKey) { + onRedoClick(); + } else { + onUndoClick(); + } + // Windows/Linux: Ctrl+Z = Undo, Ctrl+Y = Redo + } else if (event.ctrlKey && event.key.toLowerCase() === 'z') { + markHandled(); + onUndoClick(); + } else if (event.ctrlKey && event.key.toLowerCase() === 'y') { + markHandled(); + onRedoClick(); + } + }, + [ + onDiagramBackgroundClicked, + onDeleteCollection, + onDeleteRelationship, + onDeleteField, + selectedItems, + ] + ); + + // Not sure why eslint is confused by `DiagramProps` here + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents + type ThrottledDiagramProps = DiagramProps & { + onKeyDown: (event: React.KeyboardEvent | KeyboardEvent) => void; + }; + const diagramProps: ThrottledDiagramProps = useMemo( + () => + ({ + isDarkMode, + title: diagramLabel, + edges, + nodes, + onAddFieldToNodeClick: onClickAddFieldToCollection, + onAddFieldToObjectFieldClick: onClickAddFieldToObjectField, + onNodeClick, + onPaneClick, + onEdgeClick, + onFieldClick, + onNodeDragStop, + onConnect, + onSelectionChange, + onKeyDown, + } satisfies ThrottledDiagramProps), [ isDarkMode, diagramLabel, @@ -361,10 +493,18 @@ const DiagramContent: React.FunctionComponent<{ onFieldClick, onNodeDragStop, onConnect, + onSelectionChange, + onKeyDown, ] ); - const throttledDiagramProps = useThrottledProps(diagramProps); + const { onKeyDown: diagramOnKeyDown, ...throttledDiagramProps } = + useThrottledProps(diagramProps); + + useEffect(() => { + document.body.addEventListener('keydown', diagramOnKeyDown); + return () => document.body.removeEventListener('keydown', diagramOnKeyDown); + }, [diagramOnKeyDown]); return (
-
+ {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} +
{showDataInfoBanner && ( { + if (ref) { + // For debugging purposes, we attach the diagram to the ref. + (ref as any)._isDiagramToolbar = true; + } + }, + [] + ); + if (step === 'NO_DIAGRAM_SELECTED') { return null; } @@ -507,11 +667,13 @@ const DiagramEditor: React.FunctionComponent<{ return ( +
+ +
} > {content} diff --git a/packages/compass-data-modeling/src/components/drawer/diagram-editor-side-panel.spec.tsx b/packages/compass-data-modeling/src/components/drawer/diagram-editor-side-panel.spec.tsx index 13d6fa16228..5265f3be137 100644 --- a/packages/compass-data-modeling/src/components/drawer/diagram-editor-side-panel.spec.tsx +++ b/packages/compass-data-modeling/src/components/drawer/diagram-editor-side-panel.spec.tsx @@ -45,6 +45,13 @@ const updateInputWithBlur = (label: string, text: string) => { userEvent.click(document.body); }; +const updateInputWithEnter = (label: string, text: string) => { + const input = screen.getByLabelText(label); + userEvent.clear(input); + if (text.length) userEvent.type(input, text); + userEvent.type(input, '{enter}'); +}; + async function comboboxSelectItem( label: string, value: string, @@ -785,5 +792,28 @@ describe('DiagramEditorSidePanel', function () { expect(notModifiedCollection).to.exist; }); + + it('should handle collection name and notes editing using enter', async function () { + const result = renderDrawer(); + result.plugin.store.dispatch(addCollection()); + + await waitForDrawerToOpen(); + + updateInputWithEnter('Name', 'pineapple'); + + userEvent.click(screen.getByRole('textbox', { name: 'Notes' })); + userEvent.type( + screen.getByRole('textbox', { name: 'Notes' }), + 'Note about the relationship{shift>}{enter}{/shict}next line' + ); + + const collection = selectCurrentModelFromState( + result.plugin.store.getState() + ).collections.find((n) => n.ns === 'flights.pineapple'); + expect(collection).to.exist; + expect(screen.getByRole('textbox', { name: 'Notes' })).to.have.value( + 'Note about the relationship\nnext line' + ); + }); }); }); diff --git a/packages/compass-data-modeling/src/components/drawer/use-change-on-blur.tsx b/packages/compass-data-modeling/src/components/drawer/use-change-on-blur.tsx index 46d8e0aff19..d3278f2a802 100644 --- a/packages/compass-data-modeling/src/components/drawer/use-change-on-blur.tsx +++ b/packages/compass-data-modeling/src/components/drawer/use-change-on-blur.tsx @@ -7,6 +7,7 @@ export function useChangeOnBlur( value: string; onChange: React.ChangeEventHandler; onBlur: React.FocusEventHandler; + onKeyDown: React.KeyboardEventHandler; } { const [_value, setValue] = useState(value); useLayoutEffect(() => { @@ -22,5 +23,10 @@ export function useChangeOnBlur( onBlur: () => { onChange(_value); }, + onKeyDown: (evt) => { + if (evt.key === 'Enter' && !evt.shiftKey) { + (evt.target as HTMLInputElement | HTMLTextAreaElement).blur?.(); + } + }, }; } diff --git a/packages/compass-data-modeling/src/store/diagram.ts b/packages/compass-data-modeling/src/store/diagram.ts index cc8e4c85ab2..b75d49a4ecc 100644 --- a/packages/compass-data-modeling/src/store/diagram.ts +++ b/packages/compass-data-modeling/src/store/diagram.ts @@ -131,6 +131,7 @@ export type FieldSelectedAction = { export type DiagramBackgroundSelectedAction = { type: DiagramActionTypes.DIAGRAM_BACKGROUND_SELECTED; + keepFieldSelection: boolean; }; export type DiagramActions = @@ -312,6 +313,9 @@ export const diagramReducer: Reducer = ( }; } if (isAction(action, DiagramActionTypes.DIAGRAM_BACKGROUND_SELECTED)) { + if (action.keepFieldSelection && state.selectedItems?.type === 'field') { + return state; + } return { ...state, selectedItems: null, @@ -437,9 +441,12 @@ export function selectField( }; } -export function selectBackground(): DiagramBackgroundSelectedAction { +export function selectBackground( + keepFieldSelection = false +): DiagramBackgroundSelectedAction { return { type: DiagramActionTypes.DIAGRAM_BACKGROUND_SELECTED, + keepFieldSelection, }; } diff --git a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts index bff9dd760f6..346fbbdf077 100644 --- a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts +++ b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts @@ -22,7 +22,7 @@ import toNS from 'mongodb-ns'; import path from 'path'; import os from 'os'; import fs from 'fs/promises'; -import type { ChainablePromiseElement } from 'webdriverio'; +import { Key, type ChainablePromiseElement } from 'webdriverio'; type Node = { id: string; @@ -838,6 +838,16 @@ describe('Data Modeling tab', function () { // This is to ensure that the initial edit of the collection name wasn't a separate edit await browser.clickVisible(Selectors.DataModelUndoButton); await getDiagramNodes(browser, 2); + + // Repeatedly Redo + Undo through keyboard shortcuts + await browser.keys([Key.Control, 'y']); + await getDiagramNodes(browser, 3); + await browser.keys([Key.Control, 'z']); + await getDiagramNodes(browser, 2); + await browser.keys([Key.Command, Key.Shift, 'z']); + await getDiagramNodes(browser, 3); + await browser.keys([Key.Command, 'z']); + await getDiagramNodes(browser, 2); }); }); }); From d8853743c86c8eb439645e7da5d8c0110b0595e0 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sun, 19 Oct 2025 00:36:01 +0200 Subject: [PATCH 2/8] fixup: copilot cr --- .../compass-data-modeling/src/components/diagram-editor.tsx | 2 ++ .../src/components/drawer/diagram-editor-side-panel.spec.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/compass-data-modeling/src/components/diagram-editor.tsx b/packages/compass-data-modeling/src/components/diagram-editor.tsx index eaf9ce444f4..893d6e2c300 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor.tsx @@ -454,6 +454,8 @@ const DiagramContent: React.FunctionComponent<{ onDeleteRelationship, onDeleteField, selectedItems, + onUndoClick, + onRedoClick, ] ); diff --git a/packages/compass-data-modeling/src/components/drawer/diagram-editor-side-panel.spec.tsx b/packages/compass-data-modeling/src/components/drawer/diagram-editor-side-panel.spec.tsx index 5265f3be137..17ec6c08ea4 100644 --- a/packages/compass-data-modeling/src/components/drawer/diagram-editor-side-panel.spec.tsx +++ b/packages/compass-data-modeling/src/components/drawer/diagram-editor-side-panel.spec.tsx @@ -804,7 +804,7 @@ describe('DiagramEditorSidePanel', function () { userEvent.click(screen.getByRole('textbox', { name: 'Notes' })); userEvent.type( screen.getByRole('textbox', { name: 'Notes' }), - 'Note about the relationship{shift>}{enter}{/shict}next line' + 'Note about the relationship{shift>}{enter}{/shift}next line' ); const collection = selectCurrentModelFromState( From b44e0b5a44d7f3d1e014a2d29207c2f53e543eb7 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Mon, 20 Oct 2025 13:42:17 +0200 Subject: [PATCH 3/8] fixup: use useHotkeys --- .../src/components/diagram-editor.tsx | 163 ++++-------------- 1 file changed, 36 insertions(+), 127 deletions(-) diff --git a/packages/compass-data-modeling/src/components/diagram-editor.tsx b/packages/compass-data-modeling/src/components/diagram-editor.tsx index 893d6e2c300..39c355dc97f 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor.tsx @@ -44,6 +44,7 @@ import { rafraf, Diagram, useDiagram, + useHotkeys, } from '@mongodb-js/compass-components'; import { cancelAnalysis, retryAnalysis } from '../store/analysis-process'; import type { FieldPath, StaticModel } from '../services/data-model-storage'; @@ -129,20 +130,6 @@ const ZOOM_OPTIONS = { minZoom: 0.25, }; -function anyParentNodeIsDataModelingDiagramOrDiagramToolbar( - element: HTMLElement | null -): boolean { - while (element) { - if ( - (element as any)._isDataModelingDiagram || - (element as any)._isDiagramToolbar - ) - return true; - element = element.parentElement; - } - return false; -} - type SelectedItems = NonNullable['selectedItems']; const DiagramContent: React.FunctionComponent<{ @@ -210,7 +197,6 @@ const DiagramContent: React.FunctionComponent<{ if (ref) { // For debugging purposes, we attach the diagram to the ref. (ref as any)._diagram = diagram.current; - (ref as any)._isDataModelingDiagram = true; } }, []); @@ -385,86 +371,33 @@ const DiagramContent: React.FunctionComponent<{ [onDiagramBackgroundClicked, onNodeClick, onEdgeClick] ); - const onKeyDown = useCallback( - (event: React.KeyboardEvent | KeyboardEvent) => { - // If no element is focused, some inputs should still be treated as targeting the diagram - // (e.g. undo/redo) - const isUnfocusedEvent = - event.target === document.body || - event.target === document.documentElement; - if ( - !isUnfocusedEvent && - !anyParentNodeIsDataModelingDiagramOrDiagramToolbar( - event.target as HTMLElement - ) - ) { - return; - } - const markHandled = () => { - event.preventDefault(); - event.stopPropagation(); - }; - - // Backspace and Delete keys delete selected item - if ( - !isUnfocusedEvent && - (event.key === 'Backspace' || event.key === 'Delete') - ) { - markHandled(); - switch (selectedItems?.type) { - case 'collection': - onDeleteCollection(selectedItems.id); - break; - case 'relationship': - onDeleteRelationship(selectedItems.id); - break; - case 'field': - onDeleteField(selectedItems.namespace, selectedItems.fieldPath); - break; - default: - break; - } - } - - // Escape key clears selection - else if (!isUnfocusedEvent && event.key === 'Escape') { - markHandled(); - onDiagramBackgroundClicked(true); - // Undo/Redo should also trigger if there is no currently focused element - // macOS: Cmd+Shift+Z = Redo, Cmd+Z = Undo - } else if (event.metaKey && event.key.toLowerCase() === 'z') { - markHandled(); - if (event.shiftKey) { - onRedoClick(); - } else { - onUndoClick(); - } - // Windows/Linux: Ctrl+Z = Undo, Ctrl+Y = Redo - } else if (event.ctrlKey && event.key.toLowerCase() === 'z') { - markHandled(); - onUndoClick(); - } else if (event.ctrlKey && event.key.toLowerCase() === 'y') { - markHandled(); - onRedoClick(); - } - }, - [ - onDiagramBackgroundClicked, - onDeleteCollection, - onDeleteRelationship, - onDeleteField, - selectedItems, - onUndoClick, - onRedoClick, - ] - ); - - // Not sure why eslint is confused by `DiagramProps` here - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents - type ThrottledDiagramProps = DiagramProps & { - onKeyDown: (event: React.KeyboardEvent | KeyboardEvent) => void; - }; - const diagramProps: ThrottledDiagramProps = useMemo( + const deleteItem = useCallback(() => { + switch (selectedItems?.type) { + case 'collection': + onDeleteCollection(selectedItems.id); + break; + case 'relationship': + onDeleteRelationship(selectedItems.id); + break; + case 'field': + onDeleteField(selectedItems.namespace, selectedItems.fieldPath); + break; + default: + break; + } + }, [selectedItems, onDeleteCollection, onDeleteRelationship, onDeleteField]); + useHotkeys('Backspace', deleteItem, [deleteItem]); + useHotkeys('Delete', deleteItem, [deleteItem]); + useHotkeys('Escape', () => { + onDiagramBackgroundClicked(true); + }); + // macOS: Cmd+Shift+Z = Redo, Cmd+Z = Undo + // Windows/Linux: Ctrl+Z = Undo, Ctrl+Y = Redo + useHotkeys('mod+z', onUndoClick, [onUndoClick]); + useHotkeys('mod+shift+z', onRedoClick, [onRedoClick]); + useHotkeys('mod+y', onRedoClick, [onRedoClick]); + + const diagramProps: DiagramProps = useMemo( () => ({ isDarkMode, @@ -480,8 +413,7 @@ const DiagramContent: React.FunctionComponent<{ onNodeDragStop, onConnect, onSelectionChange, - onKeyDown, - } satisfies ThrottledDiagramProps), + } satisfies DiagramProps), [ isDarkMode, diagramLabel, @@ -496,17 +428,10 @@ const DiagramContent: React.FunctionComponent<{ onNodeDragStop, onConnect, onSelectionChange, - onKeyDown, ] ); - const { onKeyDown: diagramOnKeyDown, ...throttledDiagramProps } = - useThrottledProps(diagramProps); - - useEffect(() => { - document.body.addEventListener('keydown', diagramOnKeyDown); - return () => document.body.removeEventListener('keydown', diagramOnKeyDown); - }, [diagramOnKeyDown]); + const { ...throttledDiagramProps } = useThrottledProps(diagramProps); return (
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} -
+
{showDataInfoBanner && ( { - if (ref) { - // For debugging purposes, we attach the diagram to the ref. - (ref as any)._isDiagramToolbar = true; - } - }, - [] - ); - if (step === 'NO_DIAGRAM_SELECTED') { return null; } @@ -669,13 +580,11 @@ const DiagramEditor: React.FunctionComponent<{ return ( - -
+ } > {content} From 41e2ff9281f36b6d5d6c1f3ae93a532e75496d87 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Mon, 20 Oct 2025 13:47:02 +0200 Subject: [PATCH 4/8] fixup: move undo/redo hotkeys to toolbar --- .../src/components/diagram-editor-toolbar.tsx | 13 +++++++++++++ .../src/components/diagram-editor.tsx | 13 ------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/compass-data-modeling/src/components/diagram-editor-toolbar.tsx b/packages/compass-data-modeling/src/components/diagram-editor-toolbar.tsx index 4f3b14ba1a6..4d81b8789f0 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor-toolbar.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor-toolbar.tsx @@ -16,6 +16,7 @@ import { Tooltip, Breadcrumbs, type BreadcrumbItem, + useHotkeys, } from '@mongodb-js/compass-components'; import AddCollection from './icons/add-collection'; import { useOpenWorkspace } from '@mongodb-js/compass-workspaces/provider'; @@ -86,6 +87,18 @@ export const DiagramEditorToolbar: React.FunctionComponent<{ [diagramName, openDataModelingWorkspace] ); + // macOS: Cmd+Shift+Z = Redo, Cmd+Z = Undo + // Windows/Linux: Ctrl+Z = Undo, Ctrl+Y = Redo + useHotkeys('mod+z', onUndoClick, { enabled: step === 'EDITING' }, [ + onUndoClick, + ]); + useHotkeys('mod+shift+z', onRedoClick, { enabled: step === 'EDITING' }, [ + onRedoClick, + ]); + useHotkeys('mod+y', onRedoClick, { enabled: step === 'EDITING' }, [ + onRedoClick, + ]); + if (step !== 'EDITING') { return null; } diff --git a/packages/compass-data-modeling/src/components/diagram-editor.tsx b/packages/compass-data-modeling/src/components/diagram-editor.tsx index 39c355dc97f..56caf9b1fd1 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor.tsx @@ -22,8 +22,6 @@ import { deleteCollection, deleteRelationship, removeField, - undoEdit, - redoEdit, } from '../store/diagram'; import type { EdgeProps, @@ -149,8 +147,6 @@ const DiagramContent: React.FunctionComponent<{ onDeleteCollection: (ns: string) => void; onDeleteRelationship: (rId: string) => void; onDeleteField: (ns: string, fieldPath: FieldPath) => void; - onUndoClick: () => void; - onRedoClick: () => void; selectedItems: SelectedItems; onCreateNewRelationship: ({ localNamespace, @@ -180,8 +176,6 @@ const DiagramContent: React.FunctionComponent<{ onDeleteCollection, onDeleteRelationship, onDeleteField, - onUndoClick, - onRedoClick, selectedItems, DiagramComponent = Diagram, }) => { @@ -391,11 +385,6 @@ const DiagramContent: React.FunctionComponent<{ useHotkeys('Escape', () => { onDiagramBackgroundClicked(true); }); - // macOS: Cmd+Shift+Z = Redo, Cmd+Z = Undo - // Windows/Linux: Ctrl+Z = Undo, Ctrl+Y = Redo - useHotkeys('mod+z', onUndoClick, [onUndoClick]); - useHotkeys('mod+shift+z', onRedoClick, [onRedoClick]); - useHotkeys('mod+y', onRedoClick, [onRedoClick]); const diagramProps: DiagramProps = useMemo( () => @@ -494,8 +483,6 @@ const ConnectedDiagramContent = connect( onDeleteCollection: deleteCollection, onDeleteRelationship: deleteRelationship, onDeleteField: removeField, - onUndoClick: undoEdit, - onRedoClick: redoEdit, } )(DiagramContent); From c738187002bb79b81eed5ee9dc0af65cdd7573f2 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Mon, 20 Oct 2025 15:10:28 +0200 Subject: [PATCH 5/8] fixup: cr --- .../compass-data-modeling/src/components/diagram-editor.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/compass-data-modeling/src/components/diagram-editor.tsx b/packages/compass-data-modeling/src/components/diagram-editor.tsx index 56caf9b1fd1..0b241b9e1bc 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor.tsx @@ -420,7 +420,7 @@ const DiagramContent: React.FunctionComponent<{ ] ); - const { ...throttledDiagramProps } = useThrottledProps(diagramProps); + const throttledDiagramProps = useThrottledProps(diagramProps); return (
- {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
{showDataInfoBanner && ( Date: Mon, 20 Oct 2025 16:10:02 +0200 Subject: [PATCH 6/8] fixup: remove onSelectionChange, keepFieldSelection --- .../src/components/diagram-editor.tsx | 24 ++----------------- .../src/store/diagram.ts | 9 +------ 2 files changed, 3 insertions(+), 30 deletions(-) diff --git a/packages/compass-data-modeling/src/components/diagram-editor.tsx b/packages/compass-data-modeling/src/components/diagram-editor.tsx index 0b241b9e1bc..23cc71ea431 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor.tsx @@ -143,7 +143,7 @@ const DiagramContent: React.FunctionComponent<{ onCollectionSelect: (namespace: string) => void; onRelationshipSelect: (rId: string) => void; onFieldSelect: (namespace: string, fieldPath: FieldPath) => void; - onDiagramBackgroundClicked: (keepFieldSelection?: boolean) => void; + onDiagramBackgroundClicked: () => void; onDeleteCollection: (ns: string) => void; onDeleteRelationship: (rId: string) => void; onDeleteField: (ns: string, fieldPath: FieldPath) => void; @@ -347,24 +347,6 @@ const DiagramContent: React.FunctionComponent<{ [onAddFieldToObjectField] ); - const onSelectionChange = useCallback( - ({ nodes, edges }: { nodes: NodeProps[]; edges: EdgeProps[] }) => { - // Select the first selected item (if any) in the diagram - // since unlike react-flow, we only support single item selections - if (nodes.length > 0) { - onNodeClick(null, nodes[0]); - return; - } - if (edges.length > 0) { - onEdgeClick(null, edges[0]); - return; - } - // If nothing is selected, clear the selection - onDiagramBackgroundClicked(true); - }, - [onDiagramBackgroundClicked, onNodeClick, onEdgeClick] - ); - const deleteItem = useCallback(() => { switch (selectedItems?.type) { case 'collection': @@ -383,7 +365,7 @@ const DiagramContent: React.FunctionComponent<{ useHotkeys('Backspace', deleteItem, [deleteItem]); useHotkeys('Delete', deleteItem, [deleteItem]); useHotkeys('Escape', () => { - onDiagramBackgroundClicked(true); + onDiagramBackgroundClicked(); }); const diagramProps: DiagramProps = useMemo( @@ -401,7 +383,6 @@ const DiagramContent: React.FunctionComponent<{ onFieldClick, onNodeDragStop, onConnect, - onSelectionChange, } satisfies DiagramProps), [ isDarkMode, @@ -416,7 +397,6 @@ const DiagramContent: React.FunctionComponent<{ onFieldClick, onNodeDragStop, onConnect, - onSelectionChange, ] ); diff --git a/packages/compass-data-modeling/src/store/diagram.ts b/packages/compass-data-modeling/src/store/diagram.ts index b75d49a4ecc..cc8e4c85ab2 100644 --- a/packages/compass-data-modeling/src/store/diagram.ts +++ b/packages/compass-data-modeling/src/store/diagram.ts @@ -131,7 +131,6 @@ export type FieldSelectedAction = { export type DiagramBackgroundSelectedAction = { type: DiagramActionTypes.DIAGRAM_BACKGROUND_SELECTED; - keepFieldSelection: boolean; }; export type DiagramActions = @@ -313,9 +312,6 @@ export const diagramReducer: Reducer = ( }; } if (isAction(action, DiagramActionTypes.DIAGRAM_BACKGROUND_SELECTED)) { - if (action.keepFieldSelection && state.selectedItems?.type === 'field') { - return state; - } return { ...state, selectedItems: null, @@ -441,12 +437,9 @@ export function selectField( }; } -export function selectBackground( - keepFieldSelection = false -): DiagramBackgroundSelectedAction { +export function selectBackground(): DiagramBackgroundSelectedAction { return { type: DiagramActionTypes.DIAGRAM_BACKGROUND_SELECTED, - keepFieldSelection, }; } From 72d2d9dc76af617614ccf1f352c548fbb2131861 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Mon, 20 Oct 2025 18:32:12 +0200 Subject: [PATCH 7/8] fixup: cr --- .../src/components/diagram-editor.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/compass-data-modeling/src/components/diagram-editor.tsx b/packages/compass-data-modeling/src/components/diagram-editor.tsx index 23cc71ea431..0d187f8905e 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor.tsx @@ -364,9 +364,13 @@ const DiagramContent: React.FunctionComponent<{ }, [selectedItems, onDeleteCollection, onDeleteRelationship, onDeleteField]); useHotkeys('Backspace', deleteItem, [deleteItem]); useHotkeys('Delete', deleteItem, [deleteItem]); - useHotkeys('Escape', () => { - onDiagramBackgroundClicked(); - }); + useHotkeys( + 'Escape', + () => { + onDiagramBackgroundClicked(); + }, + [onDiagramBackgroundClicked] + ); const diagramProps: DiagramProps = useMemo( () => From ce5971c1f267e27132771a237780bf83abe23d6f Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Mon, 20 Oct 2025 18:33:47 +0200 Subject: [PATCH 8/8] fixup: add TODO re: application menu integration --- .../src/components/diagram-editor-toolbar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/compass-data-modeling/src/components/diagram-editor-toolbar.tsx b/packages/compass-data-modeling/src/components/diagram-editor-toolbar.tsx index 4d81b8789f0..e4182196cb4 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor-toolbar.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor-toolbar.tsx @@ -87,6 +87,7 @@ export const DiagramEditorToolbar: React.FunctionComponent<{ [diagramName, openDataModelingWorkspace] ); + // TODO(COMPASS-9976): Integrate with application menu // macOS: Cmd+Shift+Z = Redo, Cmd+Z = Undo // Windows/Linux: Ctrl+Z = Undo, Ctrl+Y = Redo useHotkeys('mod+z', onUndoClick, { enabled: step === 'EDITING' }, [