Skip to content

feat(tracker): field-prefixed search, inline filter chips and zero-hit empty state - #10998

Open
MichaelUray wants to merge 4 commits into
hcengineering:developfrom
MichaelUray:feat/tracker-search-filter
Open

feat(tracker): field-prefixed search, inline filter chips and zero-hit empty state#10998
MichaelUray wants to merge 4 commits into
hcengineering:developfrom
MichaelUray:feat/tracker-search-filter

Conversation

@MichaelUray

Copy link
Copy Markdown
Contributor

Summary

Split out of #10992 following review feedback. This supersedes the closed #10872 with a clean branch based on current develop and contains only the shared search/filter work; no Gantt code.

Adds SearchInputAdvanced, match highlighting, inline filter chips with an overflow popover, a reusable zero-hit empty state in view-resources, and search-scope/highlight view options wired into List and Kanban.

Known field prefixes are routed through Elasticsearch query_string; queries without a recognized prefix retain the existing simple_query_string path. Workspace and space access filtering remain unchanged.

The exact head passed all required fork-CI jobs, including uitest, uitest-pg, uitest-workspaces, uitest-qms, build, svelte-check, test, formatting, and docker-build.

The Gantt feature remains in #10992 and builds on this PR.

…t empty state

Adds SearchInputAdvanced (field:value prefixes routed to Elasticsearch query_string), match highlighting, inline filter chips with overflow popover, a reusable zero-hit empty state in view-resources, and search-scope/highlight view options for List and Kanban.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
/**
* Allowed ES-native fields users may type directly.
*
* KEEP IN SYNC with elastic/src/adapter.ts KNOWN_FIELD_RE (and vice versa).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about refactoring it to ensure both sides use synced fields?
For example, add a small reusable package with common configs for server side and UI.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — exported fullTextSearchFields from @hcengineering/core. Both @hcengineering/ui and @hcengineering/elastic already depend on core and it already hosts fulltext concepts, so no new package or dependency changes were needed. The encoder and the elastic adapter now derive their field lists from that single constant; per-field boosts stay in the adapter. A shared escapeRegExp (also in core) keeps the regex processing identical on both sides.

// highlight toggles stay consistent across every viewlet that opts in.
const SEARCH_VIEW_OPTIONS: ViewOptionModel[] = [
{
key: 'showQuickModeSelector',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand correctly that this isn't used in this PR, but will be used in Gantt charts?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is already used in this PR: SEARCH_VIEW_OPTIONS is included by issuesOptions(), which is consumed by both the List and Kanban viewlets. #10992 reuses it for Gantt as a third consumer.

import { encodeSearch } from './SearchInputAdvanced.encoder'
import { propSyncValue } from './SearchInputAdvanced.sync'

describe('encodeSearch', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to place tests in a separate __tests__ folder

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved all five new co-located test files into the existing package folders: packages/ui/src/__test__ and plugins/view-resources/src/__tests__.

Comment thread plugins/tracker-assets/lang/zh.json Outdated
"SearchEmptyTitle": "No issues found for \"{query}\"",
"SearchEmptyActiveFilters": "Active filters: {filters}",
"SearchEmptyClearFilters": "Search without filters",
"SearchEmptyAllProjects": "Search in all projects"

@ArtyomSavchenko ArtyomSavchenko Jul 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to fix translations for this and other locales

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Translated the new keys in all supported locales, reusing each locale's existing terminology. The placeholder-parity test verifies {query}, {filters}, and {count} across all locale files.

return '\\-' + tok.raw.slice(1).replace(/[+!(){}[\]^"~\\/:&|<>=]/g, '\\$&')
}
if (!PREFIX_VALUE_RESERVED_RE.test(tok.raw)) return tok.raw
return tok.raw.replace(/[+!(){}[\]^"~\\/:&|<>=]/g, '\\$&')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] I suggest to move this regex to const

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted — the character class was duplicated three times; it now derives from a single constant, with separate test and global-replace RegExp instances to avoid the shared-lastIndex pitfall.

// set by the fast query alone, so a stale slow-query result could skew the
// count right after a search changed.
onDestroy(() => {
resultIssueCountStore.set(-1)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resultIssueCountStore is a single global mutated by multiple viewlets.
List and Kanban both write it; IssuesView resets it to -1 on query/filter change. The -1 sentinel + onDestroy reset largely covers teardown races, but during a viewlet swap (List→Kanban) both can be briefly mounted and write competing counts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented an owner-token gate for the shared result count. Each primary result viewlet claims and releases ownership; writes and teardown resets from superseded owners are ignored. Reporting from the generic List component is opt-in and defaults to false; ListView opts in, while embedded lists remain outside the protocol. Unit tests cover owner supersession, stale writes, stale releases, query resets, and the embedded-list regression.

ArtyomSavchenko and others added 3 commits July 31, 2026 12:33
…rework

Shared, non-storage-specific field list, test-folder conventions, locale
coverage, single regex source and an owner-token gate for the result count.

- core: add shared `fullTextSearchFields` constant next to
  FullTextSearchContext as the single source of truth for the full-text
  fields exposed to `field:value` targeting. The client encoder derives
  ES_NATIVE_FIELDS from it and the elastic adapter derives KNOWN_FIELD_RE
  from it, replacing the two "KEEP IN SYNC" copies. Per-field boost weights
  stay a local adapter detail.
- tests: move the five co-located tests into each package's existing test
  folder convention (ui `__test__`, view-resources `__tests__`) and fix the
  relative imports.
- i18n: translate the new tracker search/filter strings in the remaining
  locales (zh, ja, ko, cs, es, fr, it, pt, pt-br, tr), reusing each file's
  existing terminology; ICU placeholders left unchanged.
- encoder: hoist the reserved-character class into one constant and build two
  RegExp instances from it (non-global for `.test()`, global for `.replace()`)
  to avoid the shared-lastIndex trap.
- view-resources: guard `resultIssueCountStore` writes with an owner-token
  gate so a superseded viewlet can no longer clobber the active viewlet's
  count; List/KanbanView claim and release, IssuesView resets through the
  current owner. Assumes a single active IssuesView surface.
- view-resources: make result-count reporting opt-in via a new
  `reportResultCount` prop on List (default true). Embedded, non-primary List
  instances (sub-issues / related issues in the issue edit panel, routed
  through SubIssueList) pass false and never claim the owner token, so opening
  and closing an issue can no longer strand the primary Issues viewlet with a
  dead token — the zero-hit SearchEmptyState card renders again afterwards. Add
  a regression test covering the opted-out embedded consumer.
- elastic: escape every regex metacharacter (not just `.`) when building
  KNOWN_FIELD_RE from `fullTextSearchFields`, so a future field name carrying
  another metacharacter cannot silently corrupt the alternation. Behaviour for
  the current fields is unchanged.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
…ult-count store

- Share escapeRegExp from @hcengineering/core so the client encoder and the server elastic adapter escape the fulltext field list identically, not just dots on the client.

- Export resultIssueCountStore as a read-only Readable; the owner-token gate functions (setResultCount / resetResultCount / releaseResultCountOwner) are now the only write path.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants