Skip to content

Commit 687d616

Browse files
committed
feat(compass-data-modeling): editing relationship via dragging COMPASS-9332
1 parent e7e965d commit 687d616

File tree

6 files changed

+143
-17
lines changed

6 files changed

+143
-17
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: 'Add Relationship',
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: 27 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,20 @@ 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="Add Relationship"
77+
onClick={onRelationshipDrawingToggle}
78+
active={isInRelationshipDrawingMode}
79+
aria-pressed={isInRelationshipDrawingMode}
80+
>
81+
<Icon glyph="Relationship"></Icon>
82+
</IconButton>
83+
}
84+
>
85+
Drag from one collection to another to create a relationship.
86+
</Tooltip>
6187
<IconButton aria-label="Undo" disabled={!hasUndo} onClick={onUndoClick}>
6288
<Icon glyph="Undo"></Icon>
6389
</IconButton>

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

Lines changed: 62 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,
@@ -92,19 +99,25 @@ type SelectedItems = NonNullable<DiagramState>['selectedItems'];
9299
const DiagramContent: React.FunctionComponent<{
93100
diagramLabel: string;
94101
model: StaticModel | null;
102+
isInRelationshipDrawingMode: boolean;
95103
editErrors?: string[];
96104
onMoveCollection: (ns: string, newPosition: [number, number]) => void;
97105
onCollectionSelect: (namespace: string) => void;
98106
onRelationshipSelect: (rId: string) => void;
99107
onDiagramBackgroundClicked: () => void;
100108
selectedItems: SelectedItems;
109+
onCreateNewRelationship: (source: string, target: string) => void;
110+
onRelationshipDrawn: () => void;
101111
}> = ({
102112
diagramLabel,
103113
model,
114+
isInRelationshipDrawingMode,
104115
onMoveCollection,
105116
onCollectionSelect,
106117
onRelationshipSelect,
107118
onDiagramBackgroundClicked,
119+
onCreateNewRelationship,
120+
onRelationshipDrawn,
108121
selectedItems,
109122
}) => {
110123
const isDarkMode = useDarkMode();
@@ -138,9 +151,19 @@ const DiagramContent: React.FunctionComponent<{
138151
!!selectedItems &&
139152
selectedItems.type === 'collection' &&
140153
selectedItems.id === coll.ns;
141-
return collectionToDiagramNode(coll, selectedFields, selected);
154+
return collectionToDiagramNode({
155+
coll,
156+
selectedFields,
157+
selected,
158+
isInRelationshipDrawingMode,
159+
});
142160
});
143-
}, [model?.collections, model?.relationships, selectedItems]);
161+
}, [
162+
model?.collections,
163+
model?.relationships,
164+
selectedItems,
165+
isInRelationshipDrawingMode,
166+
]);
144167

145168
// Fit to view on initial mount
146169
useEffect(() => {
@@ -155,6 +178,14 @@ const DiagramContent: React.FunctionComponent<{
155178
});
156179
}, []);
157180

181+
const handleNodesConnect = useCallback(
182+
(source: string, target: string) => {
183+
onCreateNewRelationship(source, target);
184+
onRelationshipDrawn();
185+
},
186+
[onRelationshipDrawn, onCreateNewRelationship]
187+
);
188+
158189
return (
159190
<div
160191
ref={setDiagramContainerRef}
@@ -191,6 +222,9 @@ const DiagramContent: React.FunctionComponent<{
191222
onNodeDragStop={(evt, node) => {
192223
onMoveCollection(node.id, [node.position.x, node.position.y]);
193224
}}
225+
onConnect={({ source, target }) => {
226+
handleNodesConnect(source, target);
227+
}}
194228
/>
195229
</div>
196230
</div>
@@ -211,6 +245,7 @@ const ConnectedDiagramContent = connect(
211245
onCollectionSelect: selectCollection,
212246
onRelationshipSelect: selectRelationship,
213247
onDiagramBackgroundClicked: selectBackground,
248+
onCreateNewRelationship: createNewRelationship,
214249
}
215250
)(DiagramContent);
216251

@@ -222,6 +257,17 @@ const DiagramEditor: React.FunctionComponent<{
222257
}> = ({ step, diagramId, onRetryClick, onCancelClick }) => {
223258
let content;
224259

260+
const [isInRelationshipDrawingMode, setIsInRelationshipDrawingMode] =
261+
useState(false);
262+
263+
const handleRelationshipDrawingToggle = useCallback(() => {
264+
setIsInRelationshipDrawingMode((prev) => !prev);
265+
}, []);
266+
267+
const onRelationshipDrawn = useCallback(() => {
268+
setIsInRelationshipDrawingMode(false);
269+
}, []);
270+
225271
if (step === 'NO_DIAGRAM_SELECTED') {
226272
return null;
227273
}
@@ -257,12 +303,23 @@ const DiagramEditor: React.FunctionComponent<{
257303

258304
if (step === 'EDITING' && diagramId) {
259305
content = (
260-
<ConnectedDiagramContent key={diagramId}></ConnectedDiagramContent>
306+
<ConnectedDiagramContent
307+
key={diagramId}
308+
isInRelationshipDrawingMode={isInRelationshipDrawingMode}
309+
onRelationshipDrawn={onRelationshipDrawn}
310+
></ConnectedDiagramContent>
261311
);
262312
}
263313

264314
return (
265-
<WorkspaceContainer toolbar={<DiagramEditorToolbar />}>
315+
<WorkspaceContainer
316+
toolbar={
317+
<DiagramEditorToolbar
318+
onRelationshipDrawingToggle={handleRelationshipDrawingToggle}
319+
isInRelationshipDrawingMode={isInRelationshipDrawingMode}
320+
/>
321+
}
322+
>
266323
{content}
267324
<ExportDiagramModal />
268325
</WorkspaceContainer>

packages/compass-data-modeling/src/store/analysis-process.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,11 @@ export function startAnalysis(
209209
const positioned = await applyLayout(
210210
collections.map((coll) => {
211211
return collectionToDiagramNode({
212-
ns: coll.ns,
213-
jsonSchema: coll.schema,
214-
displayPosition: [0, 0],
212+
coll: {
213+
ns: coll.ns,
214+
jsonSchema: coll.schema,
215+
displayPosition: [0, 0],
216+
},
215217
});
216218
}),
217219
[],

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 & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,17 @@ export const getFieldsFromSchema = (
142142
return fields;
143143
};
144144

145-
export function collectionToDiagramNode(
146-
coll: Pick<DataModelCollection, 'ns' | 'jsonSchema' | 'displayPosition'>,
147-
selectedFields: Record<string, string[][] | undefined> = {},
148-
selected = false
149-
): NodeProps {
145+
export function collectionToDiagramNode({
146+
coll,
147+
selectedFields = {},
148+
selected = false,
149+
isInRelationshipDrawingMode = false,
150+
}: {
151+
coll: Pick<DataModelCollection, 'ns' | 'jsonSchema' | 'displayPosition'>;
152+
selectedFields?: Record<string, string[][] | undefined>;
153+
selected?: boolean;
154+
isInRelationshipDrawingMode?: boolean;
155+
}): NodeProps {
150156
return {
151157
id: coll.ns,
152158
type: 'collection',
@@ -161,6 +167,8 @@ export function collectionToDiagramNode(
161167
0
162168
),
163169
selected,
170+
connectable: isInRelationshipDrawingMode,
171+
draggable: !isInRelationshipDrawingMode,
164172
};
165173
}
166174

0 commit comments

Comments
 (0)