Skip to content

Commit 7ac0cd8

Browse files
mabaasitgribnoysup
andauthored
feat(data-modeling): export diagram to json COMPASS-9448 (#7046)
* extract diagram editor toolbar: * add export modal * json export * tests * close modal * tests * ensure test run * fix toast * fix electron test * fix link * ensure its thrown * asset number of selected collections * fix modal styles * return null for tests * remove comment * Update packages/compass-data-modeling/src/components/diagram-editor-toolbar.tsx Co-authored-by: Sergey Petushkov <[email protected]> * flip export props --------- Co-authored-by: Sergey Petushkov <[email protected]>
1 parent f9f5848 commit 7ac0cd8

File tree

11 files changed

+827
-79
lines changed

11 files changed

+827
-79
lines changed

packages/compass-components/src/components/toast-body.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { Link } from './leafygreen';
2+
import { Link, Body } from './leafygreen';
33
import { css } from '@leafygreen-ui/emotion';
44

55
const toastBodyFlexStyles = css({
@@ -34,7 +34,7 @@ export function ToastBody({
3434
}) {
3535
return (
3636
<div className={toastBodyFlexStyles}>
37-
<p className={toastBodyTextStyles}>{statusMessage}</p>
37+
<Body className={toastBodyTextStyles}>{statusMessage}</Body>
3838
{!!actionHandler && (
3939
<Link
4040
as="button"
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React from 'react';
2+
import { expect } from 'chai';
3+
import { render, screen, userEvent } from '@mongodb-js/testing-library-compass';
4+
import { DiagramEditorToolbar } from './diagram-editor-toolbar';
5+
import sinon from 'sinon';
6+
7+
function renderDiagramEditorToolbar(
8+
props: Partial<React.ComponentProps<typeof DiagramEditorToolbar>> = {}
9+
) {
10+
render(
11+
<DiagramEditorToolbar
12+
step="EDITING"
13+
hasUndo={true}
14+
hasRedo={true}
15+
onUndoClick={() => {}}
16+
onRedoClick={() => {}}
17+
onExportClick={() => {}}
18+
{...props}
19+
/>
20+
);
21+
}
22+
23+
describe('DiagramEditorToolbar', function () {
24+
it('renders nothing if step is NO_DIAGRAM_SELECTED', function () {
25+
renderDiagramEditorToolbar({ step: 'NO_DIAGRAM_SELECTED' });
26+
expect(() => screen.getByTestId('diagram-editor-toolbar')).to.throw();
27+
});
28+
29+
it('renders nothing if step is not EDITING', function () {
30+
renderDiagramEditorToolbar({ step: 'ANALYSIS_CANCELED' });
31+
expect(() => screen.getByTestId('diagram-editor-toolbar')).to.throw();
32+
});
33+
34+
context('undo button', function () {
35+
it('renders it disabled if hasUndo is false', function () {
36+
renderDiagramEditorToolbar({ hasUndo: false });
37+
const undoButton = screen.getByRole('button', { name: 'Undo' });
38+
expect(undoButton).to.have.attribute('aria-disabled', 'true');
39+
});
40+
it('renders it enabled if hasUndo is true and calls onUndoClick', function () {
41+
const undoSpy = sinon.spy();
42+
renderDiagramEditorToolbar({ hasUndo: true, onUndoClick: undoSpy });
43+
const undoButton = screen.getByRole('button', { name: 'Undo' });
44+
expect(undoButton).to.have.attribute('aria-disabled', 'false');
45+
userEvent.click(undoButton);
46+
expect(undoSpy).to.have.been.calledOnce;
47+
});
48+
});
49+
50+
context('redo button', function () {
51+
it('renders it disabled if hasRedo is false', function () {
52+
renderDiagramEditorToolbar({ hasRedo: false });
53+
const redoButton = screen.getByRole('button', { name: 'Redo' });
54+
expect(redoButton).to.have.attribute('aria-disabled', 'true');
55+
});
56+
it('renders it enabled if hasRedo is true and calls onRedoClick', function () {
57+
const redoSpy = sinon.spy();
58+
renderDiagramEditorToolbar({ hasRedo: true, onRedoClick: redoSpy });
59+
const redoButton = screen.getByRole('button', { name: 'Redo' });
60+
expect(redoButton).to.have.attribute('aria-disabled', 'false');
61+
userEvent.click(redoButton);
62+
expect(redoSpy).to.have.been.calledOnce;
63+
});
64+
});
65+
66+
it('renders export buttona and calls onExportClick', function () {
67+
const exportSpy = sinon.spy();
68+
renderDiagramEditorToolbar({ onExportClick: exportSpy });
69+
const exportButton = screen.getByRole('button', { name: 'Export' });
70+
expect(exportButton).to.exist;
71+
userEvent.click(exportButton);
72+
expect(exportSpy).to.have.been.calledOnce;
73+
});
74+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from 'react';
2+
import { connect } from 'react-redux';
3+
import type { DataModelingState } from '../store/reducer';
4+
import { redoEdit, showExportModal, undoEdit } from '../store/diagram';
5+
import { Icon, IconButton } from '@mongodb-js/compass-components';
6+
7+
export const DiagramEditorToolbar: React.FunctionComponent<{
8+
step: DataModelingState['step'];
9+
hasUndo: boolean;
10+
hasRedo: boolean;
11+
onUndoClick: () => void;
12+
onRedoClick: () => void;
13+
onExportClick: () => void;
14+
}> = ({ step, hasUndo, onUndoClick, hasRedo, onRedoClick, onExportClick }) => {
15+
if (step !== 'EDITING') {
16+
return null;
17+
}
18+
return (
19+
<div data-testid="diagram-editor-toolbar">
20+
<IconButton aria-label="Undo" disabled={!hasUndo} onClick={onUndoClick}>
21+
<Icon glyph="Undo"></Icon>
22+
</IconButton>
23+
<IconButton aria-label="Redo" disabled={!hasRedo} onClick={onRedoClick}>
24+
<Icon glyph="Redo"></Icon>
25+
</IconButton>
26+
<IconButton aria-label="Export" onClick={onExportClick}>
27+
<Icon glyph="Export"></Icon>
28+
</IconButton>
29+
</div>
30+
);
31+
};
32+
33+
export default connect(
34+
(state: DataModelingState) => {
35+
const { diagram, step } = state;
36+
return {
37+
step: step,
38+
hasUndo: (diagram?.edits.prev.length ?? 0) > 0,
39+
hasRedo: (diagram?.edits.next.length ?? 0) > 0,
40+
};
41+
},
42+
{
43+
onUndoClick: undoEdit,
44+
onRedoClick: redoEdit,
45+
onExportClick: showExportModal,
46+
}
47+
)(DiagramEditorToolbar);

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

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,10 @@ import type { DataModelingState } from '../store/reducer';
44
import {
55
applyEdit,
66
getCurrentDiagramFromState,
7-
redoEdit,
87
selectCurrentModel,
9-
undoEdit,
108
} from '../store/diagram';
119
import {
1210
Banner,
13-
Icon,
14-
IconButton,
1511
CancelLoader,
1612
WorkspaceContainer,
1713
css,
@@ -31,6 +27,8 @@ import {
3127
} from '@mongodb-js/diagramming';
3228
import type { Edit, StaticModel } from '../services/data-model-storage';
3329
import { UUID } from 'bson';
30+
import DiagramEditorToolbar from './diagram-editor-toolbar';
31+
import ExportDiagramModal from './export-diagram-modal';
3432

3533
const loadingContainerStyles = css({
3634
width: '100%',
@@ -110,10 +108,6 @@ const editorContainerPlaceholderButtonStyles = css({
110108
const DiagramEditor: React.FunctionComponent<{
111109
diagramLabel: string;
112110
step: DataModelingState['step'];
113-
hasUndo: boolean;
114-
onUndoClick: () => void;
115-
hasRedo: boolean;
116-
onRedoClick: () => void;
117111
model: StaticModel | null;
118112
editErrors?: string[];
119113
onRetryClick: () => void;
@@ -122,10 +116,6 @@ const DiagramEditor: React.FunctionComponent<{
122116
}> = ({
123117
diagramLabel,
124118
step,
125-
hasUndo,
126-
onUndoClick,
127-
hasRedo,
128-
onRedoClick,
129119
model,
130120
editErrors,
131121
onRetryClick,
@@ -345,33 +335,9 @@ const DiagramEditor: React.FunctionComponent<{
345335
}
346336

347337
return (
348-
<WorkspaceContainer
349-
toolbar={() => {
350-
if (step !== 'EDITING') {
351-
return null;
352-
}
353-
354-
return (
355-
<>
356-
<IconButton
357-
aria-label="Undo"
358-
disabled={!hasUndo}
359-
onClick={onUndoClick}
360-
>
361-
<Icon glyph="Undo"></Icon>
362-
</IconButton>
363-
<IconButton
364-
aria-label="Redo"
365-
disabled={!hasRedo}
366-
onClick={onRedoClick}
367-
>
368-
<Icon glyph="Redo"></Icon>
369-
</IconButton>
370-
</>
371-
);
372-
}}
373-
>
338+
<WorkspaceContainer toolbar={<DiagramEditorToolbar />}>
374339
{content}
340+
<ExportDiagramModal />
375341
</WorkspaceContainer>
376342
);
377343
};
@@ -381,8 +347,6 @@ export default connect(
381347
const { diagram, step } = state;
382348
return {
383349
step: step,
384-
hasUndo: (diagram?.edits.prev.length ?? 0) > 0,
385-
hasRedo: (diagram?.edits.next.length ?? 0) > 0,
386350
model: diagram
387351
? selectCurrentModel(getCurrentDiagramFromState(state))
388352
: null,
@@ -391,8 +355,6 @@ export default connect(
391355
};
392356
},
393357
{
394-
onUndoClick: undoEdit,
395-
onRedoClick: redoEdit,
396358
onRetryClick: retryAnalysis,
397359
onCancelClick: cancelAnalysis,
398360
onApplyClick: applyEdit,
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import React, { useCallback, useState } from 'react';
2+
import {
3+
Button,
4+
css,
5+
Icon,
6+
Label,
7+
Link,
8+
Modal,
9+
ModalBody,
10+
ModalFooter,
11+
ModalHeader,
12+
Radio,
13+
RadioGroup,
14+
spacing,
15+
} from '@mongodb-js/compass-components';
16+
import {
17+
closeExportModal,
18+
selectCurrentModel,
19+
getCurrentDiagramFromState,
20+
} from '../store/diagram';
21+
import { connect } from 'react-redux';
22+
import type { DataModelingState } from '../store/reducer';
23+
import type { StaticModel } from '../services/data-model-storage';
24+
import { exportToJson } from '../services/export-diagram';
25+
26+
const nbsp = '\u00a0';
27+
28+
const modelBodyStyles = css({
29+
paddingTop: spacing[600],
30+
});
31+
32+
const contentContainerStyles = css({
33+
display: 'flex',
34+
flexDirection: 'column',
35+
gap: spacing[300],
36+
});
37+
38+
const radioItemStyles = css({
39+
display: 'flex',
40+
alignItems: 'center',
41+
gap: spacing[200],
42+
});
43+
44+
const footerStyles = css({
45+
display: 'flex',
46+
gap: spacing[200],
47+
});
48+
49+
type ExportDiagramModalProps = {
50+
isModalOpen: boolean;
51+
diagramLabel: string;
52+
model: StaticModel | null;
53+
onCloseClick: () => void;
54+
};
55+
56+
const ExportDiagramModal = ({
57+
isModalOpen,
58+
diagramLabel,
59+
model,
60+
onCloseClick,
61+
}: ExportDiagramModalProps) => {
62+
const [exportFormat, setExportFormat] = useState<'json' | null>(null);
63+
64+
const onExport = useCallback(() => {
65+
if (!exportFormat || !model) {
66+
return;
67+
}
68+
exportToJson(diagramLabel, model);
69+
onCloseClick();
70+
}, [exportFormat, onCloseClick, model, diagramLabel]);
71+
72+
return (
73+
<Modal
74+
open={isModalOpen}
75+
setOpen={onCloseClick}
76+
data-testid="export-diagram-modal"
77+
>
78+
<ModalHeader
79+
title="Export data model"
80+
subtitle={
81+
<div>
82+
Export the data modal to JSON format.
83+
{nbsp}
84+
<Link
85+
href="https://www.mongodb.com/docs/manual/data-modeling//"
86+
target="_blank"
87+
rel="noopener noreferrer"
88+
>
89+
Learn more
90+
</Link>
91+
</div>
92+
}
93+
/>
94+
<ModalBody className={modelBodyStyles}>
95+
<div className={contentContainerStyles}>
96+
<Label htmlFor="">Select file format:</Label>
97+
<RadioGroup className={contentContainerStyles} value={exportFormat}>
98+
<div className={radioItemStyles}>
99+
<Icon glyph="CurlyBraces" />
100+
<Radio
101+
checked={exportFormat === 'json'}
102+
value="json"
103+
aria-label="JSON"
104+
onClick={() => setExportFormat('json')}
105+
>
106+
JSON
107+
</Radio>
108+
</div>
109+
</RadioGroup>
110+
</div>
111+
</ModalBody>
112+
<ModalFooter className={footerStyles}>
113+
<Button
114+
variant="primary"
115+
onClick={() => void onExport()}
116+
data-testid="export-button"
117+
>
118+
Export
119+
</Button>
120+
<Button
121+
variant="default"
122+
onClick={onCloseClick}
123+
data-testid="cancel-button"
124+
>
125+
Cancel
126+
</Button>
127+
</ModalFooter>
128+
</Modal>
129+
);
130+
};
131+
132+
export default connect(
133+
(state: DataModelingState) => {
134+
const { diagram } = state;
135+
const model = diagram
136+
? selectCurrentModel(getCurrentDiagramFromState(state))
137+
: null;
138+
return {
139+
model,
140+
diagramLabel: diagram?.name ?? 'Schema Preview',
141+
isModalOpen: Boolean(diagram?.isExportModalOpen),
142+
};
143+
},
144+
{
145+
onCloseClick: closeExportModal,
146+
}
147+
)(ExportDiagramModal);

0 commit comments

Comments
 (0)