Skip to content

Commit 16b18d1

Browse files
authored
feat(data-modeling): download diagram COMPASS-9545 (#7116)
* export diagram edits * close file toast as user opens file * move to toolbar * fix test * clean up * unit test * use version and encode data
1 parent 4d5f333 commit 16b18d1

File tree

12 files changed

+492
-41
lines changed

12 files changed

+492
-41
lines changed

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ function renderDiagramEditorToolbar(
1212
step="EDITING"
1313
hasUndo={true}
1414
hasRedo={true}
15+
onDownloadClick={() => {}}
1516
onUndoClick={() => {}}
1617
onRedoClick={() => {}}
1718
onExportClick={() => {}}
@@ -63,12 +64,21 @@ describe('DiagramEditorToolbar', function () {
6364
});
6465
});
6566

66-
it('renders export buttona and calls onExportClick', function () {
67+
it('renders export button and calls onExportClick', function () {
6768
const exportSpy = sinon.spy();
6869
renderDiagramEditorToolbar({ onExportClick: exportSpy });
6970
const exportButton = screen.getByRole('button', { name: 'Export' });
7071
expect(exportButton).to.exist;
7172
userEvent.click(exportButton);
7273
expect(exportSpy).to.have.been.calledOnce;
7374
});
75+
76+
it('renders download button and calls onDownloadClick', function () {
77+
const downloadSpy = sinon.spy();
78+
renderDiagramEditorToolbar({ onDownloadClick: downloadSpy });
79+
const downloadButton = screen.getByRole('button', { name: 'Download' });
80+
expect(downloadButton).to.exist;
81+
userEvent.click(downloadButton);
82+
expect(downloadSpy).to.have.been.calledOnce;
83+
});
7484
});

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,35 @@
11
import React from 'react';
22
import { connect } from 'react-redux';
33
import type { DataModelingState } from '../store/reducer';
4-
import { redoEdit, undoEdit } from '../store/diagram';
4+
import { saveDiagram, redoEdit, undoEdit } from '../store/diagram';
55
import { showExportModal } from '../store/export-diagram';
66
import { Icon, IconButton } from '@mongodb-js/compass-components';
77

88
export const DiagramEditorToolbar: React.FunctionComponent<{
99
step: DataModelingState['step'];
1010
hasUndo: boolean;
1111
hasRedo: boolean;
12+
onDownloadClick: () => void;
1213
onUndoClick: () => void;
1314
onRedoClick: () => void;
1415
onExportClick: () => void;
15-
}> = ({ step, hasUndo, onUndoClick, hasRedo, onRedoClick, onExportClick }) => {
16+
}> = ({
17+
step,
18+
hasUndo,
19+
onUndoClick,
20+
hasRedo,
21+
onRedoClick,
22+
onExportClick,
23+
onDownloadClick,
24+
}) => {
1625
if (step !== 'EDITING') {
1726
return null;
1827
}
1928
return (
2029
<div data-testid="diagram-editor-toolbar">
30+
<IconButton aria-label="Download" onClick={onDownloadClick}>
31+
<Icon glyph="Download"></Icon>
32+
</IconButton>
2133
<IconButton aria-label="Undo" disabled={!hasUndo} onClick={onUndoClick}>
2234
<Icon glyph="Undo"></Icon>
2335
</IconButton>
@@ -44,5 +56,6 @@ export default connect(
4456
onUndoClick: undoEdit,
4557
onRedoClick: redoEdit,
4658
onExportClick: showExportModal,
59+
onDownloadClick: saveDiagram,
4760
}
4861
)(DiagramEditorToolbar);

packages/compass-data-modeling/src/services/export-diagram.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,11 @@ export function getExportJsonFromModel({
155155
};
156156
}
157157

158-
function downloadFile(uri: string, fileName: string, cleanup?: () => void) {
158+
export function downloadFile(
159+
uri: string,
160+
fileName: string,
161+
cleanup?: () => void
162+
) {
159163
const link = document.createElement('a');
160164
link.download = fileName;
161165
link.href = uri;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { expect } from 'chai';
2+
import { getDownloadDiagramContent } from './open-and-download-diagram';
3+
import FlightDiagram from '../../test/fixtures/flights-diagram.json';
4+
5+
describe('open-and-download-diagram', function () {
6+
it('should return correct content to download', function () {
7+
const fileName = 'test-diagram';
8+
9+
const { edits, ...restOfContent } = getDownloadDiagramContent(
10+
fileName,
11+
FlightDiagram.edits as any
12+
);
13+
expect(restOfContent).to.deep.equal({
14+
type: 'Compass Data Modeling Diagram',
15+
version: 1,
16+
name: fileName,
17+
});
18+
19+
const decodedEdits = JSON.parse(
20+
Buffer.from(edits, 'base64').toString('utf-8')
21+
);
22+
expect(decodedEdits).to.deep.equal(FlightDiagram.edits);
23+
});
24+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Edit } from './data-model-storage';
2+
import { downloadFile } from './export-diagram';
3+
4+
const kCurrentVersion = 1;
5+
const kFileTypeDescription = 'Compass Data Modeling Diagram';
6+
7+
export function downloadDiagram(fileName: string, edits: Edit[]) {
8+
const blob = new Blob(
9+
[JSON.stringify(getDownloadDiagramContent(fileName, edits), null, 2)],
10+
{
11+
type: 'application/json',
12+
}
13+
);
14+
const url = window.URL.createObjectURL(blob);
15+
downloadFile(url, `${fileName}.compass`, () => {
16+
window.URL.revokeObjectURL(url);
17+
});
18+
}
19+
20+
export function getDownloadDiagramContent(name: string, edits: Edit[]) {
21+
return {
22+
type: kFileTypeDescription,
23+
version: kCurrentVersion,
24+
name,
25+
edits: Buffer.from(JSON.stringify(edits)).toString('base64'),
26+
};
27+
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { AnalysisProcessActionTypes } from './analysis-process';
1111
import { memoize } from 'lodash';
1212
import type { DataModelingState, DataModelingThunkAction } from './reducer';
1313
import { showConfirmation, showPrompt } from '@mongodb-js/compass-components';
14+
import { downloadDiagram } from '../services/open-and-download-diagram';
1415

1516
function isNonEmptyArray<T>(arr: T[]): arr is [T, ...T[]] {
1617
return Array.isArray(arr) && arr.length > 0;
@@ -315,6 +316,16 @@ export function deleteDiagram(
315316
};
316317
}
317318

319+
export function saveDiagram(): DataModelingThunkAction<void, never> {
320+
return (_dispatch, getState) => {
321+
const { diagram } = getState();
322+
if (!diagram) {
323+
return;
324+
}
325+
downloadDiagram(diagram.name, diagram.edits.current);
326+
};
327+
}
328+
318329
export function renameDiagram(
319330
id: string // TODO maybe pass the whole thing here, we always have it when calling this, then we don't need to re-load storage
320331
): DataModelingThunkAction<Promise<void>, RenameDiagramAction> {

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,10 @@ export function exportDiagram(
118118
const cancelController = (cancelExportControllerRef.current =
119119
new AbortController());
120120

121-
const model = selectCurrentModel(getCurrentDiagramFromState(getState()));
122121
if (exportFormat === 'json') {
122+
const model = selectCurrentModel(
123+
getCurrentDiagramFromState(getState())
124+
);
123125
exportToJson(diagram.name, model);
124126
} else if (exportFormat === 'png') {
125127
await exportToPng(

0 commit comments

Comments
 (0)