This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 9936619c feat: support multi-term navigation search (#763)
9936619c is described below

commit 9936619c8bb55b4fb9c3f3c8ec368696fc089213
Author: yx9o <[email protected]>
AuthorDate: Mon Aug 3 11:38:14 2026 +0800

    feat: support multi-term navigation search (#763)
---
 web/src/layouts/navigationSearch.test.ts | 22 ++++++++++++++++++
 web/src/layouts/navigationSearch.ts      | 40 +++++++++++++++++++++++++++-----
 2 files changed, 56 insertions(+), 6 deletions(-)

diff --git a/web/src/layouts/navigationSearch.test.ts 
b/web/src/layouts/navigationSearch.test.ts
index be107851..18f14801 100644
--- a/web/src/layouts/navigationSearch.test.ts
+++ b/web/src/layouts/navigationSearch.test.ts
@@ -22,6 +22,7 @@ describe('navigation search helpers', () => {
   const entries = [
     { key: '/cluster', label: 'RocketMQ 集群' },
     { key: '/settings', label: 'Settings' },
+    { key: '/ops/system-alerts', label: 'Alert Events' },
   ];
 
   it('filters labels case-insensitively and ignores query whitespace', () => {
@@ -34,6 +35,27 @@ describe('navigation search helpers', () => {
     expect(filterNavigationEntries(entries, 
'/settings')).toEqual([entries[1]]);
   });
 
+  it('matches multiple terms across labels and normalized route segments', () 
=> {
+    expect(filterNavigationEntries(entries, 'system 
alerts')).toEqual([entries[2]]);
+    expect(filterNavigationEntries(entries, 'OPS / 
SYSTEM')).toEqual([entries[2]]);
+    expect(filterNavigationEntries(entries, 'rocketmq 
cluster')).toEqual([entries[0]]);
+    expect(filterNavigationEntries(entries, 'system cluster')).toEqual([]);
+  });
+
+  it('ranks exact and prefix matches before broader matches', () => {
+    const rankedEntries = [
+      { key: '/settings/data-sources', label: 'Data Source Settings' },
+      { key: '/settings', label: 'Settings' },
+      { key: '/settings/general', label: 'Settings - General' },
+    ];
+
+    expect(filterNavigationEntries(rankedEntries, 'settings')).toEqual([
+      rankedEntries[1],
+      rankedEntries[2],
+      rankedEntries[0],
+    ]);
+  });
+
   it('recognizes Control/Command-K but rejects alternative shortcuts', () => {
     expect(
       isNavigationSearchShortcut({ key: 'k', ctrlKey: true, metaKey: false, 
altKey: false }),
diff --git a/web/src/layouts/navigationSearch.ts 
b/web/src/layouts/navigationSearch.ts
index 04c58f7d..0be63cbc 100644
--- a/web/src/layouts/navigationSearch.ts
+++ b/web/src/layouts/navigationSearch.ts
@@ -23,17 +23,45 @@ export interface NavigationSearchEntry {
   icon?: ReactNode;
 }
 
+function normalizeSearchText(value: string): string {
+  return value
+    .normalize('NFKC')
+    .toLocaleLowerCase()
+    .replace(/[^\p{L}\p{N}]+/gu, ' ')
+    .trim();
+}
+
+function matchScore(entry: NavigationSearchEntry, query: string, terms: 
string[]): number | null {
+  const label = normalizeSearchText(entry.label);
+  const key = normalizeSearchText(entry.key);
+  const searchableText = `${label} ${key}`;
+
+  if (!terms.every((term) => searchableText.includes(term))) return null;
+  if (label === query) return 0;
+  if (key === query) return 1;
+  if (label.startsWith(query)) return 2;
+  if (key.startsWith(query)) return 3;
+  if (label.includes(query)) return 4;
+  if (key.includes(query)) return 5;
+  return 6;
+}
+
 export function filterNavigationEntries(
   entries: NavigationSearchEntry[],
   query: string,
 ): NavigationSearchEntry[] {
-  const normalizedQuery = query.trim().toLocaleLowerCase();
+  const normalizedQuery = normalizeSearchText(query);
   if (!normalizedQuery) return entries;
-  return entries.filter(
-    (entry) =>
-      entry.label.toLocaleLowerCase().includes(normalizedQuery) ||
-      entry.key.toLocaleLowerCase().includes(normalizedQuery),
-  );
+  const terms = normalizedQuery.split(' ');
+
+  return entries
+    .map((entry, index) => ({ entry, index, score: matchScore(entry, 
normalizedQuery, terms) }))
+    .filter(
+      (match): match is { entry: NavigationSearchEntry; index: number; score: 
number } =>
+        match.score !== null,
+    )
+    .sort((left, right) => left.score - right.score || left.index - 
right.index)
+    .map(({ entry }) => entry);
 }
 
 export function isNavigationSearchShortcut(event: {

Reply via email to