Skip to content

Commit e529bce

Browse files
authored
feat(compass-data-modeling): add relationship via dragging COMPASS-9332 (#7146)
1 parent ef92780 commit e529bce

File tree

7 files changed

+209
-15
lines changed

7 files changed

+209
-15
lines changed

packages/compass-data-modeling/src/components/diagram-editor-toolbar.spec.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ function renderDiagramEditorToolbar(
1212
step="EDITING"
1313
hasUndo={true}
1414
hasRedo={true}
15+
isInRelationshipDrawingMode={false}
1516
onUndoClick={() => {}}
1617
onRedoClick={() => {}}
1718
onExportClick={() => {}}
19+
onRelationshipDrawingToggle={() => {}}
1820
{...props}
1921
/>
2022
);
@@ -63,6 +65,36 @@ describe('DiagramEditorToolbar', function () {
6365
});
6466
});
6567

68+
context('add relationship button', function () {
69+
it('renders it active if isInRelationshipDrawingMode is true', function () {
70+
renderDiagramEditorToolbar({ isInRelationshipDrawingMode: true });
71+
const addButton = screen.getByRole('button', {
72+
name: 'Exit Relationship Drawing Mode',
73+
});
74+
expect(addButton).to.have.attribute('aria-pressed', 'true');
75+
});
76+
77+
it('does not render it active if isInRelationshipDrawingMode is false', function () {
78+
renderDiagramEditorToolbar({ isInRelationshipDrawingMode: false });
79+
const addButton = screen.getByRole('button', {
80+
name: 'Add Relationship',
81+
});
82+
expect(addButton).to.have.attribute('aria-pressed', 'false');
83+
});
84+
85+
it('clicking on it calls onRelationshipDrawingToggle', function () {
86+
const relationshipDrawingToggleSpy = sinon.spy();
87+
renderDiagramEditorToolbar({
88+
onRelationshipDrawingToggle: relationshipDrawingToggleSpy,
89+
});
90+
const addRelationshipButton = screen.getByRole('button', {
91+
name: 'Add Relationship',
92+
});
93+
userEvent.click(addRelationshipButton);
94+
expect(relationshipDrawingToggleSpy).to.have.been.calledOnce;
95+
});
96+
});
97+
6698
it('renders export button and calls onExportClick', function () {
6799
const exportSpy = sinon.spy();
68100
renderDiagramEditorToolbar({ onExportClick: exportSpy });

packages/compass-data-modeling/src/components/diagram-editor-toolbar.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
spacing,
1414
useDarkMode,
1515
transparentize,
16+
Tooltip,
1617
} from '@mongodb-js/compass-components';
1718

1819
const containerStyles = css({
@@ -44,10 +45,21 @@ export const DiagramEditorToolbar: React.FunctionComponent<{
4445
step: DataModelingState['step'];
4546
hasUndo: boolean;
4647
hasRedo: boolean;
48+
isInRelationshipDrawingMode: boolean;
4749
onUndoClick: () => void;
4850
onRedoClick: () => void;
4951
onExportClick: () => void;
50-
}> = ({ step, hasUndo, onUndoClick, hasRedo, onRedoClick, onExportClick }) => {
52+
onRelationshipDrawingToggle: () => void;
53+
}> = ({
54+
step,
55+
hasUndo,
56+
onUndoClick,
57+
hasRedo,
58+
onRedoClick,
59+
onExportClick,
60+
onRelationshipDrawingToggle,
61+
isInRelationshipDrawingMode,
62+
}) => {
5163
const darkmode = useDarkMode();
5264
if (step !== 'EDITING') {
5365
return null;
@@ -58,6 +70,24 @@ export const DiagramEditorToolbar: React.FunctionComponent<{
5870
data-testid="diagram-editor-toolbar"
5971
>
6072
<div className={toolbarGroupStyles}>
73+
<Tooltip
74+
trigger={
75+
<IconButton
76+
aria-label={
77+
!isInRelationshipDrawingMode
78+
? 'Add Relationship'
79+
: 'Exit Relationship Drawing Mode'
80+
}
81+
onClick={onRelationshipDrawingToggle}
82+
active={isInRelationshipDrawingMode}
83+
aria-pressed={isInRelationshipDrawingMode}
84+
>
85+
<Icon glyph="Relationship"></Icon>
86+
</IconButton>
87+
}
88+
>
89+
Drag from one collection to another to create a relationship.
90+
</Tooltip>
6191
<IconButton aria-label="Undo" disabled={!hasUndo} onClick={onUndoClick}>
6292
<Icon glyph="Undo"></Icon>
6393
</IconButton>

packages/compass-data-modeling/src/components/diagram-editor.tsx

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
1+
import React, {
2+
useCallback,
3+
useEffect,
4+
useMemo,
5+
useRef,
6+
useState,
7+
} from 'react';
28
import { connect } from 'react-redux';
39
import type { DataModelingState } from '../store/reducer';
410
import {
@@ -8,6 +14,7 @@ import {
814
selectBackground,
915
type DiagramState,
1016
selectCurrentModelFromState,
17+
createNewRelationship,
1118
} from '../store/diagram';
1219
import {
1320
Banner,
@@ -85,26 +92,38 @@ const modelPreviewContainerStyles = css({
8592

8693
const modelPreviewStyles = css({
8794
minHeight: 0,
95+
96+
/** reactflow handles this normally, but there is a `* { userSelect: 'text' }` in this project,
97+
* which overrides inherited userSelect */
98+
['.connectablestart']: {
99+
userSelect: 'none',
100+
},
88101
});
89102

90103
type SelectedItems = NonNullable<DiagramState>['selectedItems'];
91104

92105
const DiagramContent: React.FunctionComponent<{
93106
diagramLabel: string;
94107
model: StaticModel | null;
108+
isInRelationshipDrawingMode: boolean;
95109
editErrors?: string[];
96110
onMoveCollection: (ns: string, newPosition: [number, number]) => void;
97111
onCollectionSelect: (namespace: string) => void;
98112
onRelationshipSelect: (rId: string) => void;
99113
onDiagramBackgroundClicked: () => void;
100114
selectedItems: SelectedItems;
115+
onCreateNewRelationship: (source: string, target: string) => void;
116+
onRelationshipDrawn: () => void;
101117
}> = ({
102118
diagramLabel,
103119
model,
120+
isInRelationshipDrawingMode,
104121
onMoveCollection,
105122
onCollectionSelect,
106123
onRelationshipSelect,
107124
onDiagramBackgroundClicked,
125+
onCreateNewRelationship,
126+
onRelationshipDrawn,
108127
selectedItems,
109128
}) => {
110129
const isDarkMode = useDarkMode();
@@ -138,9 +157,18 @@ const DiagramContent: React.FunctionComponent<{
138157
!!selectedItems &&
139158
selectedItems.type === 'collection' &&
140159
selectedItems.id === coll.ns;
141-
return collectionToDiagramNode(coll, selectedFields, selected);
160+
return collectionToDiagramNode(coll, {
161+
selectedFields,
162+
selected,
163+
isInRelationshipDrawingMode,
164+
});
142165
});
143-
}, [model?.collections, model?.relationships, selectedItems]);
166+
}, [
167+
model?.collections,
168+
model?.relationships,
169+
selectedItems,
170+
isInRelationshipDrawingMode,
171+
]);
144172

145173
// Fit to view on initial mount
146174
useEffect(() => {
@@ -155,6 +183,14 @@ const DiagramContent: React.FunctionComponent<{
155183
});
156184
}, []);
157185

186+
const handleNodesConnect = useCallback(
187+
(source: string, target: string) => {
188+
onCreateNewRelationship(source, target);
189+
onRelationshipDrawn();
190+
},
191+
[onRelationshipDrawn, onCreateNewRelationship]
192+
);
193+
158194
return (
159195
<div
160196
ref={setDiagramContainerRef}
@@ -191,6 +227,9 @@ const DiagramContent: React.FunctionComponent<{
191227
onNodeDragStop={(evt, node) => {
192228
onMoveCollection(node.id, [node.position.x, node.position.y]);
193229
}}
230+
onConnect={({ source, target }) => {
231+
handleNodesConnect(source, target);
232+
}}
194233
/>
195234
</div>
196235
</div>
@@ -211,6 +250,7 @@ const ConnectedDiagramContent = connect(
211250
onCollectionSelect: selectCollection,
212251
onRelationshipSelect: selectRelationship,
213252
onDiagramBackgroundClicked: selectBackground,
253+
onCreateNewRelationship: createNewRelationship,
214254
}
215255
)(DiagramContent);
216256

@@ -222,6 +262,17 @@ const DiagramEditor: React.FunctionComponent<{
222262
}> = ({ step, diagramId, onRetryClick, onCancelClick }) => {
223263
let content;
224264

265+
const [isInRelationshipDrawingMode, setIsInRelationshipDrawingMode] =
266+
useState(false);
267+
268+
const handleRelationshipDrawingToggle = useCallback(() => {
269+
setIsInRelationshipDrawingMode((prev) => !prev);
270+
}, []);
271+
272+
const onRelationshipDrawn = useCallback(() => {
273+
setIsInRelationshipDrawingMode(false);
274+
}, []);
275+
225276
if (step === 'NO_DIAGRAM_SELECTED') {
226277
return null;
227278
}
@@ -257,12 +308,23 @@ const DiagramEditor: React.FunctionComponent<{
257308

258309
if (step === 'EDITING' && diagramId) {
259310
content = (
260-
<ConnectedDiagramContent key={diagramId}></ConnectedDiagramContent>
311+
<ConnectedDiagramContent
312+
key={diagramId}
313+
isInRelationshipDrawingMode={isInRelationshipDrawingMode}
314+
onRelationshipDrawn={onRelationshipDrawn}
315+
></ConnectedDiagramContent>
261316
);
262317
}
263318

264319
return (
265-
<WorkspaceContainer toolbar={<DiagramEditorToolbar />}>
320+
<WorkspaceContainer
321+
toolbar={
322+
<DiagramEditorToolbar
323+
onRelationshipDrawingToggle={handleRelationshipDrawingToggle}
324+
isInRelationshipDrawingMode={isInRelationshipDrawingMode}
325+
/>
326+
}
327+
>
266328
{content}
267329
<ExportDiagramModal />
268330
</WorkspaceContainer>

packages/compass-data-modeling/src/store/diagram.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,8 @@ export function selectBackground(): DiagramBackgroundSelectedAction {
284284
}
285285

286286
export function createNewRelationship(
287-
namespace: string
287+
localNamespace: string,
288+
foreignNamespace: string | null = null
288289
): DataModelingThunkAction<void, RelationSelectedAction> {
289290
return (dispatch, getState, { track }) => {
290291
const relationshipId = new UUID().toString();
@@ -297,8 +298,8 @@ export function createNewRelationship(
297298
relationship: {
298299
id: relationshipId,
299300
relationship: [
300-
{ ns: namespace, cardinality: 1, fields: null },
301-
{ ns: null, cardinality: 1, fields: null },
301+
{ ns: localNamespace, cardinality: 1, fields: null },
302+
{ ns: foreignNamespace, cardinality: 1, fields: null },
302303
],
303304
isInferred: false,
304305
},

packages/compass-data-modeling/src/utils/nodes-and-edges.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,18 @@ export const getFieldsFromSchema = (
144144

145145
export function collectionToDiagramNode(
146146
coll: Pick<DataModelCollection, 'ns' | 'jsonSchema' | 'displayPosition'>,
147-
selectedFields: Record<string, string[][] | undefined> = {},
148-
selected = false
147+
options: {
148+
selectedFields?: Record<string, string[][] | undefined>;
149+
selected?: boolean;
150+
isInRelationshipDrawingMode?: boolean;
151+
} = {}
149152
): NodeProps {
153+
const {
154+
selectedFields = {},
155+
selected = false,
156+
isInRelationshipDrawingMode = false,
157+
} = options;
158+
150159
return {
151160
id: coll.ns,
152161
type: 'collection',
@@ -161,6 +170,8 @@ export function collectionToDiagramNode(
161170
0
162171
),
163172
selected,
173+
connectable: isInRelationshipDrawingMode,
174+
draggable: !isInRelationshipDrawingMode,
164175
};
165176
}
166177

packages/compass-e2e-tests/helpers/selectors.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,6 +1457,10 @@ export const DataModelApplyEditor = `${DataModelEditor} [data-testid="apply-edit
14571457
export const DataModelEditorApplyButton = `${DataModelApplyEditor} [data-testid="apply-button"]`;
14581458
export const DataModelUndoButton = 'button[aria-label="Undo"]';
14591459
export const DataModelRedoButton = 'button[aria-label="Redo"]';
1460+
export const DataModelRelationshipDrawingButton = (isActive: boolean = false) =>
1461+
`button[aria-label="${
1462+
isActive ? 'Exit Relationship Drawing Mode' : 'Add Relationship'
1463+
}"]`;
14601464
export const DataModelExportButton = 'button[aria-label="Export"]';
14611465
export const DataModelExportModal = '[data-testid="export-diagram-modal"]';
14621466
export const DataModelExportPngOption = `${DataModelExportModal} input[aria-label="PNG"]`;

0 commit comments

Comments
 (0)