Skip to content

Commit d1c1078

Browse files
authored
feat(data-modeling): add add field button to collection node COMPASS-9697 (#7221)
1 parent f1a9036 commit d1c1078

File tree

10 files changed

+381
-52
lines changed

10 files changed

+381
-52
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: 9 additions & 1 deletion
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,
@@ -171,17 +174,21 @@ const DiagramContent: React.FunctionComponent<{
171174
!!selectedItems &&
172175
selectedItems.type === 'collection' &&
173176
selectedItems.id === coll.ns;
174-
return collectionToDiagramNode(coll, {
177+
return collectionToDiagramNode({
178+
...coll,
175179
highlightedFields,
176180
selectedField:
177181
selectedItems?.type === 'field' && selectedItems.namespace === coll.ns
178182
? selectedItems.fieldPath
179183
: undefined,
184+
onClickAddNewFieldToCollection: () =>
185+
onAddNewFieldToCollection(coll.ns),
180186
selected,
181187
isInRelationshipDrawingMode,
182188
});
183189
});
184190
}, [
191+
onAddNewFieldToCollection,
185192
model?.collections,
186193
model?.relationships,
187194
selectedItems,
@@ -301,6 +308,7 @@ const ConnectedDiagramContent = connect(
301308
};
302309
},
303310
{
311+
onAddNewFieldToCollection: addNewFieldToCollection,
304312
onMoveCollection: moveCollection,
305313
onCollectionSelect: selectCollection,
306314
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/analysis-process.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { Document } from 'bson';
77
import type { AggregationCursor } from 'mongodb';
88
import type { Relationship } from '../services/data-model-storage';
99
import { applyLayout } from '@mongodb-js/diagramming';
10-
import { collectionToDiagramNode } from '../utils/nodes-and-edges';
10+
import { collectionToBaseNodeForLayout } from '../utils/nodes-and-edges';
1111

1212
export type AnalysisProcessState = {
1313
currentAnalysisOptions:
@@ -207,13 +207,13 @@ export function startAnalysis(
207207
}
208208

209209
const positioned = await applyLayout(
210-
collections.map((coll) => {
211-
return collectionToDiagramNode({
210+
collections.map((coll) =>
211+
collectionToBaseNodeForLayout({
212212
ns: coll.ns,
213213
jsonSchema: coll.schema,
214214
displayPosition: [0, 0],
215-
});
216-
}),
215+
})
216+
),
217217
[],
218218
'LEFT_RIGHT'
219219
);

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: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ import {
2727
} from '../services/open-and-download-diagram';
2828
import type { MongoDBJSONSchema } from 'mongodb-schema';
2929
import { getCoordinatesForNewNode } from '@mongodb-js/diagramming';
30-
import { collectionToDiagramNode } from '../utils/nodes-and-edges';
30+
import { collectionToBaseNodeForLayout } 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;
@@ -394,6 +395,13 @@ const updateSelectedItemsFromAppliedEdit = (
394395
],
395396
};
396397
}
398+
case 'AddField': {
399+
return {
400+
type: 'field',
401+
namespace: edit.ns,
402+
fieldPath: edit.field,
403+
};
404+
}
397405
}
398406

399407
return currentSelection;
@@ -485,6 +493,34 @@ export function redoEdit(): DataModelingThunkAction<void, RedoEditAction> {
485493
};
486494
}
487495

496+
export function addNewFieldToCollection(
497+
ns: string
498+
): DataModelingThunkAction<void, ApplyEditAction | ApplyEditFailedAction> {
499+
return (dispatch, getState) => {
500+
const modelState = selectCurrentModelFromState(getState());
501+
502+
const collection = modelState.collections.find((c) => c.ns === ns);
503+
if (!collection) {
504+
throw new Error('Collection to add field to not found');
505+
}
506+
507+
const edit: Omit<
508+
Extract<Edit, { type: 'AddField' }>,
509+
'id' | 'timestamp'
510+
> = {
511+
type: 'AddField',
512+
ns,
513+
// Use the first unique field name we can use.
514+
field: [getNewUnusedFieldName(collection.jsonSchema)],
515+
jsonSchema: {
516+
bsonType: 'string',
517+
},
518+
};
519+
520+
return dispatch(applyEdit(edit));
521+
};
522+
}
523+
488524
export function moveCollection(
489525
ns: string,
490526
newPosition: [number, number]
@@ -507,18 +543,16 @@ export function renameCollection(
507543
void,
508544
ApplyEditAction | ApplyEditFailedAction | CollectionSelectedAction
509545
> {
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));
546+
const edit: Omit<
547+
Extract<Edit, { type: 'RenameCollection' }>,
548+
'id' | 'timestamp'
549+
> = {
550+
type: 'RenameCollection',
551+
fromNS,
552+
toNS,
521553
};
554+
555+
return applyEdit(edit);
522556
}
523557

524558
export function applyEdit(
@@ -689,9 +723,9 @@ function getPositionForNewCollection(
689723
newCollection: Omit<DataModelCollection, 'displayPosition'>
690724
): [number, number] {
691725
const existingNodes = existingCollections.map((collection) =>
692-
collectionToDiagramNode(collection)
726+
collectionToBaseNodeForLayout(collection)
693727
);
694-
const newNode = collectionToDiagramNode({
728+
const newNode = collectionToBaseNodeForLayout({
695729
ns: newCollection.ns,
696730
jsonSchema: newCollection.jsonSchema,
697731
displayPosition: [0, 0],

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

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import React from 'react';
22
import toNS from 'mongodb-ns';
3-
import { InlineDefinition, Body, css } from '@mongodb-js/compass-components';
4-
import type { NodeProps, EdgeProps } from '@mongodb-js/diagramming';
3+
import {
4+
Body,
5+
IconButton,
6+
InlineDefinition,
7+
css,
8+
} from '@mongodb-js/compass-components';
9+
import type { NodeProps, EdgeProps, BaseNode } from '@mongodb-js/diagramming';
510
import type { MongoDBJSONSchema } from 'mongodb-schema';
611
import type { SelectedItems } from '../store/diagram';
712
import type {
@@ -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',
@@ -116,38 +126,77 @@ export const getFieldsFromSchema = ({
116126
return fields;
117127
};
118128

119-
export function collectionToDiagramNode(
120-
coll: Pick<DataModelCollection, 'ns' | 'jsonSchema' | 'displayPosition'>,
121-
options: {
122-
highlightedFields?: Record<string, FieldPath[] | undefined>;
123-
selectedField?: FieldPath;
124-
selected?: boolean;
125-
isInRelationshipDrawingMode?: boolean;
126-
} = {}
127-
): NodeProps {
128-
const {
129-
highlightedFields = {},
130-
selectedField,
131-
selected = false,
132-
isInRelationshipDrawingMode = false,
133-
} = options;
129+
/**
130+
* Create a base node to be used for positioning and measuring in node layouts.
131+
*/
132+
export function collectionToBaseNodeForLayout({
133+
ns,
134+
jsonSchema,
135+
displayPosition,
136+
}: Pick<
137+
DataModelCollection,
138+
'ns' | 'jsonSchema' | 'displayPosition'
139+
>): BaseNode & Pick<NodeProps, 'fields'> {
140+
return {
141+
id: ns,
142+
position: {
143+
x: displayPosition[0],
144+
y: displayPosition[1],
145+
},
146+
fields: getFieldsFromSchema({ jsonSchema }),
147+
};
148+
}
134149

150+
type CollectionWithRenderOptions = Pick<
151+
DataModelCollection,
152+
'ns' | 'jsonSchema' | 'displayPosition'
153+
> & {
154+
highlightedFields: Record<string, FieldPath[] | undefined>;
155+
selectedField?: FieldPath;
156+
selected: boolean;
157+
isInRelationshipDrawingMode: boolean;
158+
onClickAddNewFieldToCollection: () => void;
159+
};
160+
161+
export function collectionToDiagramNode({
162+
ns,
163+
jsonSchema,
164+
displayPosition,
165+
selectedField,
166+
highlightedFields,
167+
selected,
168+
isInRelationshipDrawingMode,
169+
onClickAddNewFieldToCollection,
170+
}: CollectionWithRenderOptions): NodeProps {
135171
return {
136-
id: coll.ns,
172+
id: ns,
137173
type: 'collection',
138174
position: {
139-
x: coll.displayPosition[0],
140-
y: coll.displayPosition[1],
175+
x: displayPosition[0],
176+
y: displayPosition[1],
141177
},
142-
title: toNS(coll.ns).collection,
178+
title: toNS(ns).collection,
143179
fields: getFieldsFromSchema({
144-
jsonSchema: coll.jsonSchema,
145-
highlightedFields: highlightedFields[coll.ns] ?? undefined,
180+
jsonSchema: jsonSchema,
181+
highlightedFields: highlightedFields[ns] ?? undefined,
146182
selectedField,
147183
}),
148184
selected,
149185
connectable: isInRelationshipDrawingMode,
150186
draggable: !isInRelationshipDrawingMode,
187+
actions: onClickAddNewFieldToCollection ? (
188+
<IconButton
189+
aria-label="Add Field"
190+
className={addNewFieldStyles}
191+
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
192+
event.stopPropagation();
193+
onClickAddNewFieldToCollection();
194+
}}
195+
title="Add Field"
196+
>
197+
<PlusWithSquare />
198+
</IconButton>
199+
) : undefined,
151200
};
152201
}
153202

0 commit comments

Comments
 (0)