Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0357a4e
feat(compass-crud): add 'Add to query' to the element context menu
gagik Jul 3, 2025
4657eb9
fix: make onAddToQuery optional
gagik Jul 4, 2025
0c72b00
fix: hide Add to query hiding test
gagik Jul 4, 2025
9876045
fix: remove redundant type check and rename
gagik Jul 4, 2025
545f6e6
fix: type for HadronDocument
gagik Jul 4, 2025
8ce471a
feat: use nested key path
gagik Jul 4, 2025
70c95f6
test: add getNestedKeyPath test
gagik Jul 4, 2025
cc275a3
test: add argument test
gagik Jul 4, 2025
a5551e8
feat: add toggling queries
gagik Jul 7, 2025
6cc78ce
refactor: use a single queryBar prop
gagik Jul 7, 2025
ab94021
fix: add test and fix type errors
gagik Jul 7, 2025
eb146d0
test: fix add and remove test
gagik Jul 7, 2025
f34cc0d
Revert "refactor: use a single queryBar prop"
gagik Jul 7, 2025
3bf74ef
feat: pass a query and onAddToQuery
gagik Jul 8, 2025
d688868
fix: remove query check
gagik Jul 8, 2025
cfc1426
fix: update hasDistinctValue types
gagik Jul 8, 2025
a616886
fix: use queryFilter
gagik Jul 8, 2025
3c14dbd
fix: avoid circular dependency
gagik Jul 8, 2025
7475afd
fix: pass query filter directly
gagik Jul 8, 2025
487eda5
refactor: rename to onUpdateQuery
gagik Jul 8, 2025
1f7a2fa
fix: add mongodb-query-util
gagik Jul 8, 2025
01f6d7f
fix: remove mongodb-query-util from compass-crud
gagik Jul 8, 2025
c828f28
fix: use a MockQueryBarPlugin when testing DocumentListViewItem
gagik Jul 8, 2025
56f6f82
fix: add renderWithQueryBar hook
gagik Jul 8, 2025
3071d1c
fix: remove unused function
gagik Jul 8, 2025
3b84ba2
fix: limit query handling only to DocumentListItem
gagik Jul 8, 2025
ea3fa83
fix: remove unused import
gagik Jul 9, 2025
0ae4dd5
fix: keep hasDistinctValue changes minimal
gagik Jul 9, 2025
bb34432
fix: import type
gagik Jul 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/compass-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"hadron-type-checker": "^7.4.11",
"is-electron-renderer": "^2.0.1",
"lodash": "^4.17.21",
"mongodb-query-util": "^2.4.11",
"polished": "^4.2.2",
"react": "^17.0.2",
"react-hotkeys-hook": "^4.3.7",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,16 @@ const HadronDocument: React.FunctionComponent<{
editing?: boolean;
onEditStart?: () => void;
extraGutterWidth?: number;
onUpdateQuery?: (field: string, value: unknown) => void;
query?: Record<string, unknown>;
}> = ({
value: document,
editable = false,
editing = false,
onEditStart,
extraGutterWidth,
onUpdateQuery,
query,
}) => {
const { elements, visibleElements } = useHadronDocument(document);
const [autoFocus, setAutoFocus] = useState<{
Expand Down Expand Up @@ -156,6 +160,8 @@ const HadronDocument: React.FunctionComponent<{
});
}}
extraGutterWidth={extraGutterWidth}
onUpdateQuery={onUpdateQuery}
query={query}
></HadronElement>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { render, screen, userEvent } from '@mongodb-js/testing-library-compass';
import { expect } from 'chai';
import sinon from 'sinon';
import HadronDocument from 'hadron-document';
import { HadronElement } from './element';
import { HadronElement, getNestedKeyPathForElement } from './element';
import type { Element } from 'hadron-document';

describe('HadronElement', function () {
Expand All @@ -26,6 +26,77 @@ describe('HadronElement', function () {
clipboardWriteTextStub.restore();
});

it('can add to query and then remove from query', function () {
const nestedDoc = new HadronDocument({ user: { name: 'John' } });
const nestedElement = nestedDoc.get('user')!.get('name')!;

// Mock onUpdateQuery callback
const mockonUpdateQuery = sinon.spy();

// Start with empty query
const { rerender } = render(
<HadronElement
value={nestedElement}
editable={true}
editingEnabled={true}
lineNumberSize={1}
onAddElement={() => {}}
onUpdateQuery={mockonUpdateQuery}
query={{}}
/>
);

// Open context menu - should show "Add to query"
const elementNode = screen.getByTestId('hadron-document-element');
userEvent.click(elementNode, { button: 2 });

expect(screen.getByText('Add to query')).to.exist;
expect(screen.queryByText('Remove from query')).to.not.exist;

userEvent.click(screen.getByText('Add to query'), undefined, {
skipPointerEventsCheck: true,
});

expect(mockonUpdateQuery).to.have.been.calledWith(
'user.name',
nestedElement.generateObject()
);

// Now simulate that the field is in query
const queryWithField = {
'user.name': nestedElement.generateObject(),
};

// Re-render with updated query state
rerender(
<HadronElement
value={nestedElement}
editable={true}
editingEnabled={true}
lineNumberSize={1}
onAddElement={() => {}}
onUpdateQuery={mockonUpdateQuery}
query={queryWithField}
/>
);

// Open context menu again - should now show "Remove from query"
userEvent.click(elementNode, { button: 2 });

expect(screen.getByText('Remove from query')).to.exist;
expect(screen.queryByText('Add to query')).to.not.exist;

userEvent.click(screen.getByText('Remove from query'), undefined, {
skipPointerEventsCheck: true,
});

expect(mockonUpdateQuery).to.have.been.calledTwice;
expect(mockonUpdateQuery.secondCall).to.have.been.calledWith(
'user.name',
nestedElement.generateObject()
);
});

it('copies field and value when "Copy field & value" is clicked', function () {
render(
<HadronElement
Expand Down Expand Up @@ -117,5 +188,190 @@ describe('HadronElement', function () {
// Check that the menu item doesn't exist
expect(screen.queryByText('Open URL in browser')).to.not.exist;
});

it('does not show "Add to query" when onUpdateQuery is not provided', function () {
render(
<HadronElement
value={element}
editable={true}
editingEnabled={true}
lineNumberSize={1}
onAddElement={() => {}}
/>
);
const elementNode = screen.getByTestId('hadron-document-element');
userEvent.click(elementNode, { button: 2 });

expect(screen.queryByText('Add to query')).to.not.exist;
});

it('calls the correct parameters when "Add to query" is clicked', function () {
const nestedDoc = new HadronDocument({ user: { name: 'John' } });
const nestedElement = nestedDoc.get('user')!.get('name')!;
const mockonUpdateQuery = sinon.spy();

render(
<HadronElement
value={nestedElement}
editable={true}
editingEnabled={true}
lineNumberSize={1}
onAddElement={() => {}}
onUpdateQuery={mockonUpdateQuery}
query={{}}
/>
);

// Open context menu and click the add to query option
const elementNode = screen.getByTestId('hadron-document-element');
userEvent.click(elementNode, { button: 2 });
userEvent.click(screen.getByText('Add to query'), undefined, {
skipPointerEventsCheck: true,
});

// Verify that toggleQueryFilter was called with the nested field path and element's generated object
expect(mockonUpdateQuery).to.have.been.calledWith(
'user.name',
nestedElement.generateObject()
);
});
});

describe('getNestedKeyPathForElement', function () {
it('returns the field name for a top-level field', function () {
const doc = new HadronDocument({ field: 'value' });
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const element = doc.elements.at(0)!;

const result = getNestedKeyPathForElement(element);

expect(result).to.equal('field');
});

it('returns dot notation path for nested object fields', function () {
const doc = new HadronDocument({
user: {
profile: {
name: 'John',
},
},
});
const nameElement = doc.get('user')!.get('profile')!.get('name')!;

const result = getNestedKeyPathForElement(nameElement);

expect(result).to.equal('user.profile.name');
});

it('skips array indices in the path', function () {
const doc = new HadronDocument({
items: [{ name: 'item1' }, { name: 'item2' }],
});
const nameElement = doc.get('items')!.elements!.at(0)!.get('name')!;

const result = getNestedKeyPathForElement(nameElement);

expect(result).to.equal('items.name');
});

it('handles mixed nesting with arrays and objects', function () {
const doc = new HadronDocument({
orders: [
{
items: [{ product: { name: 'Widget' } }],
},
],
});
const nameElement = doc
.get('orders')!
.elements!.at(0)!
.get('items')!
.elements!.at(0)!
.get('product')!
.get('name')!;

const result = getNestedKeyPathForElement(nameElement);

expect(result).to.equal('orders.items.product.name');
});

it('handles array elements at the top level', function () {
const doc = new HadronDocument({
items: [{ name: 'item1' }, { name: 'item2' }],
});
const nameElement = doc.elements.get('items')!.at(0)!.get('name')!;

const result = getNestedKeyPathForElement(nameElement);

expect(result).to.equal('items.name');
});

it('handles deeply nested objects', function () {
const doc = new HadronDocument({
level1: {
level2: {
level3: {
level4: {
value: 'deep',
},
},
},
},
});
const valueElement = doc
.get('level1')!
.get('level2')!
.get('level3')!
.get('level4')!
.get('value')!;

const result = getNestedKeyPathForElement(valueElement);

expect(result).to.equal('level1.level2.level3.level4.value');
});

it('handles field names with special characters', function () {
const doc = new HadronDocument({
'field-with-dashes': {
field_with_underscores: {
'field.with.dots': 'value',
},
},
});
const dotsElement = doc
.get('field-with-dashes')!
.get('field_with_underscores')!
.get('field.with.dots')!;

const result = getNestedKeyPathForElement(dotsElement);

expect(result).to.equal(
'field-with-dashes.field_with_underscores.field.with.dots'
);
});

it('handles numeric field names', function () {
const doc = new HadronDocument({
123: {
456: 'value',
},
});
const numericElement = doc.get('123')!.get('456')!;

const result = getNestedKeyPathForElement(numericElement);

expect(numericElement.value).to.equal('value');
expect(result).to.equal('123.456');
});

it('handles empty object elements', function () {
const doc = new HadronDocument({ emptyObj: {} });
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const emptyObjElement = doc.elements.at(0)!;

const result = getNestedKeyPathForElement(emptyObjElement);

expect(result).to.equal('emptyObj');
});
});
});
Loading
Loading