Skip to content

Commit a00c30f

Browse files
authored
feat(compass-data-modeling): delete and rename field COMPASS-9659 (#7237)
1 parent fd8a388 commit a00c30f

File tree

10 files changed

+1235
-181
lines changed

10 files changed

+1235
-181
lines changed

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

Lines changed: 97 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ const waitForDrawerToOpen = async () => {
3636
});
3737
};
3838

39+
const updateInputWithBlur = (label: string, text: string) => {
40+
const input = screen.getByLabelText(label);
41+
userEvent.clear(input);
42+
if (text.length) userEvent.type(input, text);
43+
44+
// Blur/unfocus the input.
45+
userEvent.click(document.body);
46+
};
47+
3948
async function comboboxSelectItem(
4049
label: string,
4150
value: string,
@@ -136,8 +145,8 @@ describe('DiagramEditorSidePanel', function () {
136145
).to.be.visible;
137146
});
138147

139-
describe('Field context drawer', function () {
140-
it('should render a field context drawer when field is clicked', async function () {
148+
describe('When a field is selected', function () {
149+
it('should render a field context drawer', async function () {
141150
const result = renderDrawer();
142151
result.plugin.store.dispatch(selectField('flights.airlines', ['alias']));
143152

@@ -154,7 +163,7 @@ describe('DiagramEditorSidePanel', function () {
154163
expect(selectedTypes).to.include('int');
155164
});
156165

157-
it('should render a nested field context drawer when field is clicked', async function () {
166+
it('should render a nested field context drawer', async function () {
158167
const result = renderDrawer();
159168
result.plugin.store.dispatch(
160169
selectField('flights.routes', ['airline', 'id'])
@@ -171,6 +180,87 @@ describe('DiagramEditorSidePanel', function () {
171180
expect(selectedTypes).to.have.lengthOf(1);
172181
expect(selectedTypes).to.include('string');
173182
});
183+
184+
it('should delete a field', async function () {
185+
const result = renderDrawer();
186+
result.plugin.store.dispatch(
187+
selectField('flights.routes', ['airline', 'id'])
188+
);
189+
190+
await waitForDrawerToOpen();
191+
expect(screen.getByTitle('routes.airline.id')).to.be.visible;
192+
193+
userEvent.click(screen.getByLabelText(/delete field/i));
194+
195+
await waitFor(() => {
196+
expect(screen.queryByText('routes.airline.id')).not.to.exist;
197+
});
198+
expect(screen.queryByLabelText('Name')).to.not.exist;
199+
200+
const modifiedCollection = selectCurrentModelFromState(
201+
result.plugin.store.getState()
202+
).collections.find((coll) => {
203+
return coll.ns === 'flights.routes';
204+
});
205+
206+
expect(
207+
modifiedCollection?.jsonSchema.properties?.airline.properties
208+
).to.not.have.property('id'); // deleted field
209+
expect(
210+
modifiedCollection?.jsonSchema.properties?.airline.properties
211+
).to.have.property('name'); // sibling field remains
212+
});
213+
214+
it('should rename a field and keep it selected', async function () {
215+
const result = renderDrawer();
216+
result.plugin.store.dispatch(
217+
selectField('flights.routes', ['airline', 'name'])
218+
);
219+
220+
await waitForDrawerToOpen();
221+
expect(screen.getByTitle('routes.airline.name')).to.be.visible;
222+
223+
updateInputWithBlur('Field name', 'new_name');
224+
225+
await waitFor(() => {
226+
expect(screen.queryByText('routes.airline.name')).not.to.exist;
227+
expect(screen.queryByText('routes.airline.new_name')).to.exist;
228+
});
229+
});
230+
231+
it('should not rename a field to an empty string', async function () {
232+
const result = renderDrawer();
233+
result.plugin.store.dispatch(
234+
selectField('flights.routes', ['airline', 'name'])
235+
);
236+
237+
await waitForDrawerToOpen();
238+
expect(screen.getByTitle('routes.airline.name')).to.be.visible;
239+
240+
updateInputWithBlur('Field name', '');
241+
242+
await waitFor(() => {
243+
expect(screen.queryByText('Field name cannot be empty.')).to.exist;
244+
expect(screen.queryByText('routes.airline.name')).to.exist;
245+
});
246+
});
247+
248+
it('should not rename a field to a duplicate', async function () {
249+
const result = renderDrawer();
250+
result.plugin.store.dispatch(
251+
selectField('flights.routes', ['airline', 'name'])
252+
);
253+
254+
await waitForDrawerToOpen();
255+
expect(screen.getByTitle('routes.airline.name')).to.be.visible;
256+
257+
updateInputWithBlur('Field name', 'id');
258+
259+
await waitFor(() => {
260+
expect(screen.queryByText('Field already exists.')).to.exist;
261+
expect(screen.queryByText('routes.airline.name')).to.exist;
262+
});
263+
});
174264
});
175265

176266
it('should change the content of the drawer when selecting different items', async function () {
@@ -456,11 +546,7 @@ describe('DiagramEditorSidePanel', function () {
456546
expect(screen.getByLabelText('Name')).to.have.value('countries');
457547

458548
// Update the name.
459-
userEvent.clear(screen.getByLabelText('Name'));
460-
userEvent.type(screen.getByLabelText('Name'), 'pineapple');
461-
462-
// Blur/unfocus the input.
463-
userEvent.click(document.body);
549+
updateInputWithBlur('Name', 'pineapple');
464550

465551
// Check the name in the model.
466552
const modifiedCollection = selectCurrentModelFromState(
@@ -486,11 +572,7 @@ describe('DiagramEditorSidePanel', function () {
486572
expect(activeElement).to.equal(nameInput);
487573

488574
// Update the name.
489-
userEvent.clear(nameInput);
490-
userEvent.type(nameInput, 'pineapple');
491-
492-
// Blur/unfocus the input - now the collection should be names
493-
userEvent.click(document.body);
575+
updateInputWithBlur('Name', 'pineapple');
494576

495577
// Check the name in the model.
496578
const newCollection = selectCurrentModelFromState(
@@ -516,7 +598,7 @@ describe('DiagramEditorSidePanel', function () {
516598
'false'
517599
);
518600

519-
userEvent.clear(screen.getByLabelText('Name'));
601+
updateInputWithBlur('Name', '');
520602

521603
await waitFor(() => {
522604
expect(screen.getByLabelText('Name')).to.have.attribute(
@@ -525,9 +607,6 @@ describe('DiagramEditorSidePanel', function () {
525607
);
526608
});
527609

528-
// Blur/unfocus the input.
529-
userEvent.click(document.body);
530-
531610
const notModifiedCollection = selectCurrentModelFromState(
532611
result.plugin.store.getState()
533612
).collections.find((c: DataModelCollection) => {
@@ -549,8 +628,7 @@ describe('DiagramEditorSidePanel', function () {
549628
'false'
550629
);
551630

552-
userEvent.clear(screen.getByLabelText('Name'));
553-
userEvent.type(screen.getByLabelText('Name'), 'airlines');
631+
updateInputWithBlur('Name', 'airlines');
554632

555633
await waitFor(() => {
556634
expect(screen.getByLabelText('Name')).to.have.attribute(
@@ -559,9 +637,6 @@ describe('DiagramEditorSidePanel', function () {
559637
);
560638
});
561639

562-
// Blur/unfocus the input.
563-
userEvent.click(document.body);
564-
565640
const notModifiedCollection = selectCurrentModelFromState(
566641
result.plugin.store.getState()
567642
).collections.find((c: DataModelCollection) => {

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import RelationshipDrawerContent from './relationship-drawer-content';
1212
import {
1313
deleteCollection,
1414
deleteRelationship,
15+
removeField,
1516
selectCurrentModelFromState,
1617
type SelectedItems,
1718
} from '../../store/diagram';
1819
import { getDefaultRelationshipName } from '../../utils';
1920
import FieldDrawerContent from './field-drawer-content';
2021
import type { FieldPath } from '../../services/data-model-storage';
2122
import { getFieldFromSchema } from '../../utils/schema-traversal';
23+
import { isIdField } from '../../utils/utils';
2224

2325
export const DATA_MODELING_DRAWER_ID = 'data-modeling-drawer';
2426

@@ -109,7 +111,15 @@ function DiagramEditorSidePanel({
109111
></FieldDrawerContent>
110112
),
111113
actions: [
112-
{ action: 'delete', label: 'Delete', icon: 'Trash' as const },
114+
...(!isIdField(selectedItems.fieldPath)
115+
? [
116+
{
117+
action: 'delete',
118+
label: 'Delete Field',
119+
icon: 'Trash' as const,
120+
},
121+
]
122+
: []),
113123
],
114124
handleAction: (actionName: string) => {
115125
if (actionName === 'delete') {
@@ -241,6 +251,6 @@ export default connect(
241251
{
242252
onDeleteCollection: deleteCollection,
243253
onDeleteRelationship: deleteRelationship,
244-
onDeleteField: () => {}, // TODO(COMPASS-9659) part 2 - implement onDeleteField,
254+
onDeleteField: removeField,
245255
}
246256
)(DiagramEditorSidePanel);

packages/compass-data-modeling/src/components/drawer/field-drawer-content.tsx

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import { BSONType } from 'mongodb';
1313
import {
1414
createNewRelationship,
1515
deleteRelationship,
16-
selectCurrentModelFromState,
16+
getCurrentDiagramFromState,
17+
renameField,
18+
selectCurrentModel,
19+
selectFieldsForCurrentModel,
1720
selectRelationship,
1821
} from '../../store/diagram';
1922
import type { DataModelingState } from '../../store/reducer';
@@ -24,7 +27,11 @@ import {
2427
import { useChangeOnBlur } from './use-change-on-blur';
2528
import { RelationshipsSection } from './relationships-section';
2629
import { getFieldFromSchema } from '../../utils/schema-traversal';
27-
import { areFieldPathsEqual } from '../../utils/utils';
30+
import {
31+
areFieldPathsEqual,
32+
isIdField,
33+
isRelationshipOfAField,
34+
} from '../../utils/utils';
2835

2936
type FieldDrawerContentProps = {
3037
namespace: string;
@@ -44,7 +51,7 @@ type FieldDrawerContentProps = {
4451
onRenameField: (
4552
namespace: string,
4653
fromFieldPath: FieldPath,
47-
toFieldPath: FieldPath
54+
newName: string
4855
) => void;
4956
onChangeFieldType: (
5057
namespace: string,
@@ -72,11 +79,23 @@ export function getIsFieldNameValid(
7279
};
7380
}
7481

75-
const fieldsNamesWithoutCurrent = existingFields
76-
.filter((fieldPath) => !areFieldPathsEqual(fieldPath, currentFieldPath))
82+
const siblingFields = existingFields
83+
.filter(
84+
(fieldPath) =>
85+
// same level
86+
fieldPath.length === currentFieldPath.length &&
87+
// same path to that level
88+
areFieldPathsEqual(
89+
fieldPath.slice(0, fieldPath.length - 1),
90+
currentFieldPath.slice(0, fieldPath.length - 1)
91+
) &&
92+
// not the same field
93+
fieldPath[fieldPath.length - 1] !==
94+
currentFieldPath[currentFieldPath.length - 1]
95+
)
7796
.map((fieldPath) => fieldPath[fieldPath.length - 1]);
7897

79-
const isDuplicate = fieldsNamesWithoutCurrent.some(
98+
const isDuplicate = siblingFields.some(
8099
(fieldName) => fieldName === trimmedName
81100
);
82101

@@ -108,10 +127,7 @@ const FieldDrawerContent: React.FunctionComponent<FieldDrawerContentProps> = ({
108127
if (!isFieldNameValid) {
109128
return;
110129
}
111-
onRenameField(namespace, fieldPath, [
112-
...fieldPath.slice(0, fieldPath.length - 1),
113-
trimmedName,
114-
]);
130+
onRenameField(namespace, fieldPath, trimmedName);
115131
}
116132
);
117133

@@ -131,7 +147,7 @@ const FieldDrawerContent: React.FunctionComponent<FieldDrawerContentProps> = ({
131147
<DMFormFieldContainer>
132148
<TextInput
133149
label="Field name"
134-
disabled={true} // TODO: enable when field renaming is implemented
150+
disabled={isIdField(fieldPath)}
135151
data-testid="data-model-collection-drawer-name-input"
136152
sizeVariant="small"
137153
value={fieldName}
@@ -181,35 +197,37 @@ export default connect(
181197
state: DataModelingState,
182198
ownProps: { namespace: string; fieldPath: FieldPath }
183199
) => {
184-
const model = selectCurrentModelFromState(state);
200+
const diagram = getCurrentDiagramFromState(state);
201+
const model = selectCurrentModel(diagram.edits);
202+
const collectionSchema = model.collections.find(
203+
(collection) => collection.ns === ownProps.namespace
204+
)?.jsonSchema;
205+
if (!collectionSchema) {
206+
throw new Error('Collection not found');
207+
}
185208
return {
186209
types:
187210
getFieldFromSchema({
188-
jsonSchema:
189-
model.collections.find(
190-
(collection) => collection.ns === ownProps.namespace
191-
)?.jsonSchema ?? {},
211+
jsonSchema: collectionSchema,
192212
fieldPath: ownProps.fieldPath,
193213
})?.fieldTypes ?? [],
194-
fieldPaths: [], // TODO(COMPASS-9659): get existing field paths
195-
relationships: model.relationships.filter((r) => {
196-
const [local, foreign] = r.relationship;
197-
return (
198-
(local.ns === ownProps.namespace &&
199-
local.fields &&
200-
areFieldPathsEqual(local.fields, ownProps.fieldPath)) ||
201-
(foreign.ns === ownProps.namespace &&
202-
foreign.fields &&
203-
areFieldPathsEqual(foreign.fields, ownProps.fieldPath))
204-
);
205-
}),
214+
fieldPaths: selectFieldsForCurrentModel(diagram.edits)[
215+
ownProps.namespace
216+
],
217+
relationships: model.relationships.filter(({ relationship }) =>
218+
isRelationshipOfAField(
219+
relationship,
220+
ownProps.namespace,
221+
ownProps.fieldPath
222+
)
223+
),
206224
};
207225
},
208226
{
209227
onCreateNewRelationshipClick: createNewRelationship,
210228
onEditRelationshipClick: selectRelationship,
211229
onDeleteRelationshipClick: deleteRelationship,
212-
onRenameField: () => {}, // TODO(COMPASS-9659): renameField,
230+
onRenameField: renameField,
213231
onChangeFieldType: () => {}, // TODO(COMPASS-9659): updateFieldSchema,
214232
}
215233
)(FieldDrawerContent);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ const EditSchemaVariants = z.discriminatedUnion('type', [
9898
z.object({
9999
type: z.literal('RenameField'),
100100
ns: z.string(),
101-
from: FieldPathSchema,
102-
to: FieldPathSchema,
101+
field: FieldPathSchema,
102+
newName: z.string(),
103103
}),
104104
z.object({
105105
type: z.literal('RemoveField'),

0 commit comments

Comments
 (0)