globalMenuSearch.ts 4.5 KB
import type { CurrentUserMenuNodeDto } from '../types/authSession';

export type GlobalMenuSearchItem = {
  /** 传给 navigateToView / setCurrentView 的视图键 */
  viewKey: string;
  /** 展示名 */
  label: string;
  /** 父级路径,如 Labeling › Labels */
  breadcrumb?: string;
};

/** 与 App.tsx / Sidebar 一致的菜单名解析 */
export function resolveMenuViewKey(name: string): string {
  const s = (name ?? '').trim();
  if (!s) return 'Dashboard';
  if (s === 'System Management') return 'System Menu';
  if (s === 'Location Management') return 'Location Manager';
  if (s === 'Account Manager') return 'Account Management';
  if (s === 'Multiple Options') return 'Multiple Option Sets';
  return s;
}

function normalizeLabel(name: string): string {
  return name.trim() || 'N/A';
}

function displayMenuLabel(name: string): string {
  const s = normalizeLabel(name);
  if (s === 'Multiple Options') return 'Multiple Option Sets';
  return s;
}

function isDirectory(node: CurrentUserMenuNodeDto): boolean {
  const children = node.children ?? [];
  return (node.menuType ?? 0) === 0 || children.length > 0;
}

function walkRemoteMenus(
  nodes: CurrentUserMenuNodeDto[],
  parentTrail: string[],
  push: (item: GlobalMenuSearchItem) => void,
): void {
  for (const node of nodes) {
    const raw = normalizeLabel(String(node.menuName ?? node.routerName ?? node.routeUrl ?? node.id ?? ''));
    const label = displayMenuLabel(raw);
    const children = node.children ?? [];

    if (isDirectory(node)) {
      if (label !== 'Management' && label !== 'N/A') {
        walkRemoteMenus(children, [...parentTrail, label], push);
      } else {
        walkRemoteMenus(children, parentTrail, push);
      }
      continue;
    }

    const viewKey = resolveMenuViewKey(label);
    if (viewKey === 'Log Out') continue;

    push({
      viewKey,
      label,
      breadcrumb: parentTrail.length ? parentTrail.join(' › ') : undefined,
    });
  }
}

const FALLBACK_NAVIGABLE: GlobalMenuSearchItem[] = [
  { viewKey: 'Dashboard', label: 'Dashboard' },
  { viewKey: 'Labels', label: 'Labels', breadcrumb: 'Labeling' },
  { viewKey: 'Label Categories', label: 'Label Categories', breadcrumb: 'Labeling' },
  { viewKey: 'Label Types', label: 'Label Types', breadcrumb: 'Labeling' },
  { viewKey: 'Label Templates', label: 'Label Templates', breadcrumb: 'Labeling' },
  { viewKey: 'Multiple Option Sets', label: 'Multiple Option Sets', breadcrumb: 'Labeling' },
  { viewKey: 'Account Management', label: 'Account Management', breadcrumb: 'Management' },
  { viewKey: 'Location Manager', label: 'Location Manager', breadcrumb: 'Management' },
  { viewKey: 'Menu Management', label: 'Menu Management', breadcrumb: 'Management' },
  { viewKey: 'System Menu', label: 'System Menu', breadcrumb: 'Management' },
  { viewKey: 'Reports', label: 'Reports', breadcrumb: 'Management' },
  { viewKey: 'Support', label: 'Support', breadcrumb: 'Management' },
];

/** 从当前用户菜单树生成可跳转项(含 Dashboard) */
export function buildGlobalMenuSearchItems(menus?: CurrentUserMenuNodeDto[]): GlobalMenuSearchItem[] {
  const items: GlobalMenuSearchItem[] = [{ viewKey: 'Dashboard', label: 'Dashboard' }];
  const seen = new Set<string>(['Dashboard']);

  const push = (item: GlobalMenuSearchItem) => {
    if (seen.has(item.viewKey)) return;
    seen.add(item.viewKey);
    items.push(item);
  };

  if ((menus?.length ?? 0) > 0) {
    walkRemoteMenus(menus ?? [], [], push);
    return items;
  }

  for (const item of FALLBACK_NAVIGABLE) {
    push(item);
  }
  return items;
}

/** 按关键词过滤(匹配 label / breadcrumb / viewKey) */
export function filterGlobalMenuSearchItems(
  items: GlobalMenuSearchItem[],
  query: string,
  limit = 12,
): GlobalMenuSearchItem[] {
  const q = query.trim().toLowerCase();
  if (!q) return [];

  const scored = items
    .map((item) => {
      const label = item.label.toLowerCase();
      const crumb = (item.breadcrumb ?? '').toLowerCase();
      const key = item.viewKey.toLowerCase();
      const hay = `${label} ${crumb} ${key}`;
      if (!hay.includes(q)) return null;
      let score = 0;
      if (label.startsWith(q)) score += 3;
      else if (label.includes(q)) score += 2;
      if (key.startsWith(q)) score += 2;
      if (crumb.includes(q)) score += 1;
      return { item, score };
    })
    .filter((x): x is { item: GlobalMenuSearchItem; score: number } => x != null);

  scored.sort((a, b) => b.score - a.score || a.item.label.localeCompare(b.item.label));
  return scored.slice(0, limit).map((x) => x.item);
}