Hotfix v0.3.3: search covers hidden columns and translation fields (#24)#56
Merged
Conversation
Two sub-bugs reported by @DjLawRee and @OSZII over the course of 8 months, plus a related side-bug discovered during code review. Sub-Bug A (hidden columns): the all-fields fallback in buildSearchFilter ran only when no visible column produced a clause, so any layout with a visible string column silently skipped every other searchable column. The fallback is now cumulative — visible and hidden passes run sequentially with processedFields deduplication. Sub-Bug B (translations alias): top-level translations aliases were not searched at all. Detection now via field.meta.special (works for renamed aliases like i18n / localizations), relations store resolves the translations collection, one outer-_or clause per searchable column. Clauses are NOT wrapped in `{ _some: { _or: [...] } }` because Directus' query parser does not evaluate _or inside _some — it matches every row. Bug caught by integration testing during the fix. Side-fix: useTableFields.ts and super-table.vue used `relation.related_collection || relation.collection` for the translations collection lookup. For parent → translations relations this returned the parent itself; the bug was masked by a hardcoded commonTranslationFields fallback (name, title, description, content, subtitle). Both call sites now use a new shared helper that reads `relation.meta.many_collection` and trusts the schema entirely — custom field names (body, slug, summary, …) resolve correctly. Code: - new pure function src/utils/buildSearchFilter.ts (extracted from super-table.vue for direct unit testability) - new src/utils/resolveTranslationsCollection.ts (shared by super-table.vue and useTableFields.ts) 37 new unit tests, 18 E2E browser scenarios verified including SQL-injection safety, CJK multibyte, case-insensitivity, sort+search, pagination, multi-relation, M2O FK exclusion, meta.hidden exclusion, and deduplication. Closes #24
Quality Check ResultsTypeScript Type Check✅ Passed - No type errors found ESLint✅ Passed - No linting errors Prettier Format Check✅ Passed - Code is properly formatted Build✅ Passed - Extension builds successfully Updated: 2026-05-09T22:14:55.067Z |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #24 — search now correctly covers hidden columns and translation fields, plus a related side-bug fix for the translation collection lookup.
Reported in #24 over the course of 8 months by @DjLawRee, @OSZII, and several other users. Two distinct sub-bugs are addressed in this PR.
Sub-Bug A — hidden columns ignored (@DjLawRee)
The all-fields fallback inside
buildSearchFilterwas gated byconditions.length === 0, meaning it only ran when no visible column produced a clause. As soon as any visible string column contributed, every other searchable column the user had not added to the layout was silently skipped — even legitimate ones withmeta.hidden !== true.Fix: the fallback is now cumulative. Visible-pass and hidden-pass run sequentially, deduplicating via
processedFieldsso no field contributes twice.Sub-Bug B — top-level translations alias not searched (@OSZII)
When the translations alias is configured at the top level (just
translationsas a column), the visible pass landed in the type check, foundalias≠ string/text, and produced no clause at all. OSZII reported the inability to find ""Berg"" in a translation'snamecolumn.Fix: the visible pass now detects translation aliases via
field.meta.special.includes('translations')(works for renamed aliases likei18n/localizationstoo — not just the literal field name), resolves the translations collection through the relations store, enumerates its searchable string/text columns (excluding M2O FKs and hidden fields), and emits one outer-_orclause per column.Critical detail discovered during integration testing
Clauses are emitted as one outer-
_orentry per column:NOT wrapped in a single
_some._or:Directus' query parser does not evaluate
_orcorrectly inside_some— it returns every row. My initial implementation used the wrapped form, all 24 unit tests passed, but the live API call returned 30/30 items instead of 1. The browser-level integration test was what caught it. Tests now pin the correct unwrapped form.Side-fix — wrong relations property in existing code
While building the new helper I noticed
useTableFields.ts:73and the inline implementation insuper-table.vue:534were usingrelation.related_collection || relation.collectionto find the translations collection. For parent → translations O2M relations this returned the parent collection itself, not the translations collection.The bug was masked by a hardcoded
commonTranslationFieldsfallback (name,title,description,content,subtitle). Custom translation field names likebody,slug, orsummarydid not resolve correctly.Fix: both call sites now use the new shared helper
getTranslationFieldMetadatainsrc/utils/resolveTranslationsCollection.ts, which:relation.meta.many_collectionas the canonical path (withrelation.collectionas defensive fallback), andCode organization
src/utils/buildSearchFilter.ts— pure function extracted fromsuper-table.vue(was 120 LOC inline) for direct unit testability without Vue mounting.src/utils/resolveTranslationsCollection.ts— shared helper used by bothsuper-table.vueanduseTableFields.ts. No more duplicate inline implementations.Test plan
tests/unit/utils/buildSearchFilter.test.tsandresolveTranslationsCollection.test.tscovering empty/whitespace input, visible strings/UUIDs/integers, dot-notation translations, top-level translations (single/multi-column, with/without renamed alias, M2O exclusion, FK exclusion, hidden-field exclusion, missing-relation graceful skip, multiple translations relations), and the cumulative hidden-field fallback.pnpm run type-check)--max-warnings=0cleanEnd-to-end browser verification (18 scenarios)
Tested live against three reproduction collections plus a realistic multi-relation articles collection:
fact-checkininternal_notes(not as column)meta.hidden=trueexclusion —SECRET-ADMIN(secret_admin_notes)HochzeitweddingWeltklasseviai18n_seo.meta_description_eq—1500onview_countbulkItalien+ sort-slugHOCHZEITmatchesHochzeit%_icontainsbehavior, consistent with native searchO'Brien'); DROP TABLE articles;--日本語matches translation bodymeta.hidden=trueexcludes)a(deduplication)test_bug_44regression smoke testWhat is intentionally out of scope
_icontainsdoes not supportjsoncolumns (raises 400); native search has the same limitation. Sub-Bug A covers DjLawRee's actual use case (term in a hidden text/string column rather than truly inside a JSON tree).%and_LIKE-wildcard escaping — Directus'_icontainsoperator does not escape these by default; native search behaves identically. Documenting, not fixing here.Reviewer notes
FieldsStoreLike,RelationsStoreLike) so tests work without Pinia._some._orDirectus parser quirk is documented inline inbuildSearchFilter.tsto prevent future regressions..titletranslation hardcoded-fallback bug for translations); this PR removes the residual hardcoded fallback at its root.