diff --git a/packages/compass-data-modeling/src/components/diagram-editor.tsx b/packages/compass-data-modeling/src/components/diagram-editor.tsx index 0676c3e6fa2..aa5352d2415 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor.tsx @@ -10,6 +10,7 @@ import type { DataModelingState } from '../store/reducer'; import { addNewFieldToCollection, moveCollection, + moveMultipleCollections, onAddNestedField, selectCollection, selectRelationship, @@ -171,6 +172,9 @@ const DiagramContent: React.FunctionComponent<{ source: 'side_panel' | 'diagram' ) => void; onMoveCollection: (ns: string, newPosition: [number, number]) => void; + onMoveMultipleCollections: ( + newPositions: Record + ) => void; onCollectionSelect: (namespace: string) => void; onRelationshipSelect: (rId: string) => void; onFieldSelect: (namespace: string, fieldPath: FieldPath) => void; @@ -210,6 +214,7 @@ const DiagramContent: React.FunctionComponent<{ onAddFieldToObjectField, onAddNewFieldToCollection, onMoveCollection, + onMoveMultipleCollections, onCollectionSelect, onRelationshipSelect, onFieldSelect, @@ -382,10 +387,20 @@ const DiagramContent: React.FunctionComponent<{ ); const onNodeDragStop = useCallback( - (evt: React.MouseEvent, node: NodeProps) => { - onMoveCollection(node.id, [node.position.x, node.position.y]); + (evt: React.MouseEvent, node: NodeProps, nodes: NodeProps[]) => { + if (nodes.length === 1) { + onMoveCollection(node.id, [node.position.x, node.position.y]); + } else { + const newPositions = Object.fromEntries( + nodes.map((node): [string, [number, number]] => [ + node.id, + [node.position.x, node.position.y], + ]) + ); + onMoveMultipleCollections(newPositions); + } }, - [onMoveCollection] + [onMoveCollection, onMoveMultipleCollections] ); const onPaneClick = useCallback(() => { @@ -543,6 +558,7 @@ const ConnectedDiagramContent = connect( onAddNewFieldToCollection: addNewFieldToCollection, onAddFieldToObjectField: onAddNestedField, onMoveCollection: moveCollection, + onMoveMultipleCollections: moveMultipleCollections, onCollectionSelect: selectCollection, onRelationshipSelect: selectRelationship, onFieldSelect: selectField, diff --git a/packages/compass-data-modeling/src/services/data-model-storage.ts b/packages/compass-data-modeling/src/services/data-model-storage.ts index 1ba4ee94f35..df51c575e59 100644 --- a/packages/compass-data-modeling/src/services/data-model-storage.ts +++ b/packages/compass-data-modeling/src/services/data-model-storage.ts @@ -78,6 +78,10 @@ const EditSchemaVariants = z.discriminatedUnion('type', [ ns: z.string(), newPosition: z.tuple([z.number(), z.number()]), }), + z.object({ + type: z.literal('MoveMultipleCollections'), + newPositions: z.record(z.string(), z.tuple([z.number(), z.number()])), + }), z.object({ type: z.literal('RemoveCollection'), ns: z.string(), diff --git a/packages/compass-data-modeling/src/store/apply-edit.ts b/packages/compass-data-modeling/src/store/apply-edit.ts index 495b0c43819..222be42c87d 100644 --- a/packages/compass-data-modeling/src/store/apply-edit.ts +++ b/packages/compass-data-modeling/src/store/apply-edit.ts @@ -111,6 +111,24 @@ export function applyEdit(edit: Edit, model?: StaticModel): StaticModel { }), }; } + case 'MoveMultipleCollections': { + const movedCollections = new Set(Object.keys(edit.newPositions)); + for (const ns of movedCollections) { + assertCollectionExists(model.collections, ns); + } + return { + ...model, + collections: model.collections.map((collection) => { + if (movedCollections.has(collection.ns)) { + return { + ...collection, + displayPosition: edit.newPositions[collection.ns], + }; + } + return collection; + }), + }; + } case 'RemoveCollection': { assertCollectionExists(model.collections, edit.ns); return { diff --git a/packages/compass-data-modeling/src/store/diagram.spec.ts b/packages/compass-data-modeling/src/store/diagram.spec.ts index 4cd459ed780..2b4821d9061 100644 --- a/packages/compass-data-modeling/src/store/diagram.spec.ts +++ b/packages/compass-data-modeling/src/store/diagram.spec.ts @@ -380,6 +380,45 @@ describe('Data Modeling store', function () { ]); }); + it('should apply a valid MoveMultipleCollections edit', function () { + store.dispatch(openDiagram(loadedDiagram)); + + const newPosition0 = [ + model.collections[0].displayPosition[0] + 20, + 100, + ] as [number, number]; + const newPosition1 = [ + model.collections[1].displayPosition[0] + 20, + 200, + ] as [number, number]; + const edit: Omit< + Extract, + 'id' | 'timestamp' + > = { + type: 'MoveMultipleCollections', + newPositions: { + [model.collections[0].ns]: newPosition0, + [model.collections[1].ns]: newPosition1, + }, + }; + store.dispatch(applyEdit(edit)); + + const state = store.getState(); + const diagram = getCurrentDiagramFromState(state); + expect(openToastSpy).not.to.have.been.called; + expect(diagram.edits).to.have.length(2); + expect(diagram.edits[0]).to.deep.equal(loadedDiagram.edits[0]); + expect(diagram.edits[1]).to.deep.include(edit); + + const currentModel = getCurrentModel(diagram.edits); + expect(currentModel.collections[0].displayPosition).to.deep.equal( + newPosition0 + ); + expect(currentModel.collections[1].displayPosition).to.deep.equal( + newPosition1 + ); + }); + it('should not apply invalid MoveCollection edit', function () { store.dispatch(openDiagram(loadedDiagram)); diff --git a/packages/compass-data-modeling/src/store/diagram.ts b/packages/compass-data-modeling/src/store/diagram.ts index b1ab2003a77..3694c301139 100644 --- a/packages/compass-data-modeling/src/store/diagram.ts +++ b/packages/compass-data-modeling/src/store/diagram.ts @@ -591,6 +591,19 @@ export function moveCollection( return applyEdit(edit); } +export function moveMultipleCollections( + newPositions: Record +): DataModelingThunkAction { + const edit: Omit< + Extract, + 'id' | 'timestamp' + > = { + type: 'MoveMultipleCollections', + newPositions, + }; + return applyEdit(edit); +} + export function renameCollection( fromNS: string, toNS: string