Skip to content

Commit c05da8a

Browse files
committed
feat(compass-data-modeling): editing relationship via dragging COMPASS-9332
1 parent 71f0259 commit c05da8a

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 {
@@ -9,6 +15,7 @@ import {
915
selectRelationship,
1016
selectBackground,
1117
type DiagramState,
18+
createNewRelationship,
1219
} from '../store/diagram';
1320
import {
1421
Banner,
@@ -93,19 +100,25 @@ type SelectedItems = NonNullable<DiagramState>['selectedItems'];
93100
const DiagramContent: React.FunctionComponent<{
94101
diagramLabel: string;
95102
model: StaticModel | null;
103+
isInRelationshipDrawingMode: boolean;
96104
editErrors?: string[];
97105
onMoveCollection: (ns: string, newPosition: [number, number]) => void;
98106
onCollectionSelect: (namespace: string) => void;
99107
onRelationshipSelect: (rId: string) => void;
100108
onDiagramBackgroundClicked: () => void;
101109
selectedItems: SelectedItems;
110+
onCreateNewRelationship: (source: string, target: string) => void;
111+
onRelationshipDrawn: () => void;
102112
}> = ({
103113
diagramLabel,
104114
model,
115+
isInRelationshipDrawingMode,
105116
onMoveCollection,
106117
onCollectionSelect,
107118
onRelationshipSelect,
108119
onDiagramBackgroundClicked,
120+
onCreateNewRelationship,
121+
onRelationshipDrawn,
109122
selectedItems,
110123
}) => {
111124
const isDarkMode = useDarkMode();
@@ -139,9 +152,19 @@ const DiagramContent: React.FunctionComponent<{
139152
!!selectedItems &&
140153
selectedItems.type === 'collection' &&
141154
selectedItems.id === coll.ns;
142-
return collectionToDiagramNode(coll, selectedFields, selected);
155+
return collectionToDiagramNode({
156+
coll,
157+
selectedFields,
158+
selected,
159+
isInRelationshipDrawingMode,
160+
});
143161
});
144-
}, [model?.collections, model?.relationships, selectedItems]);
162+
}, [
163+
model?.collections,
164+
model?.relationships,
165+
selectedItems,
166+
isInRelationshipDrawingMode,
167+
]);
145168

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

182+
const handleNodesConnect = useCallback(
183+
(source: string, target: string) => {
184+
onCreateNewRelationship(source, target);
185+
onRelationshipDrawn();
186+
},
187+
[onRelationshipDrawn, onCreateNewRelationship]
188+
);
189+
159190
return (
160191
<div
161192
ref={setDiagramContainerRef}
@@ -192,6 +223,9 @@ const DiagramContent: React.FunctionComponent<{
192223
onNodeDragStop={(evt, node) => {
193224
onMoveCollection(node.id, [node.position.x, node.position.y]);
194225
}}
226+
onConnect={({ source, target }) => {
227+
handleNodesConnect(source, target);
228+
}}
195229
/>
196230
</div>
197231
</div>
@@ -214,6 +248,7 @@ const ConnectedDiagramContent = connect(
214248
onCollectionSelect: selectCollection,
215249
onRelationshipSelect: selectRelationship,
216250
onDiagramBackgroundClicked: selectBackground,
251+
onCreateNewRelationship: createNewRelationship,
217252
}
218253
)(DiagramContent);
219254

@@ -225,6 +260,17 @@ const DiagramEditor: React.FunctionComponent<{
225260
}> = ({ step, diagramId, onRetryClick, onCancelClick }) => {
226261
let content;
227262

263+
const [isInRelationshipDrawingMode, setIsInRelationshipDrawingMode] =
264+
useState(false);
265+
266+
const handleRelationshipDrawingToggle = useCallback(() => {
267+
setIsInRelationshipDrawingMode((prev) => !prev);
268+
}, []);
269+
270+
const onRelationshipDrawn = useCallback(() => {
271+
setIsInRelationshipDrawingMode(false);
272+
}, []);
273+
228274
if (step === 'NO_DIAGRAM_SELECTED') {
229275
return null;
230276
}
@@ -260,12 +306,23 @@ const DiagramEditor: React.FunctionComponent<{
260306

261307
if (step === 'EDITING' && diagramId) {
262308
content = (
263-
<ConnectedDiagramContent key={diagramId}></ConnectedDiagramContent>
309+
<ConnectedDiagramContent
310+
key={diagramId}
311+
isInRelationshipDrawingMode={isInRelationshipDrawingMode}
312+
onRelationshipDrawn={onRelationshipDrawn}
313+
></ConnectedDiagramContent>
264314
);
265315
}
266316

267317
return (
268-
<WorkspaceContainer toolbar={<DiagramEditorToolbar />}>
318+
<WorkspaceContainer
319+
toolbar={
320+
<DiagramEditorToolbar
321+
onRelationshipDrawingToggle={handleRelationshipDrawingToggle}
322+
isInRelationshipDrawingMode={isInRelationshipDrawingMode}
323+
/>
324+
}
325+
>
269326
{content}
270327
<ExportDiagramModal />
271328
</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
@@ -293,7 +293,8 @@ export function selectBackground(): DiagramBackgroundSelectedAction {
293293
}
294294

295295
export function createNewRelationship(
296-
namespace: string
296+
localNamespace: string,
297+
foreignNamespace: string | null = null
297298
): DataModelingThunkAction<void, RelationSelectedAction> {
298299
return (dispatch, getState, { track }) => {
299300
const relationshipId = new UUID().toString();
@@ -306,8 +307,8 @@ export function createNewRelationship(
306307
relationship: {
307308
id: relationshipId,
308309
relationship: [
309-
{ ns: namespace, cardinality: 1, fields: null },
310-
{ ns: null, cardinality: 1, fields: null },
310+
{ ns: localNamespace, cardinality: 1, fields: null },
311+
{ ns: foreignNamespace, cardinality: 1, fields: null },
311312
],
312313
isInferred: false,
313314
},

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,17 @@ export const getFieldsFromSchema = (
137137
return fields;
138138
};
139139

140-
export function collectionToDiagramNode(
141-
coll: Pick<DataModelCollection, 'ns' | 'jsonSchema' | 'displayPosition'>,
142-
selectedFields: Record<string, string[] | null | undefined> = {},
143-
selected = false
144-
): NodeProps {
140+
export function collectionToDiagramNode({
141+
coll,
142+
selectedFields = {},
143+
selected = false,
144+
isInRelationshipDrawingMode = false,
145+
}: {
146+
coll: Pick<DataModelCollection, 'ns' | 'jsonSchema' | 'displayPosition'>;
147+
selectedFields?: Record<string, string[] | null | undefined>;
148+
selected?: boolean;
149+
isInRelationshipDrawingMode?: boolean;
150+
}): NodeProps {
145151
return {
146152
id: coll.ns,
147153
type: 'collection',
@@ -156,6 +162,8 @@ export function collectionToDiagramNode(
156162
0
157163
),
158164
selected,
165+
connectable: isInRelationshipDrawingMode,
166+
draggable: !isInRelationshipDrawingMode,
159167
};
160168
}
161169

0 commit comments

Comments
 (0)