diff --git a/package-lock.json b/package-lock.json index cdb331342aa..d3f2218d3f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44238,7 +44238,6 @@ "@mongodb-js/compass-app-stores": "^7.48.1", "@mongodb-js/compass-components": "^1.40.0", "@mongodb-js/compass-connections": "^1.62.1", - "@mongodb-js/compass-editor": "^0.42.0", "@mongodb-js/compass-logging": "^1.7.4", "@mongodb-js/compass-telemetry": "^1.10.2", "@mongodb-js/compass-user-data": "^0.7.4", @@ -56788,7 +56787,6 @@ "@mongodb-js/compass-app-stores": "^7.48.1", "@mongodb-js/compass-components": "^1.40.0", "@mongodb-js/compass-connections": "^1.62.1", - "@mongodb-js/compass-editor": "^0.42.0", "@mongodb-js/compass-logging": "^1.7.4", "@mongodb-js/compass-telemetry": "^1.10.2", "@mongodb-js/compass-user-data": "^0.7.4", diff --git a/packages/compass-data-modeling/package.json b/packages/compass-data-modeling/package.json index c454fcd8fff..a740429fe79 100644 --- a/packages/compass-data-modeling/package.json +++ b/packages/compass-data-modeling/package.json @@ -58,7 +58,6 @@ "@mongodb-js/compass-app-stores": "^7.48.1", "@mongodb-js/compass-components": "^1.40.0", "@mongodb-js/compass-connections": "^1.62.1", - "@mongodb-js/compass-editor": "^0.42.0", "@mongodb-js/compass-logging": "^1.7.4", "@mongodb-js/compass-telemetry": "^1.10.2", "@mongodb-js/compass-user-data": "^0.7.4", diff --git a/packages/compass-data-modeling/src/components/diagram-editor.tsx b/packages/compass-data-modeling/src/components/diagram-editor.tsx index 4aa22f5650f..d64803b4845 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor.tsx @@ -9,8 +9,8 @@ import { connect } from 'react-redux'; import type { MongoDBJSONSchema } from 'mongodb-schema'; import type { DataModelingState } from '../store/reducer'; import { - applyEdit, applyInitialLayout, + moveCollection, getCurrentDiagramFromState, selectCurrentModel, } from '../store/diagram'; @@ -23,11 +23,8 @@ import { css, spacing, Button, - palette, - ErrorSummary, useDarkMode, } from '@mongodb-js/compass-components'; -import { CodemirrorMultilineEditor } from '@mongodb-js/compass-editor'; import { cancelAnalysis, retryAnalysis } from '../store/analysis-process'; import { Diagram, @@ -36,8 +33,7 @@ import { useDiagram, applyLayout, } from '@mongodb-js/diagramming'; -import type { Edit, StaticModel } from '../services/data-model-storage'; -import { UUID } from 'bson'; +import type { StaticModel } from '../services/data-model-storage'; import DiagramEditorToolbar from './diagram-editor-toolbar'; import ExportDiagramModal from './export-diagram-modal'; import { useLogger } from '@mongodb-js/compass-logging/provider'; @@ -119,31 +115,6 @@ const modelPreviewStyles = css({ minHeight: 0, }); -const editorContainerStyles = css({ - display: 'flex', - flexDirection: 'column', - gap: 8, - boxShadow: `0 0 0 2px ${palette.gray.light2}`, -}); - -const editorContainerApplyContainerStyles = css({ - padding: spacing[200], - justifyContent: 'flex-end', - gap: spacing[200], - display: 'flex', - width: '100%', - alignItems: 'center', -}); - -const editorContainerPlaceholderButtonStyles = css({ - paddingLeft: 8, - paddingRight: 8, - alignSelf: 'flex-start', - display: 'flex', - gap: spacing[200], - paddingTop: spacing[200], -}); - const DiagramEditor: React.FunctionComponent<{ diagramLabel: string; step: DataModelingState['step']; @@ -151,17 +122,16 @@ const DiagramEditor: React.FunctionComponent<{ editErrors?: string[]; onRetryClick: () => void; onCancelClick: () => void; - onApplyClick: (edit: Omit) => void; onApplyInitialLayout: (positions: Record) => void; + onMoveCollection: (ns: string, newPosition: [number, number]) => void; }> = ({ diagramLabel, step, model, - editErrors, onRetryClick, onCancelClick, - onApplyClick, onApplyInitialLayout, + onMoveCollection, }) => { const { log, mongoLogId } = useLogger('COMPASS-DATA-MODELING-DIAGRAM-EDITOR'); const isDarkMode = useDarkMode(); @@ -180,54 +150,6 @@ const DiagramEditor: React.FunctionComponent<{ [diagram] ); - const [applyInput, setApplyInput] = useState('{}'); - - const isEditValid = useMemo(() => { - try { - JSON.parse(applyInput); - return true; - } catch { - return false; - } - }, [applyInput]); - - const applyPlaceholder = - (type: 'AddRelationship' | 'RemoveRelationship') => () => { - let placeholder = {}; - switch (type) { - case 'AddRelationship': - placeholder = { - type: 'AddRelationship', - relationship: { - id: new UUID().toString(), - relationship: [ - { - ns: 'db.sourceCollection', - cardinality: 1, - fields: ['field1'], - }, - { - ns: 'db.targetCollection', - cardinality: 1, - fields: ['field2'], - }, - ], - isInferred: false, - }, - }; - break; - case 'RemoveRelationship': - placeholder = { - type: 'RemoveRelationship', - relationshipId: new UUID().toString(), - }; - break; - default: - throw new Error(`Unknown placeholder ${type}`); - } - setApplyInput(JSON.stringify(placeholder, null, 2)); - }; - const edges = useMemo(() => { return (model?.relationships ?? []).map((relationship): EdgeProps => { const [source, target] = relationship.relationship; @@ -241,31 +163,6 @@ const DiagramEditor: React.FunctionComponent<{ }); }, [model?.relationships]); - const applyInitialLayout = useCallback(async () => { - try { - const { nodes: positionedNodes } = await applyLayout( - nodes, - edges, - 'LEFT_RIGHT' - ); - onApplyInitialLayout( - Object.fromEntries( - positionedNodes.map((node) => [ - node.id, - [node.position.x, node.position.y], - ]) - ) - ); - } catch (err) { - log.error( - mongoLogId(1_001_000_361), - 'DiagramEditor', - 'Error applying layout:', - err - ); - } - }, [edges, log, mongoLogId, onApplyInitialLayout]); - const nodes = useMemo(() => { return (model?.collections ?? []).map( (coll): NodeProps => ({ @@ -290,6 +187,31 @@ const DiagramEditor: React.FunctionComponent<{ ); }, [model?.collections]); + const applyInitialLayout = useCallback(async () => { + try { + const { nodes: positionedNodes } = await applyLayout( + nodes, + edges, + 'LEFT_RIGHT' + ); + onApplyInitialLayout( + Object.fromEntries( + positionedNodes.map((node) => [ + node.id, + [node.position.x, node.position.y], + ]) + ) + ); + } catch (err) { + log.error( + mongoLogId(1_001_000_361), + 'DiagramEditor', + 'Error applying layout:', + err + ); + } + }, [edges, log, nodes, mongoLogId, onApplyInitialLayout]); + useEffect(() => { if (nodes.length === 0) return; const isInitialState = nodes.some( @@ -300,8 +222,10 @@ const DiagramEditor: React.FunctionComponent<{ return; } if (!areNodesReady) { - void diagram.fitView(); setAreNodesReady(true); + setTimeout(() => { + void diagram.fitView(); + }); } }, [areNodesReady, nodes, diagram, applyInitialLayout]); @@ -357,56 +281,11 @@ const DiagramEditor: React.FunctionComponent<{ maxZoom: 1, minZoom: 0.25, }} - onEdgeClick={(evt, edge) => { - setApplyInput( - JSON.stringify( - { - type: 'RemoveRelationship', - relationshipId: edge.id, - }, - null, - 2 - ) - ); + onNodeDragStop={(evt, node) => { + onMoveCollection(node.id, [node.position.x, node.position.y]); }} /> -
-
- - -
-
- -
-
- {editErrors && } - -
-
); } @@ -434,7 +313,7 @@ export default connect( { onRetryClick: retryAnalysis, onCancelClick: cancelAnalysis, - onApplyClick: applyEdit, onApplyInitialLayout: applyInitialLayout, + onMoveCollection: moveCollection, } )(DiagramEditor); 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 b6ab3f417ab..7134c94c6d6 100644 --- a/packages/compass-data-modeling/src/services/data-model-storage.ts +++ b/packages/compass-data-modeling/src/services/data-model-storage.ts @@ -55,6 +55,11 @@ const EditSchemaVariants = z.discriminatedUnion('type', [ type: z.literal('RemoveRelationship'), relationshipId: z.string().uuid(), }), + z.object({ + type: z.literal('MoveCollection'), + ns: z.string(), + newPosition: z.tuple([z.number(), z.number()]), + }), ]); export const EditSchema = z.intersection(EditSchemaBase, EditSchemaVariants); diff --git a/packages/compass-data-modeling/src/store/diagram.spec.ts b/packages/compass-data-modeling/src/store/diagram.spec.ts index bcfa0ac055b..5b3e1b87592 100644 --- a/packages/compass-data-modeling/src/store/diagram.spec.ts +++ b/packages/compass-data-modeling/src/store/diagram.spec.ts @@ -4,6 +4,7 @@ import { applyEdit, applyInitialLayout, getCurrentDiagramFromState, + getCurrentModel, openDiagram, redoEdit, undoEdit, @@ -201,6 +202,7 @@ describe('Data Modeling store', function () { ], isInferred: false, }; + store.dispatch( applyEdit({ type: 'AddRelationship', @@ -217,6 +219,9 @@ describe('Data Modeling store', function () { type: 'AddRelationship', relationship: newRelationship, }); + + const currentModel = getCurrentModel(diagram); + expect(currentModel.relationships).to.have.length(2); }); it('should not apply invalid AddRelationship edit', function () { @@ -239,6 +244,48 @@ describe('Data Modeling store', function () { const diagram = getCurrentDiagramFromState(store.getState()); expect(diagram.edits).to.deep.equal(loadedDiagram.edits); }); + + it('should apply a valid MoveCollection edit', function () { + store.dispatch(openDiagram(loadedDiagram)); + + const edit: Omit< + Extract, + 'id' | 'timestamp' + > = { + type: 'MoveCollection', + ns: model.collections[0].ns, + newPosition: [100, 100], + }; + store.dispatch(applyEdit(edit)); + + const state = store.getState(); + const diagram = getCurrentDiagramFromState(state); + expect(state.diagram?.editErrors).to.be.undefined; + 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); + expect(currentModel.collections[0].displayPosition).to.deep.equal([ + 100, 100, + ]); + }); + + it('should not apply invalid MoveCollection edit', function () { + store.dispatch(openDiagram(loadedDiagram)); + + const edit = { + type: 'MoveCollection', + ns: 'nonexistent.collection', + } as unknown as Edit; + store.dispatch(applyEdit(edit)); + + const editErrors = store.getState().diagram?.editErrors; + expect(editErrors).to.have.length(1); + expect(editErrors && editErrors[0]).to.equal("'newPosition' is required"); + const diagram = getCurrentDiagramFromState(store.getState()); + expect(diagram.edits).to.deep.equal(loadedDiagram.edits); + }); }); it('undo & redo', function () { diff --git a/packages/compass-data-modeling/src/store/diagram.ts b/packages/compass-data-modeling/src/store/diagram.ts index 46459f4f67e..ac9e2e7352f 100644 --- a/packages/compass-data-modeling/src/store/diagram.ts +++ b/packages/compass-data-modeling/src/store/diagram.ts @@ -260,6 +260,21 @@ export function redoEdit(): DataModelingThunkAction { }; } +export function moveCollection( + ns: string, + newPosition: [number, number] +): DataModelingThunkAction { + const edit: Omit< + Extract, + 'id' | 'timestamp' + > = { + type: 'MoveCollection', + ns, + newPosition, + }; + return applyEdit(edit); +} + export function applyEdit( rawEdit: Omit ): DataModelingThunkAction { @@ -367,6 +382,20 @@ function _applyEdit(edit: Edit, model?: StaticModel): StaticModel { ), }; } + case 'MoveCollection': { + return { + ...model, + collections: model.collections.map((collection) => { + if (collection.ns === edit.ns) { + return { + ...collection, + displayPosition: edit.newPosition, + }; + } + return collection; + }), + }; + } default: { return model; } diff --git a/packages/compass-e2e-tests/helpers/selectors.ts b/packages/compass-e2e-tests/helpers/selectors.ts index 2a4e6050760..5e9838fb166 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -1442,6 +1442,8 @@ export const CreateDataModelCollectionCheckbox = ( `${CreateDataModelModal} [data-testid="new-diagram-collection-checkbox-${collectionName}"]`; export const DataModelEditor = '[data-testid="diagram-editor-container"]'; export const DataModelPreview = `${DataModelEditor} [data-testid="model-preview"]`; +export const DataModelPreviewCollection = (collectionId: string) => + `${DataModelPreview} [data-nodeid="${collectionId}"]`; export const DataModelApplyEditor = `${DataModelEditor} [data-testid="apply-editor"]`; export const DataModelEditorApplyButton = `${DataModelApplyEditor} [data-testid="apply-button"]`; export const DataModelUndoButton = 'button[aria-label="Undo"]'; diff --git a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts index ea1e29b11b8..ead65345527 100644 --- a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts +++ b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts @@ -18,10 +18,13 @@ import { } from '../helpers/downloads'; import { readFileSync } from 'fs'; +interface Node { + id: string; + position: { x: number; y: number }; +} + type DiagramInstance = { - getNodes: () => Array<{ - id: string; - }>; + getNodes: () => Array; }; async function setupDiagram( @@ -70,7 +73,7 @@ async function setupDiagram( await dataModelEditor.waitForDisplayed(); } -async function getDiagramNodes(browser: CompassBrowser): Promise { +async function getDiagramNodes(browser: CompassBrowser): Promise { const nodes = await browser.execute(function (selector) { const node = document.querySelector(selector); if (!node) { @@ -80,7 +83,7 @@ async function getDiagramNodes(browser: CompassBrowser): Promise { node as Element & { _diagram: DiagramInstance } )._diagram.getNodes(); }, Selectors.DataModelEditor); - return nodes.map((x) => x.id); + return nodes; } describe('Data Modeling tab', function () { @@ -129,49 +132,51 @@ describe('Data Modeling tab', function () { const dataModelEditor = browser.$(Selectors.DataModelEditor); await dataModelEditor.waitForDisplayed(); - let nodes = await getDiagramNodes(browser); + const nodes = await getDiagramNodes(browser); expect(nodes).to.have.lengthOf(2); - expect(nodes).to.deep.equal([ - 'test.testCollection1', - 'test.testCollection2', - ]); + expect(nodes[0].id).to.equal('test.testCollection1'); + expect(nodes[1].id).to.equal('test.testCollection2'); // Apply change to the model - const newModel = { - type: 'SetModel', - model: { - collections: [], - relationships: [], - }, - }; - await browser.setCodemirrorEditorValue( - Selectors.DataModelApplyEditor, - JSON.stringify(newModel) + + // react flow uses its own coordinate system, + // so we get the node element location for the pointer action + const testCollection1 = browser.$( + Selectors.DataModelPreviewCollection('test.testCollection1') ); - await browser.clickVisible(Selectors.DataModelEditorApplyButton); + const startPosition = await testCollection1.getLocation(); + const nodeSize = await testCollection1.getSize(); + + await browser + .action('pointer') + .move({ + x: Math.round(startPosition.x + nodeSize.width / 2), + y: Math.round(startPosition.y + nodeSize.height / 2), + }) + .down({ button: 0 }) // Left mouse button + .move({ x: 100, y: 0, duration: 1000, origin: 'pointer' }) + .pause(1000) + .move({ x: 100, y: 0, duration: 1000, origin: 'pointer' }) + .up({ button: 0 }) // Release the left mouse button + .perform(); await browser.waitForAnimations(dataModelEditor); - // Verify that the model is updated - nodes = await getDiagramNodes(browser); - expect(nodes).to.have.lengthOf(0); + // Check that the first node has moved and mark the new position + const newPosition = await testCollection1.getLocation(); + expect(newPosition).not.to.deep.equal(startPosition); // Undo the change await browser.clickVisible(Selectors.DataModelUndoButton); await browser.waitForAnimations(dataModelEditor); - nodes = await getDiagramNodes(browser); - expect(nodes).to.have.lengthOf(2); - expect(nodes).to.deep.equal([ - 'test.testCollection1', - 'test.testCollection2', - ]); + const positionAfterUndone = await testCollection1.getLocation(); + expect(positionAfterUndone).to.deep.equal(startPosition); // Redo the change await browser.waitForAriaDisabled(Selectors.DataModelRedoButton, false); await browser.clickVisible(Selectors.DataModelRedoButton); await browser.waitForAnimations(dataModelEditor); - nodes = await getDiagramNodes(browser); - expect(nodes).to.have.lengthOf(0); - + const positionAfterRedo = await testCollection1.getLocation(); + expect(positionAfterRedo).to.deep.equal(newPosition); // Open a new tab await browser.openNewTab(); @@ -179,9 +184,9 @@ describe('Data Modeling tab', function () { await browser.clickVisible(Selectors.DataModelsListItem(dataModelName)); await browser.$(Selectors.DataModelEditor).waitForDisplayed(); - // Verify that the diagram has the latest changes - nodes = await getDiagramNodes(browser); - expect(nodes).to.have.lengthOf(0); + // TODO: Verify that the diagram has the latest changes COMPASS-9479 + const savedNodes = await getDiagramNodes(browser); + expect(savedNodes).to.have.lengthOf(2); // Open a new tab await browser.openNewTab();