Skip to content

Commit 915cb75

Browse files
Anemypaula-stacho
authored andcommitted
feat(data-modeling): add add field button to collection node COMPASS-9697
1 parent 5cb9069 commit 915cb75

File tree

8 files changed

+296
-13
lines changed

8 files changed

+296
-13
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ const mockDiagramming = {
9393
<div data-testid="mock-diagram">
9494
{Object.entries(props).map(([key, value]) => (
9595
<div key={key} data-testid={`diagram-prop-${key}`}>
96-
{JSON.stringify(value)}
96+
{typeof value === 'object' ? 'object' : JSON.stringify(value)}
9797
</div>
9898
))}
9999
</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,
@@ -111,6 +112,7 @@ const DiagramContent: React.FunctionComponent<{
111112
isInRelationshipDrawingMode: boolean;
112113
editErrors?: string[];
113114
newCollection?: string;
115+
onAddNewFieldToCollection: (ns: string) => void;
114116
onMoveCollection: (ns: string, newPosition: [number, number]) => void;
115117
onCollectionSelect: (namespace: string) => void;
116118
onRelationshipSelect: (rId: string) => void;
@@ -130,6 +132,7 @@ const DiagramContent: React.FunctionComponent<{
130132
model,
131133
isInRelationshipDrawingMode,
132134
newCollection,
135+
onAddNewFieldToCollection,
133136
onMoveCollection,
134137
onCollectionSelect,
135138
onRelationshipSelect,
@@ -177,11 +180,14 @@ const DiagramContent: React.FunctionComponent<{
177180
selectedItems?.type === 'field' && selectedItems.namespace === coll.ns
178181
? selectedItems.fieldPath
179182
: undefined,
183+
onClickAddNewFieldToCollection: () =>
184+
onAddNewFieldToCollection(coll.ns),
180185
selected,
181186
isInRelationshipDrawingMode,
182187
});
183188
});
184189
}, [
190+
onAddNewFieldToCollection,
185191
model?.collections,
186192
model?.relationships,
187193
selectedItems,
@@ -301,6 +307,7 @@ const ConnectedDiagramContent = connect(
301307
};
302308
},
303309
{
310+
onAddNewFieldToCollection: addNewFieldToCollection,
304311
onMoveCollection: moveCollection,
305312
onCollectionSelect: selectCollection,
306313
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
@@ -57,6 +57,12 @@ const EditSchemaVariants = z.discriminatedUnion('type', [
5757
type: z.literal('SetModel'),
5858
model: StaticModelSchema,
5959
}),
60+
z.object({
61+
type: z.literal('AddField'),
62+
ns: z.string(),
63+
field: z.array(z.string()),
64+
jsonSchema: z.custom<MongoDBJSONSchema>(),
65+
}),
6066
z.object({
6167
type: z.literal('AddRelationship'),
6268
relationship: RelationshipSchema,

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

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { getCoordinatesForNewNode } from '@mongodb-js/diagramming';
3030
import { collectionToDiagramNode } from '../utils/nodes-and-edges';
3131
import toNS from 'mongodb-ns';
3232
import { traverseSchema } from '../utils/schema-traversal';
33+
import { addFieldToJSONSchema, getNewUnusedFieldName } from '../utils/schema';
3334

3435
function isNonEmptyArray<T>(arr: T[]): arr is [T, ...T[]] {
3536
return Array.isArray(arr) && arr.length > 0;
@@ -474,6 +475,34 @@ export function redoEdit(): DataModelingThunkAction<void, RedoEditAction> {
474475
};
475476
}
476477

478+
export function addNewFieldToCollection(
479+
ns: string
480+
): DataModelingThunkAction<void, ApplyEditAction | ApplyEditFailedAction> {
481+
return (dispatch, getState) => {
482+
const modelState = selectCurrentModelFromState(getState());
483+
484+
const collection = modelState.collections.find((c) => c.ns === ns);
485+
if (!collection) {
486+
throw new Error('Collection to add field to not found');
487+
}
488+
489+
const edit: Omit<
490+
Extract<Edit, { type: 'AddField' }>,
491+
'id' | 'timestamp'
492+
> = {
493+
type: 'AddField',
494+
ns,
495+
// Use the first unique field name we can use.
496+
field: [getNewUnusedFieldName(collection.jsonSchema)],
497+
jsonSchema: {
498+
bsonType: 'string',
499+
},
500+
};
501+
502+
return dispatch(applyEdit(edit));
503+
};
504+
}
505+
477506
export function moveCollection(
478507
ns: string,
479508
newPosition: [number, number]
@@ -496,18 +525,16 @@ export function renameCollection(
496525
void,
497526
ApplyEditAction | ApplyEditFailedAction | CollectionSelectedAction
498527
> {
499-
return (dispatch) => {
500-
const edit: Omit<
501-
Extract<Edit, { type: 'RenameCollection' }>,
502-
'id' | 'timestamp'
503-
> = {
504-
type: 'RenameCollection',
505-
fromNS,
506-
toNS,
507-
};
508-
509-
dispatch(applyEdit(edit));
528+
const edit: Omit<
529+
Extract<Edit, { type: 'RenameCollection' }>,
530+
'id' | 'timestamp'
531+
> = {
532+
type: 'RenameCollection',
533+
fromNS,
534+
toNS,
510535
};
536+
537+
return applyEdit(edit);
511538
}
512539

513540
export function applyEdit(
@@ -752,6 +779,24 @@ function _applyEdit(edit: Edit, model?: StaticModel): StaticModel {
752779
collections: [...model.collections, newCollection],
753780
};
754781
}
782+
case 'AddField': {
783+
return {
784+
...model,
785+
collections: model.collections.map((collection) => {
786+
if (collection.ns === edit.ns) {
787+
return {
788+
...collection,
789+
jsonSchema: addFieldToJSONSchema(
790+
collection.jsonSchema,
791+
edit.field,
792+
edit.jsonSchema
793+
),
794+
};
795+
}
796+
return collection;
797+
}),
798+
};
799+
}
755800
case 'AddRelationship': {
756801
return {
757802
...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,6 +1,11 @@
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';
@@ -11,6 +16,7 @@ import type {
1116
} from '../services/data-model-storage';
1217
import { traverseSchema } from './schema-traversal';
1318
import { areFieldPathsEqual } from './utils';
19+
import PlusWithSquare from '../components/icons/plus-with-square';
1420

1521
function getBsonTypeName(bsonType: string) {
1622
switch (bsonType) {
@@ -21,6 +27,10 @@ function getBsonTypeName(bsonType: string) {
2127
}
2228
}
2329

30+
const addNewFieldStyles = css({
31+
marginLeft: 'auto',
32+
});
33+
2434
const mixedTypeTooltipContentStyles = css({
2535
overflowWrap: 'anywhere',
2636
textWrap: 'wrap',
@@ -121,13 +131,15 @@ export function collectionToDiagramNode(
121131
options: {
122132
highlightedFields?: Record<string, FieldPath[] | undefined>;
123133
selectedField?: FieldPath;
134+
onClickAddNewFieldToCollection?: () => void;
124135
selected?: boolean;
125136
isInRelationshipDrawingMode?: boolean;
126137
} = {}
127138
): NodeProps {
128139
const {
129140
highlightedFields = {},
130141
selectedField,
142+
onClickAddNewFieldToCollection,
131143
selected = false,
132144
isInRelationshipDrawingMode = false,
133145
} = options;
@@ -145,6 +157,19 @@ export function collectionToDiagramNode(
145157
highlightedFields: highlightedFields[coll.ns] ?? undefined,
146158
selectedField,
147159
}),
160+
actions: onClickAddNewFieldToCollection ? (
161+
<IconButton
162+
aria-label="Add Field"
163+
className={addNewFieldStyles}
164+
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
165+
event.stopPropagation();
166+
onClickAddNewFieldToCollection();
167+
}}
168+
title="Add Field"
169+
>
170+
<PlusWithSquare />
171+
</IconButton>
172+
) : undefined,
148173
selected,
149174
connectable: isInRelationshipDrawingMode,
150175
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)