From b5cc9c0a07be4948ca1018b87eb4b924664cee01 Mon Sep 17 00:00:00 2001 From: Martin Wiesner Date: Tue, 21 Jul 2026 06:55:09 +0200 Subject: [PATCH 1/2] [Element Editor] Fix relation grid search not matching visible column values In the many-to-many object relation editor the search box should match the configured visibleFields column values, not only the object path. applySearchFilter joins each row to its visible-field values by id (a Map keyed by field.id, dropping nil-id entries), but the visibleFieldsValue producer emitted only column key/value pairs with no id, so every entry was dropped and search silently fell back to matching fullPath only. Attach the row id to each produced record so the join resolves. --- .../many-to-many-object-relation.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/assets/js/src/core/modules/element/dynamic-types/definitions/objects/data-related/components/many-to-many-object-relation/many-to-many-object-relation.tsx b/assets/js/src/core/modules/element/dynamic-types/definitions/objects/data-related/components/many-to-many-object-relation/many-to-many-object-relation.tsx index f3742b3cc8..bb2a73cc1d 100644 --- a/assets/js/src/core/modules/element/dynamic-types/definitions/objects/data-related/components/many-to-many-object-relation/many-to-many-object-relation.tsx +++ b/assets/js/src/core/modules/element/dynamic-types/definitions/objects/data-related/components/many-to-many-object-relation/many-to-many-object-relation.tsx @@ -216,10 +216,15 @@ const ManyToManyObjectRelationInner = (props: ManyToManyObjectRelationProps): Re const visibleFieldsValue = useMemo(() => { return mergedGridFullData.map(item => { - return item?.columns?.reduce>((acc, col) => { + const columns = item?.columns?.reduce>((acc, col) => { acc[col.key!] = col.value return acc }, {}) + + // applySearchFilter joins a grid row to its visible-field values by id: it + // builds a Map keyed by field.id and drops entries with a nil id. Without + // the id here the lookup misses and search silently falls back to fullPath. + return columns === undefined ? undefined : { ...columns, id: item.id } }) }, [mergedGridFullData]) From 2011b53df9809f004207c8774956b3110e5ce305 Mon Sep 17 00:00:00 2001 From: Martin Wiesner Date: Tue, 21 Jul 2026 08:50:52 +0200 Subject: [PATCH 2/2] [Element Editor] Cover relation search with a useValue test; use isUndefined guard - Add a renderHook(useValue) regression test for visible-column search: a column value matches only when visibleFieldsValue entries carry the row id, and the filter falls back to fullPath when they do not. - Use lodash isUndefined for the producer's undefined check, matching the file's existing guard style (review feedback). --- .../hooks/use-value.test.ts | 102 ++++++++++++++++++ .../many-to-many-object-relation.tsx | 2 +- 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 assets/js/src/core/components/many-to-many-relation/hooks/use-value.test.ts diff --git a/assets/js/src/core/components/many-to-many-relation/hooks/use-value.test.ts b/assets/js/src/core/components/many-to-many-relation/hooks/use-value.test.ts new file mode 100644 index 0000000000..6ab16e7dc6 --- /dev/null +++ b/assets/js/src/core/components/many-to-many-relation/hooks/use-value.test.ts @@ -0,0 +1,102 @@ +/** + * This source file is available under the terms of the + * Pimcore Open Core License (POCL) + * Full copyright and license information is available in + * LICENSE.md which is distributed with this source code. + * + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) + */ + +import { renderHook } from '@testing-library/react' +import { useValue, type DisplayManyToManyRelationValue, type ManyToManyRelationValue } from './use-value' + +// Isolate the hook from its data/UI dependencies; the search path taken here is +// synchronous (no valid pathFormatterConfig), so formatPath is never invoked. +jest.mock('@Pimcore/modules/data-object/hooks/use-data-object', () => ({ + useDataObject: () => ({ id: 1 }) +})) +jest.mock('@Pimcore/modules/data-object/hooks/use-format-path', () => ({ + useFormatPath: () => ({ formatPath: jest.fn(), hasUncachedItems: () => false }) +})) +jest.mock('@Pimcore/components/modal/alert-modal/hooks/use-alert-modal', () => ({ + useAlertModal: () => ({ warn: jest.fn() }) +})) +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => key }) +})) +jest.mock('../utils/path-formatter', () => ({ + isValidPathFormatterConfig: () => false +})) + +describe('useValue search filter', () => { + const value: ManyToManyRelationValue = [ + { id: 1, type: 'object', subtype: 'Institut', fullPath: '/institute/first', isPublished: true }, + { id: 2, type: 'object', subtype: 'Institut', fullPath: '/institute/second', isPublished: true } + ] + + const renderUseValue = ( + visibleFieldsValue?: Array | undefined> + ): { setDisplayedValue: jest.Mock, onSearch: (term: string) => void } => { + const setDisplayedValue = jest.fn() + const { result } = renderHook(() => + useValue( + value, + jest.fn(), + value as DisplayManyToManyRelationValue, + setDisplayedValue, + null, + false, + undefined, + visibleFieldsValue + ) + ) + return { setDisplayedValue, onSearch: result.current.onSearch } + } + + // onSearch mutates a ref and calls the (mocked) setDisplayedValue synchronously + // with no valid pathFormatterConfig, so no React state update needs flushing. + const search = (onSearch: (term: string) => void, term: string): void => { + onSearch(term) + } + + const displayedAfterSearch = (setDisplayedValue: jest.Mock): DisplayManyToManyRelationValue => + (setDisplayedValue.mock.calls.at(-1)?.[0] ?? []) as DisplayManyToManyRelationValue + + it('matches a visible column value only reachable via the row id', () => { + // Entries carry the row id, as the producer now emits them. + const { setDisplayedValue, onSearch } = renderUseValue([ + { id: 1, bankname: 'Sparkasse Musterstadt' }, + { id: 2, bankname: 'Volksbank Beispiel' } + ]) + + search(onSearch, 'sparkasse') + + // 'sparkasse' appears only in a column value, never in a fullPath. + expect(displayedAfterSearch(setDisplayedValue).map((item) => item.id)).toEqual([1]) + }) + + it('cannot match a column value when entries lack the row id (regression)', () => { + // Without an id the join drops the entry, so column search is unreachable and + // the filter falls back to the fullPath only — the bug this fix addresses. + const { setDisplayedValue, onSearch } = renderUseValue([ + { bankname: 'Sparkasse Musterstadt' }, + { bankname: 'Volksbank Beispiel' } + ]) + + search(onSearch, 'sparkasse') + + expect(displayedAfterSearch(setDisplayedValue)).toEqual([]) + }) + + it('still matches the object fullPath (fallback intact)', () => { + const { setDisplayedValue, onSearch } = renderUseValue([ + { id: 1, bankname: 'Sparkasse Musterstadt' }, + { id: 2, bankname: 'Volksbank Beispiel' } + ]) + + search(onSearch, 'second') + + expect(displayedAfterSearch(setDisplayedValue).map((item) => item.id)).toEqual([2]) + }) +}) diff --git a/assets/js/src/core/modules/element/dynamic-types/definitions/objects/data-related/components/many-to-many-object-relation/many-to-many-object-relation.tsx b/assets/js/src/core/modules/element/dynamic-types/definitions/objects/data-related/components/many-to-many-object-relation/many-to-many-object-relation.tsx index bb2a73cc1d..d535618007 100644 --- a/assets/js/src/core/modules/element/dynamic-types/definitions/objects/data-related/components/many-to-many-object-relation/many-to-many-object-relation.tsx +++ b/assets/js/src/core/modules/element/dynamic-types/definitions/objects/data-related/components/many-to-many-object-relation/many-to-many-object-relation.tsx @@ -224,7 +224,7 @@ const ManyToManyObjectRelationInner = (props: ManyToManyObjectRelationProps): Re // applySearchFilter joins a grid row to its visible-field values by id: it // builds a Map keyed by field.id and drops entries with a nil id. Without // the id here the lookup misses and search silently falls back to fullPath. - return columns === undefined ? undefined : { ...columns, id: item.id } + return isUndefined(columns) ? undefined : { ...columns, id: item.id } }) }, [mergedGridFullData])