Skip to content

Commit 65c5085

Browse files
committed
feat(data-modeling): add add field button to collection node COMPASS-9697
1 parent 8f183d0 commit 65c5085

File tree

10 files changed

+577
-274
lines changed

10 files changed

+577
-274
lines changed

package-lock.json

Lines changed: 280 additions & 260 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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
"@mongodb-js/compass-user-data": "^0.9.0",
6464
"@mongodb-js/compass-utils": "^0.9.10",
6565
"@mongodb-js/compass-workspaces": "^0.51.0",
66-
"@mongodb-js/diagramming": "^1.3.3",
66+
"@mongodb-js/diagramming": "^1.5.0",
6767
"bson": "^6.10.4",
6868
"compass-preferences-model": "^2.50.0",
6969
"html-to-image": "1.11.11",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ const mockDiagramming = {
9292
<div data-testid="mock-diagram">
9393
{Object.entries(props).map(([key, value]) => (
9494
<div key={key} data-testid={`diagram-prop-${key}`}>
95-
{JSON.stringify(value)}
95+
{typeof value === 'object' ? 'object' : JSON.stringify(value)}
9696
</div>
9797
))}
9898
</div>

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import React, {
88
import { connect } from 'react-redux';
99
import type { DataModelingState } from '../store/reducer';
1010
import {
11+
addNewFieldToCollection,
1112
moveCollection,
1213
selectCollection,
1314
selectRelationship,
@@ -107,6 +108,7 @@ const DiagramContent: React.FunctionComponent<{
107108
model: StaticModel | null;
108109
isInRelationshipDrawingMode: boolean;
109110
editErrors?: string[];
111+
onAddNewFieldToCollection: (ns: string) => void;
110112
onMoveCollection: (ns: string, newPosition: [number, number]) => void;
111113
onCollectionSelect: (namespace: string) => void;
112114
onRelationshipSelect: (rId: string) => void;
@@ -118,6 +120,7 @@ const DiagramContent: React.FunctionComponent<{
118120
diagramLabel,
119121
model,
120122
isInRelationshipDrawingMode,
123+
onAddNewFieldToCollection,
121124
onMoveCollection,
122125
onCollectionSelect,
123126
onRelationshipSelect,
@@ -158,12 +161,15 @@ const DiagramContent: React.FunctionComponent<{
158161
selectedItems.type === 'collection' &&
159162
selectedItems.id === coll.ns;
160163
return collectionToDiagramNode(coll, {
164+
onClickAddNewFieldToCollection: () =>
165+
onAddNewFieldToCollection(coll.ns),
161166
selectedFields,
162167
selected,
163168
isInRelationshipDrawingMode,
164169
});
165170
});
166171
}, [
172+
onAddNewFieldToCollection,
167173
model?.collections,
168174
model?.relationships,
169175
selectedItems,
@@ -244,6 +250,7 @@ const ConnectedDiagramContent = connect(
244250
};
245251
},
246252
{
253+
onAddNewFieldToCollection: addNewFieldToCollection,
247254
onMoveCollection: moveCollection,
248255
onCollectionSelect: selectCollection,
249256
onRelationshipSelect: selectRelationship,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React, { useMemo } from 'react';
2+
import { palette, useDarkMode } from '@mongodb-js/compass-components';
3+
4+
const PlusWithSquare: React.FunctionComponent = () => {
5+
const darkMode = useDarkMode();
6+
const strokeColor = useMemo(
7+
() => (darkMode ? palette.gray.light1 : palette.gray.dark1),
8+
[darkMode]
9+
);
10+
11+
return (
12+
<svg
13+
width="14"
14+
height="14"
15+
viewBox="0 0 14 14"
16+
fill="none"
17+
xmlns="http://www.w3.org/2000/svg"
18+
>
19+
<path
20+
d="M12 0.75H2C1.66848 0.75 1.35054 0.881696 1.11612 1.11612C0.881696 1.35054 0.75 1.66848 0.75 2V12C0.75 12.3315 0.881696 12.6495 1.11612 12.8839C1.35054 13.1183 1.66848 13.25 2 13.25H12C12.3315 13.25 12.6495 13.1183 12.8839 12.8839C13.1183 12.6495 13.25 12.3315 13.25 12V2C13.25 1.66848 13.1183 1.35054 12.8839 1.11612C12.6495 0.881696 12.3315 0.75 12 0.75ZM11.75 11.75H2.25V2.25H11.75V11.75ZM3.75 7C3.75 6.80109 3.82902 6.61032 3.96967 6.46967C4.11032 6.32902 4.30109 6.25 4.5 6.25H6.25V4.5C6.25 4.30109 6.32902 4.11032 6.46967 3.96967C6.61032 3.82902 6.80109 3.75 7 3.75C7.19891 3.75 7.38968 3.82902 7.53033 3.96967C7.67098 4.11032 7.75 4.30109 7.75 4.5V6.25H9.5C9.69891 6.25 9.88968 6.32902 10.0303 6.46967C10.171 6.61032 10.25 6.80109 10.25 7C10.25 7.19891 10.171 7.38968 10.0303 7.53033C9.88968 7.67098 9.69891 7.75 9.5 7.75H7.75V9.5C7.75 9.69891 7.67098 9.88968 7.53033 10.0303C7.38968 10.171 7.19891 10.25 7 10.25C6.80109 10.25 6.61032 10.171 6.46967 10.0303C6.32902 9.88968 6.25 9.69891 6.25 9.5V7.75H4.5C4.30109 7.75 4.11032 7.67098 3.96967 7.53033C3.82902 7.38968 3.75 7.19891 3.75 7Z"
21+
fill={strokeColor}
22+
/>
23+
</svg>
24+
);
25+
};
26+
27+
export default PlusWithSquare;

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ const EditSchemaVariants = z.discriminatedUnion('type', [
4949
type: z.literal('SetModel'),
5050
model: StaticModelSchema,
5151
}),
52+
z.object({
53+
type: z.literal('AddField'),
54+
ns: z.string(),
55+
field: z.array(z.string()),
56+
jsonSchema: z.custom<MongoDBJSONSchema>(),
57+
}),
5258
z.object({
5359
type: z.literal('AddRelationship'),
5460
relationship: RelationshipSchema,

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

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
getDiagramName,
2222
} from '../services/open-and-download-diagram';
2323
import type { MongoDBJSONSchema } from 'mongodb-schema';
24+
import { addFieldToJSONSchema, getNewUnusedFieldName } from '../utils/schema';
2425

2526
function isNonEmptyArray<T>(arr: T[]): arr is [T, ...T[]] {
2627
return Array.isArray(arr) && arr.length > 0;
@@ -354,6 +355,34 @@ export function redoEdit(): DataModelingThunkAction<void, RedoEditAction> {
354355
};
355356
}
356357

358+
export function addNewFieldToCollection(
359+
ns: string
360+
): DataModelingThunkAction<void, ApplyEditAction | ApplyEditFailedAction> {
361+
return (dispatch, getState) => {
362+
const modelState = selectCurrentModelFromState(getState());
363+
364+
const collection = modelState.collections.find((c) => c.ns === ns);
365+
if (!collection) {
366+
throw new Error('Collection to add field to not found');
367+
}
368+
369+
const edit: Omit<
370+
Extract<Edit, { type: 'AddField' }>,
371+
'id' | 'timestamp'
372+
> = {
373+
type: 'AddField',
374+
ns,
375+
// Use the first unique field name we can use.
376+
field: [getNewUnusedFieldName(collection.jsonSchema)],
377+
jsonSchema: {
378+
bsonType: 'string',
379+
},
380+
};
381+
382+
return dispatch(applyEdit(edit));
383+
};
384+
}
385+
357386
export function moveCollection(
358387
ns: string,
359388
newPosition: [number, number]
@@ -376,18 +405,16 @@ export function renameCollection(
376405
void,
377406
ApplyEditAction | ApplyEditFailedAction | CollectionSelectedAction
378407
> {
379-
return (dispatch) => {
380-
const edit: Omit<
381-
Extract<Edit, { type: 'RenameCollection' }>,
382-
'id' | 'timestamp'
383-
> = {
384-
type: 'RenameCollection',
385-
fromNS,
386-
toNS,
387-
};
388-
389-
dispatch(applyEdit(edit));
408+
const edit: Omit<
409+
Extract<Edit, { type: 'RenameCollection' }>,
410+
'id' | 'timestamp'
411+
> = {
412+
type: 'RenameCollection',
413+
fromNS,
414+
toNS,
390415
};
416+
417+
return applyEdit(edit);
391418
}
392419

393420
export function applyEdit(
@@ -546,6 +573,24 @@ function _applyEdit(edit: Edit, model?: StaticModel): StaticModel {
546573
throw new Error('Editing a model that has not been initialized');
547574
}
548575
switch (edit.type) {
576+
case 'AddField': {
577+
return {
578+
...model,
579+
collections: model.collections.map((collection) => {
580+
if (collection.ns === edit.ns) {
581+
return {
582+
...collection,
583+
jsonSchema: addFieldToJSONSchema(
584+
collection.jsonSchema,
585+
edit.field,
586+
edit.jsonSchema
587+
),
588+
};
589+
}
590+
return collection;
591+
}),
592+
};
593+
}
549594
case 'AddRelationship': {
550595
return {
551596
...model,

packages/compass-data-modeling/src/utils/nodes-and-edges.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import React from 'react';
22
import toNS from 'mongodb-ns';
3-
import { InlineDefinition, Body, css } from '@mongodb-js/compass-components';
3+
import {
4+
Body,
5+
IconButton,
6+
InlineDefinition,
7+
css,
8+
} from '@mongodb-js/compass-components';
49
import type { NodeProps, EdgeProps } from '@mongodb-js/diagramming';
510
import type { MongoDBJSONSchema } from 'mongodb-schema';
611
import type { SelectedItems } from '../store/diagram';
712
import type {
813
DataModelCollection,
914
Relationship,
1015
} from '../services/data-model-storage';
16+
import PlusWithSquare from '../components/icons/plus-with-square';
1117

1218
function getBsonTypeName(bsonType: string) {
1319
switch (bsonType) {
@@ -18,6 +24,10 @@ function getBsonTypeName(bsonType: string) {
1824
}
1925
}
2026

27+
const addNewFieldStyles = css({
28+
marginLeft: 'auto',
29+
});
30+
2131
const mixedTypeTooltipContentStyles = css({
2232
overflowWrap: 'anywhere',
2333
textWrap: 'wrap',
@@ -145,12 +155,14 @@ export const getFieldsFromSchema = (
145155
export function collectionToDiagramNode(
146156
coll: Pick<DataModelCollection, 'ns' | 'jsonSchema' | 'displayPosition'>,
147157
options: {
158+
onClickAddNewFieldToCollection?: () => void;
148159
selectedFields?: Record<string, string[][] | undefined>;
149160
selected?: boolean;
150161
isInRelationshipDrawingMode?: boolean;
151162
} = {}
152163
): NodeProps {
153164
const {
165+
onClickAddNewFieldToCollection,
154166
selectedFields = {},
155167
selected = false,
156168
isInRelationshipDrawingMode = false,
@@ -169,6 +181,19 @@ export function collectionToDiagramNode(
169181
selectedFields[coll.ns] ?? undefined,
170182
0
171183
),
184+
actions: onClickAddNewFieldToCollection ? (
185+
<IconButton
186+
aria-label="Add Field"
187+
className={addNewFieldStyles}
188+
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
189+
event.stopPropagation();
190+
onClickAddNewFieldToCollection();
191+
}}
192+
title="Add Field"
193+
>
194+
<PlusWithSquare />
195+
</IconButton>
196+
) : undefined,
172197
selected,
173198
connectable: isInRelationshipDrawingMode,
174199
draggable: !isInRelationshipDrawingMode,
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { expect } from 'chai';
2+
import { addFieldToJSONSchema, getNewUnusedFieldName } from './schema';
3+
4+
describe('schema diagram utils', function () {
5+
describe('#getNewUnusedFieldName', function () {
6+
it('should return a new unused field name', function () {
7+
const jsonSchema = {
8+
bsonType: 'object',
9+
properties: {
10+
a: {
11+
bsonType: 'string',
12+
},
13+
b: {
14+
bsonType: 'string',
15+
},
16+
},
17+
};
18+
const newFieldName = getNewUnusedFieldName(jsonSchema);
19+
expect(newFieldName).to.equal('field-1');
20+
});
21+
22+
it('should return a new unused field name when there are conflicts', function () {
23+
const jsonSchema = {
24+
bsonType: 'object',
25+
properties: {
26+
'field-1': {
27+
bsonType: 'string',
28+
},
29+
'field-2': {
30+
bsonType: 'string',
31+
},
32+
},
33+
};
34+
const newFieldName = getNewUnusedFieldName(jsonSchema);
35+
expect(newFieldName).to.equal('field-3');
36+
});
37+
});
38+
39+
describe('#addFieldToJSONSchema', function () {
40+
it('should add a field to the root of the schema', function () {
41+
const jsonSchema = {
42+
bsonType: 'object',
43+
properties: {
44+
a: {
45+
bsonType: 'string',
46+
},
47+
b: {
48+
bsonType: 'string',
49+
},
50+
},
51+
};
52+
const newFieldSchema = {
53+
bsonType: 'string',
54+
};
55+
const newJsonSchema = addFieldToJSONSchema(
56+
jsonSchema,
57+
['c'],
58+
newFieldSchema
59+
);
60+
expect(newJsonSchema).to.deep.equal({
61+
bsonType: 'object',
62+
properties: {
63+
a: {
64+
bsonType: 'string',
65+
},
66+
b: {
67+
bsonType: 'string',
68+
},
69+
c: {
70+
bsonType: 'string',
71+
},
72+
},
73+
});
74+
});
75+
76+
it('should add a field to a nested object in the schema', function () {
77+
const jsonSchema = {
78+
bsonType: 'object',
79+
properties: {
80+
a: {
81+
bsonType: 'string',
82+
},
83+
b: {
84+
bsonType: 'object',
85+
properties: {
86+
c: {
87+
bsonType: 'string',
88+
},
89+
},
90+
},
91+
},
92+
};
93+
const newFieldSchema = {
94+
bsonType: 'string',
95+
};
96+
const newJsonSchema = addFieldToJSONSchema(
97+
jsonSchema,
98+
['b', 'd'],
99+
newFieldSchema
100+
);
101+
expect(newJsonSchema).to.deep.equal({
102+
bsonType: 'object',
103+
properties: {
104+
a: {
105+
bsonType: 'string',
106+
},
107+
b: {
108+
bsonType: 'object',
109+
properties: {
110+
c: {
111+
bsonType: 'string',
112+
},
113+
d: {
114+
bsonType: 'string',
115+
},
116+
},
117+
},
118+
},
119+
});
120+
});
121+
});
122+
});

0 commit comments

Comments
 (0)