Skip to content

Commit d409119

Browse files
feat: MoveCollection COMPASS-9434 (#7060)
--------- Co-authored-by: Sergey Petushkov <[email protected]>
1 parent e59a5ef commit d409119

File tree

8 files changed

+159
-195
lines changed

8 files changed

+159
-195
lines changed

package-lock.json

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass-data-modeling/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
"@mongodb-js/compass-app-stores": "^7.48.1",
5959
"@mongodb-js/compass-components": "^1.40.0",
6060
"@mongodb-js/compass-connections": "^1.62.1",
61-
"@mongodb-js/compass-editor": "^0.42.0",
6261
"@mongodb-js/compass-logging": "^1.7.4",
6362
"@mongodb-js/compass-telemetry": "^1.10.2",
6463
"@mongodb-js/compass-user-data": "^0.7.4",

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

Lines changed: 35 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import { connect } from 'react-redux';
99
import type { MongoDBJSONSchema } from 'mongodb-schema';
1010
import type { DataModelingState } from '../store/reducer';
1111
import {
12-
applyEdit,
1312
applyInitialLayout,
13+
moveCollection,
1414
getCurrentDiagramFromState,
1515
selectCurrentModel,
1616
} from '../store/diagram';
@@ -23,11 +23,8 @@ import {
2323
css,
2424
spacing,
2525
Button,
26-
palette,
27-
ErrorSummary,
2826
useDarkMode,
2927
} from '@mongodb-js/compass-components';
30-
import { CodemirrorMultilineEditor } from '@mongodb-js/compass-editor';
3128
import { cancelAnalysis, retryAnalysis } from '../store/analysis-process';
3229
import {
3330
Diagram,
@@ -36,8 +33,7 @@ import {
3633
useDiagram,
3734
applyLayout,
3835
} from '@mongodb-js/diagramming';
39-
import type { Edit, StaticModel } from '../services/data-model-storage';
40-
import { UUID } from 'bson';
36+
import type { StaticModel } from '../services/data-model-storage';
4137
import DiagramEditorToolbar from './diagram-editor-toolbar';
4238
import ExportDiagramModal from './export-diagram-modal';
4339
import { useLogger } from '@mongodb-js/compass-logging/provider';
@@ -119,49 +115,23 @@ const modelPreviewStyles = css({
119115
minHeight: 0,
120116
});
121117

122-
const editorContainerStyles = css({
123-
display: 'flex',
124-
flexDirection: 'column',
125-
gap: 8,
126-
boxShadow: `0 0 0 2px ${palette.gray.light2}`,
127-
});
128-
129-
const editorContainerApplyContainerStyles = css({
130-
padding: spacing[200],
131-
justifyContent: 'flex-end',
132-
gap: spacing[200],
133-
display: 'flex',
134-
width: '100%',
135-
alignItems: 'center',
136-
});
137-
138-
const editorContainerPlaceholderButtonStyles = css({
139-
paddingLeft: 8,
140-
paddingRight: 8,
141-
alignSelf: 'flex-start',
142-
display: 'flex',
143-
gap: spacing[200],
144-
paddingTop: spacing[200],
145-
});
146-
147118
const DiagramEditor: React.FunctionComponent<{
148119
diagramLabel: string;
149120
step: DataModelingState['step'];
150121
model: StaticModel | null;
151122
editErrors?: string[];
152123
onRetryClick: () => void;
153124
onCancelClick: () => void;
154-
onApplyClick: (edit: Omit<Edit, 'id' | 'timestamp'>) => void;
155125
onApplyInitialLayout: (positions: Record<string, [number, number]>) => void;
126+
onMoveCollection: (ns: string, newPosition: [number, number]) => void;
156127
}> = ({
157128
diagramLabel,
158129
step,
159130
model,
160-
editErrors,
161131
onRetryClick,
162132
onCancelClick,
163-
onApplyClick,
164133
onApplyInitialLayout,
134+
onMoveCollection,
165135
}) => {
166136
const { log, mongoLogId } = useLogger('COMPASS-DATA-MODELING-DIAGRAM-EDITOR');
167137
const isDarkMode = useDarkMode();
@@ -180,54 +150,6 @@ const DiagramEditor: React.FunctionComponent<{
180150
[diagram]
181151
);
182152

183-
const [applyInput, setApplyInput] = useState('{}');
184-
185-
const isEditValid = useMemo(() => {
186-
try {
187-
JSON.parse(applyInput);
188-
return true;
189-
} catch {
190-
return false;
191-
}
192-
}, [applyInput]);
193-
194-
const applyPlaceholder =
195-
(type: 'AddRelationship' | 'RemoveRelationship') => () => {
196-
let placeholder = {};
197-
switch (type) {
198-
case 'AddRelationship':
199-
placeholder = {
200-
type: 'AddRelationship',
201-
relationship: {
202-
id: new UUID().toString(),
203-
relationship: [
204-
{
205-
ns: 'db.sourceCollection',
206-
cardinality: 1,
207-
fields: ['field1'],
208-
},
209-
{
210-
ns: 'db.targetCollection',
211-
cardinality: 1,
212-
fields: ['field2'],
213-
},
214-
],
215-
isInferred: false,
216-
},
217-
};
218-
break;
219-
case 'RemoveRelationship':
220-
placeholder = {
221-
type: 'RemoveRelationship',
222-
relationshipId: new UUID().toString(),
223-
};
224-
break;
225-
default:
226-
throw new Error(`Unknown placeholder ${type}`);
227-
}
228-
setApplyInput(JSON.stringify(placeholder, null, 2));
229-
};
230-
231153
const edges = useMemo(() => {
232154
return (model?.relationships ?? []).map((relationship): EdgeProps => {
233155
const [source, target] = relationship.relationship;
@@ -241,31 +163,6 @@ const DiagramEditor: React.FunctionComponent<{
241163
});
242164
}, [model?.relationships]);
243165

244-
const applyInitialLayout = useCallback(async () => {
245-
try {
246-
const { nodes: positionedNodes } = await applyLayout(
247-
nodes,
248-
edges,
249-
'LEFT_RIGHT'
250-
);
251-
onApplyInitialLayout(
252-
Object.fromEntries(
253-
positionedNodes.map((node) => [
254-
node.id,
255-
[node.position.x, node.position.y],
256-
])
257-
)
258-
);
259-
} catch (err) {
260-
log.error(
261-
mongoLogId(1_001_000_361),
262-
'DiagramEditor',
263-
'Error applying layout:',
264-
err
265-
);
266-
}
267-
}, [edges, log, mongoLogId, onApplyInitialLayout]);
268-
269166
const nodes = useMemo<NodeProps[]>(() => {
270167
return (model?.collections ?? []).map(
271168
(coll): NodeProps => ({
@@ -290,6 +187,31 @@ const DiagramEditor: React.FunctionComponent<{
290187
);
291188
}, [model?.collections]);
292189

190+
const applyInitialLayout = useCallback(async () => {
191+
try {
192+
const { nodes: positionedNodes } = await applyLayout(
193+
nodes,
194+
edges,
195+
'LEFT_RIGHT'
196+
);
197+
onApplyInitialLayout(
198+
Object.fromEntries(
199+
positionedNodes.map((node) => [
200+
node.id,
201+
[node.position.x, node.position.y],
202+
])
203+
)
204+
);
205+
} catch (err) {
206+
log.error(
207+
mongoLogId(1_001_000_361),
208+
'DiagramEditor',
209+
'Error applying layout:',
210+
err
211+
);
212+
}
213+
}, [edges, log, nodes, mongoLogId, onApplyInitialLayout]);
214+
293215
useEffect(() => {
294216
if (nodes.length === 0) return;
295217
const isInitialState = nodes.some(
@@ -300,8 +222,10 @@ const DiagramEditor: React.FunctionComponent<{
300222
return;
301223
}
302224
if (!areNodesReady) {
303-
void diagram.fitView();
304225
setAreNodesReady(true);
226+
setTimeout(() => {
227+
void diagram.fitView();
228+
});
305229
}
306230
}, [areNodesReady, nodes, diagram, applyInitialLayout]);
307231

@@ -357,56 +281,11 @@ const DiagramEditor: React.FunctionComponent<{
357281
maxZoom: 1,
358282
minZoom: 0.25,
359283
}}
360-
onEdgeClick={(evt, edge) => {
361-
setApplyInput(
362-
JSON.stringify(
363-
{
364-
type: 'RemoveRelationship',
365-
relationshipId: edge.id,
366-
},
367-
null,
368-
2
369-
)
370-
);
284+
onNodeDragStop={(evt, node) => {
285+
onMoveCollection(node.id, [node.position.x, node.position.y]);
371286
}}
372287
/>
373288
</div>
374-
<div className={editorContainerStyles} data-testid="apply-editor">
375-
<div className={editorContainerPlaceholderButtonStyles}>
376-
<Button
377-
onClick={applyPlaceholder('AddRelationship')}
378-
data-testid="placeholder-addrelationship-button"
379-
>
380-
Add relationship
381-
</Button>
382-
<Button
383-
onClick={applyPlaceholder('RemoveRelationship')}
384-
data-testid="placeholder-removerelationship-button"
385-
>
386-
Remove relationship
387-
</Button>
388-
</div>
389-
<div>
390-
<CodemirrorMultilineEditor
391-
language="json"
392-
text={applyInput}
393-
onChangeText={setApplyInput}
394-
maxLines={10}
395-
></CodemirrorMultilineEditor>
396-
</div>
397-
<div className={editorContainerApplyContainerStyles}>
398-
{editErrors && <ErrorSummary errors={editErrors} />}
399-
<Button
400-
onClick={() => {
401-
onApplyClick(JSON.parse(applyInput));
402-
}}
403-
data-testid="apply-button"
404-
disabled={!isEditValid}
405-
>
406-
Apply
407-
</Button>
408-
</div>
409-
</div>
410289
</div>
411290
);
412291
}
@@ -434,7 +313,7 @@ export default connect(
434313
{
435314
onRetryClick: retryAnalysis,
436315
onCancelClick: cancelAnalysis,
437-
onApplyClick: applyEdit,
438316
onApplyInitialLayout: applyInitialLayout,
317+
onMoveCollection: moveCollection,
439318
}
440319
)(DiagramEditor);

packages/compass-data-modeling/src/services/data-model-storage.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ const EditSchemaVariants = z.discriminatedUnion('type', [
5555
type: z.literal('RemoveRelationship'),
5656
relationshipId: z.string().uuid(),
5757
}),
58+
z.object({
59+
type: z.literal('MoveCollection'),
60+
ns: z.string(),
61+
newPosition: z.tuple([z.number(), z.number()]),
62+
}),
5863
]);
5964

6065
export const EditSchema = z.intersection(EditSchemaBase, EditSchemaVariants);

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
applyEdit,
55
applyInitialLayout,
66
getCurrentDiagramFromState,
7+
getCurrentModel,
78
openDiagram,
89
redoEdit,
910
undoEdit,
@@ -201,6 +202,7 @@ describe('Data Modeling store', function () {
201202
],
202203
isInferred: false,
203204
};
205+
204206
store.dispatch(
205207
applyEdit({
206208
type: 'AddRelationship',
@@ -217,6 +219,9 @@ describe('Data Modeling store', function () {
217219
type: 'AddRelationship',
218220
relationship: newRelationship,
219221
});
222+
223+
const currentModel = getCurrentModel(diagram);
224+
expect(currentModel.relationships).to.have.length(2);
220225
});
221226

222227
it('should not apply invalid AddRelationship edit', function () {
@@ -239,6 +244,48 @@ describe('Data Modeling store', function () {
239244
const diagram = getCurrentDiagramFromState(store.getState());
240245
expect(diagram.edits).to.deep.equal(loadedDiagram.edits);
241246
});
247+
248+
it('should apply a valid MoveCollection edit', function () {
249+
store.dispatch(openDiagram(loadedDiagram));
250+
251+
const edit: Omit<
252+
Extract<Edit, { type: 'MoveCollection' }>,
253+
'id' | 'timestamp'
254+
> = {
255+
type: 'MoveCollection',
256+
ns: model.collections[0].ns,
257+
newPosition: [100, 100],
258+
};
259+
store.dispatch(applyEdit(edit));
260+
261+
const state = store.getState();
262+
const diagram = getCurrentDiagramFromState(state);
263+
expect(state.diagram?.editErrors).to.be.undefined;
264+
expect(diagram.edits).to.have.length(2);
265+
expect(diagram.edits[0]).to.deep.equal(loadedDiagram.edits[0]);
266+
expect(diagram.edits[1]).to.deep.include(edit);
267+
268+
const currentModel = getCurrentModel(diagram);
269+
expect(currentModel.collections[0].displayPosition).to.deep.equal([
270+
100, 100,
271+
]);
272+
});
273+
274+
it('should not apply invalid MoveCollection edit', function () {
275+
store.dispatch(openDiagram(loadedDiagram));
276+
277+
const edit = {
278+
type: 'MoveCollection',
279+
ns: 'nonexistent.collection',
280+
} as unknown as Edit;
281+
store.dispatch(applyEdit(edit));
282+
283+
const editErrors = store.getState().diagram?.editErrors;
284+
expect(editErrors).to.have.length(1);
285+
expect(editErrors && editErrors[0]).to.equal("'newPosition' is required");
286+
const diagram = getCurrentDiagramFromState(store.getState());
287+
expect(diagram.edits).to.deep.equal(loadedDiagram.edits);
288+
});
242289
});
243290

244291
it('undo & redo', function () {

0 commit comments

Comments
 (0)