Skip to content

Commit 2102205

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

File tree

8 files changed

+291
-13
lines changed

8 files changed

+291
-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/store/apply-edit.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
Relationship,
55
StaticModel,
66
} from '../services/data-model-storage';
7+
import { addFieldToJSONSchema } from '../utils/schema';
78
import { updateSchema } from '../utils/schema-traversal';
89
import {
910
isRelationshipInvolvingField,
@@ -149,6 +150,24 @@ export function applyEdit(edit: Edit, model?: StaticModel): StaticModel {
149150
}),
150151
};
151152
}
153+
case 'AddField': {
154+
return {
155+
...model,
156+
collections: model.collections.map((collection) => {
157+
if (collection.ns === edit.ns) {
158+
return {
159+
...collection,
160+
jsonSchema: addFieldToJSONSchema(
161+
collection.jsonSchema,
162+
edit.field,
163+
edit.jsonSchema
164+
),
165+
};
166+
}
167+
return collection;
168+
}),
169+
};
170+
}
152171
case 'RemoveField': {
153172
return {
154173
...model,

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

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { collectionToDiagramNode } from '../utils/nodes-and-edges';
3131
import toNS from 'mongodb-ns';
3232
import { traverseSchema } from '../utils/schema-traversal';
3333
import { applyEdit as _applyEdit } from './apply-edit';
34+
import { getNewUnusedFieldName } from '../utils/schema';
3435

3536
function isNonEmptyArray<T>(arr: T[]): arr is [T, ...T[]] {
3637
return Array.isArray(arr) && arr.length > 0;
@@ -485,6 +486,34 @@ export function redoEdit(): DataModelingThunkAction<void, RedoEditAction> {
485486
};
486487
}
487488

489+
export function addNewFieldToCollection(
490+
ns: string
491+
): DataModelingThunkAction<void, ApplyEditAction | ApplyEditFailedAction> {
492+
return (dispatch, getState) => {
493+
const modelState = selectCurrentModelFromState(getState());
494+
495+
const collection = modelState.collections.find((c) => c.ns === ns);
496+
if (!collection) {
497+
throw new Error('Collection to add field to not found');
498+
}
499+
500+
const edit: Omit<
501+
Extract<Edit, { type: 'AddField' }>,
502+
'id' | 'timestamp'
503+
> = {
504+
type: 'AddField',
505+
ns,
506+
// Use the first unique field name we can use.
507+
field: [getNewUnusedFieldName(collection.jsonSchema)],
508+
jsonSchema: {
509+
bsonType: 'string',
510+
},
511+
};
512+
513+
return dispatch(applyEdit(edit));
514+
};
515+
}
516+
488517
export function moveCollection(
489518
ns: string,
490519
newPosition: [number, number]
@@ -507,18 +536,16 @@ export function renameCollection(
507536
void,
508537
ApplyEditAction | ApplyEditFailedAction | CollectionSelectedAction
509538
> {
510-
return (dispatch) => {
511-
const edit: Omit<
512-
Extract<Edit, { type: 'RenameCollection' }>,
513-
'id' | 'timestamp'
514-
> = {
515-
type: 'RenameCollection',
516-
fromNS,
517-
toNS,
518-
};
519-
520-
dispatch(applyEdit(edit));
539+
const edit: Omit<
540+
Extract<Edit, { type: 'RenameCollection' }>,
541+
'id' | 'timestamp'
542+
> = {
543+
type: 'RenameCollection',
544+
fromNS,
545+
toNS,
521546
};
547+
548+
return applyEdit(edit);
522549
}
523550

524551
export function applyEdit(

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)