Skip to content

Commit e49efac

Browse files
authored
feat(compass-crud): add 'Add to query' to the element context menu COMPASS-9394 (#7091)
1 parent de2f911 commit e49efac

File tree

16 files changed

+537
-147
lines changed

16 files changed

+537
-147
lines changed

package-lock.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass-components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"hadron-type-checker": "^7.4.11",
8787
"is-electron-renderer": "^2.0.1",
8888
"lodash": "^4.17.21",
89+
"mongodb-query-util": "^2.4.11",
8990
"polished": "^4.2.2",
9091
"react": "^17.0.2",
9192
"react-hotkeys-hook": "^4.3.7",

packages/compass-components/src/components/document-list/document.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,16 @@ const HadronDocument: React.FunctionComponent<{
8686
editing?: boolean;
8787
onEditStart?: () => void;
8888
extraGutterWidth?: number;
89+
onUpdateQuery?: (field: string, value: unknown) => void;
90+
query?: Record<string, unknown>;
8991
}> = ({
9092
value: document,
9193
editable = false,
9294
editing = false,
9395
onEditStart,
9496
extraGutterWidth,
97+
onUpdateQuery,
98+
query,
9599
}) => {
96100
const { elements, visibleElements } = useHadronDocument(document);
97101
const [autoFocus, setAutoFocus] = useState<{
@@ -156,6 +160,8 @@ const HadronDocument: React.FunctionComponent<{
156160
});
157161
}}
158162
extraGutterWidth={extraGutterWidth}
163+
onUpdateQuery={onUpdateQuery}
164+
query={query}
159165
></HadronElement>
160166
);
161167
})}

packages/compass-components/src/components/document-list/element.spec.tsx

Lines changed: 257 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { render, screen, userEvent } from '@mongodb-js/testing-library-compass';
33
import { expect } from 'chai';
44
import sinon from 'sinon';
55
import HadronDocument from 'hadron-document';
6-
import { HadronElement } from './element';
6+
import { HadronElement, getNestedKeyPathForElement } from './element';
77
import type { Element } from 'hadron-document';
88

99
describe('HadronElement', function () {
@@ -26,6 +26,77 @@ describe('HadronElement', function () {
2626
clipboardWriteTextStub.restore();
2727
});
2828

29+
it('can add to query and then remove from query', function () {
30+
const nestedDoc = new HadronDocument({ user: { name: 'John' } });
31+
const nestedElement = nestedDoc.get('user')!.get('name')!;
32+
33+
// Mock onUpdateQuery callback
34+
const mockonUpdateQuery = sinon.spy();
35+
36+
// Start with empty query
37+
const { rerender } = render(
38+
<HadronElement
39+
value={nestedElement}
40+
editable={true}
41+
editingEnabled={true}
42+
lineNumberSize={1}
43+
onAddElement={() => {}}
44+
onUpdateQuery={mockonUpdateQuery}
45+
query={{}}
46+
/>
47+
);
48+
49+
// Open context menu - should show "Add to query"
50+
const elementNode = screen.getByTestId('hadron-document-element');
51+
userEvent.click(elementNode, { button: 2 });
52+
53+
expect(screen.getByText('Add to query')).to.exist;
54+
expect(screen.queryByText('Remove from query')).to.not.exist;
55+
56+
userEvent.click(screen.getByText('Add to query'), undefined, {
57+
skipPointerEventsCheck: true,
58+
});
59+
60+
expect(mockonUpdateQuery).to.have.been.calledWith(
61+
'user.name',
62+
nestedElement.generateObject()
63+
);
64+
65+
// Now simulate that the field is in query
66+
const queryWithField = {
67+
'user.name': nestedElement.generateObject(),
68+
};
69+
70+
// Re-render with updated query state
71+
rerender(
72+
<HadronElement
73+
value={nestedElement}
74+
editable={true}
75+
editingEnabled={true}
76+
lineNumberSize={1}
77+
onAddElement={() => {}}
78+
onUpdateQuery={mockonUpdateQuery}
79+
query={queryWithField}
80+
/>
81+
);
82+
83+
// Open context menu again - should now show "Remove from query"
84+
userEvent.click(elementNode, { button: 2 });
85+
86+
expect(screen.getByText('Remove from query')).to.exist;
87+
expect(screen.queryByText('Add to query')).to.not.exist;
88+
89+
userEvent.click(screen.getByText('Remove from query'), undefined, {
90+
skipPointerEventsCheck: true,
91+
});
92+
93+
expect(mockonUpdateQuery).to.have.been.calledTwice;
94+
expect(mockonUpdateQuery.secondCall).to.have.been.calledWith(
95+
'user.name',
96+
nestedElement.generateObject()
97+
);
98+
});
99+
29100
it('copies field and value when "Copy field & value" is clicked', function () {
30101
render(
31102
<HadronElement
@@ -117,5 +188,190 @@ describe('HadronElement', function () {
117188
// Check that the menu item doesn't exist
118189
expect(screen.queryByText('Open URL in browser')).to.not.exist;
119190
});
191+
192+
it('does not show "Add to query" when onUpdateQuery is not provided', function () {
193+
render(
194+
<HadronElement
195+
value={element}
196+
editable={true}
197+
editingEnabled={true}
198+
lineNumberSize={1}
199+
onAddElement={() => {}}
200+
/>
201+
);
202+
const elementNode = screen.getByTestId('hadron-document-element');
203+
userEvent.click(elementNode, { button: 2 });
204+
205+
expect(screen.queryByText('Add to query')).to.not.exist;
206+
});
207+
208+
it('calls the correct parameters when "Add to query" is clicked', function () {
209+
const nestedDoc = new HadronDocument({ user: { name: 'John' } });
210+
const nestedElement = nestedDoc.get('user')!.get('name')!;
211+
const mockonUpdateQuery = sinon.spy();
212+
213+
render(
214+
<HadronElement
215+
value={nestedElement}
216+
editable={true}
217+
editingEnabled={true}
218+
lineNumberSize={1}
219+
onAddElement={() => {}}
220+
onUpdateQuery={mockonUpdateQuery}
221+
query={{}}
222+
/>
223+
);
224+
225+
// Open context menu and click the add to query option
226+
const elementNode = screen.getByTestId('hadron-document-element');
227+
userEvent.click(elementNode, { button: 2 });
228+
userEvent.click(screen.getByText('Add to query'), undefined, {
229+
skipPointerEventsCheck: true,
230+
});
231+
232+
// Verify that toggleQueryFilter was called with the nested field path and element's generated object
233+
expect(mockonUpdateQuery).to.have.been.calledWith(
234+
'user.name',
235+
nestedElement.generateObject()
236+
);
237+
});
238+
});
239+
240+
describe('getNestedKeyPathForElement', function () {
241+
it('returns the field name for a top-level field', function () {
242+
const doc = new HadronDocument({ field: 'value' });
243+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
244+
const element = doc.elements.at(0)!;
245+
246+
const result = getNestedKeyPathForElement(element);
247+
248+
expect(result).to.equal('field');
249+
});
250+
251+
it('returns dot notation path for nested object fields', function () {
252+
const doc = new HadronDocument({
253+
user: {
254+
profile: {
255+
name: 'John',
256+
},
257+
},
258+
});
259+
const nameElement = doc.get('user')!.get('profile')!.get('name')!;
260+
261+
const result = getNestedKeyPathForElement(nameElement);
262+
263+
expect(result).to.equal('user.profile.name');
264+
});
265+
266+
it('skips array indices in the path', function () {
267+
const doc = new HadronDocument({
268+
items: [{ name: 'item1' }, { name: 'item2' }],
269+
});
270+
const nameElement = doc.get('items')!.elements!.at(0)!.get('name')!;
271+
272+
const result = getNestedKeyPathForElement(nameElement);
273+
274+
expect(result).to.equal('items.name');
275+
});
276+
277+
it('handles mixed nesting with arrays and objects', function () {
278+
const doc = new HadronDocument({
279+
orders: [
280+
{
281+
items: [{ product: { name: 'Widget' } }],
282+
},
283+
],
284+
});
285+
const nameElement = doc
286+
.get('orders')!
287+
.elements!.at(0)!
288+
.get('items')!
289+
.elements!.at(0)!
290+
.get('product')!
291+
.get('name')!;
292+
293+
const result = getNestedKeyPathForElement(nameElement);
294+
295+
expect(result).to.equal('orders.items.product.name');
296+
});
297+
298+
it('handles array elements at the top level', function () {
299+
const doc = new HadronDocument({
300+
items: [{ name: 'item1' }, { name: 'item2' }],
301+
});
302+
const nameElement = doc.elements.get('items')!.at(0)!.get('name')!;
303+
304+
const result = getNestedKeyPathForElement(nameElement);
305+
306+
expect(result).to.equal('items.name');
307+
});
308+
309+
it('handles deeply nested objects', function () {
310+
const doc = new HadronDocument({
311+
level1: {
312+
level2: {
313+
level3: {
314+
level4: {
315+
value: 'deep',
316+
},
317+
},
318+
},
319+
},
320+
});
321+
const valueElement = doc
322+
.get('level1')!
323+
.get('level2')!
324+
.get('level3')!
325+
.get('level4')!
326+
.get('value')!;
327+
328+
const result = getNestedKeyPathForElement(valueElement);
329+
330+
expect(result).to.equal('level1.level2.level3.level4.value');
331+
});
332+
333+
it('handles field names with special characters', function () {
334+
const doc = new HadronDocument({
335+
'field-with-dashes': {
336+
field_with_underscores: {
337+
'field.with.dots': 'value',
338+
},
339+
},
340+
});
341+
const dotsElement = doc
342+
.get('field-with-dashes')!
343+
.get('field_with_underscores')!
344+
.get('field.with.dots')!;
345+
346+
const result = getNestedKeyPathForElement(dotsElement);
347+
348+
expect(result).to.equal(
349+
'field-with-dashes.field_with_underscores.field.with.dots'
350+
);
351+
});
352+
353+
it('handles numeric field names', function () {
354+
const doc = new HadronDocument({
355+
123: {
356+
456: 'value',
357+
},
358+
});
359+
const numericElement = doc.get('123')!.get('456')!;
360+
361+
const result = getNestedKeyPathForElement(numericElement);
362+
363+
expect(numericElement.value).to.equal('value');
364+
expect(result).to.equal('123.456');
365+
});
366+
367+
it('handles empty object elements', function () {
368+
const doc = new HadronDocument({ emptyObj: {} });
369+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
370+
const emptyObjElement = doc.elements.at(0)!;
371+
372+
const result = getNestedKeyPathForElement(emptyObjElement);
373+
374+
expect(result).to.equal('emptyObj');
375+
});
120376
});
121377
});

0 commit comments

Comments
 (0)