Files
Edu/apps/portal-shell/src/lib/api/topbar.ts
SpecialX 2910a90271 feat(portal-shell): extract domain API layer and migrate 31 widgets
Task 4-10 of portal-shell data abstraction plan (M1-M2).

Add 7 domain API modules under src/lib/api/ (parent/admin/teacher/
student/universal/sidebar/topbar), each exposing semantic hooks that
wrap useWidgetQuery/useWidgetMutation and return flattened domain
models. Widget code now imports from @/lib/api instead of inlining
gql literals.

- 31 widgets migrated (gql literal count in widgets: 0)
- 7 test files (85 cases, all passing)
- topbar.useNotifications renamed to useNotificationBell to avoid
  barrel export collision with universal.useNotifications
- typecheck + lint (0 errors) + test (85/85) verified
2026-07-17 13:07:24 +08:00

136 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
/**
* Topbar domain API
*
* 涵盖useNotificationBell、useCurrentUser、useGlobalSearch
*
* widget 通过 `import { useNotificationBell } from "@/lib/api/topbar"` 调用,
* 不再各自内嵌 gql/接口/手写类型。
*
* 注意useNotificationBell 与 universal.useNotifications分页通知列表语义不同
* 前者是 topbar 铃铛 widget 专用(按 limit 取最近 N 条),后者是分页查询。
*
* 关联spec §2.2
*/
import {
GET_NOTIFICATIONS_DOC,
GET_CURRENT_USER_DOC,
SEARCH_DOC,
} from "@/lib/api/operations/topbar.graphql";
import { useWidgetQuery } from "@/lib/useWidgetQuery";
import type { UseQueryResult } from "./types";
// ===== 领域模型类型 =====
export interface NotificationItem {
id: string;
title: string;
}
export interface CurrentUser {
id: string;
name: string;
email: string;
role: string;
}
export interface SearchResult {
id: string;
type: string;
title: string;
subtitle: string;
}
// ===== 内部 Query 类型 =====
interface NotificationsQueryData {
notifications: NotificationItem[];
}
type NotificationsQueryVars = {
limit: number;
};
interface CurrentUserQueryData {
me: CurrentUser | null;
}
type CurrentUserQueryVars = Record<string, never>;
interface SearchQueryData {
search: SearchResult[];
}
type SearchQueryVars = {
keyword: string;
limit: number;
};
// ===== Hooks =====
/**
* 查询通知列表topbar 铃铛 widget 专用,按 limit 取最近 N 条)。
*
* 关联portal-shell spec §5.6 统一 Hook
*/
export function useNotificationBell(
limit: number,
): UseQueryResult<NotificationItem[]> {
const result = useWidgetQuery<NotificationsQueryData, NotificationsQueryVars>(
GET_NOTIFICATIONS_DOC,
{ limit },
);
return {
data: result.data?.notifications,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 查询当前登录用户iam 子图 me 字段)。
*
* 关联portal-shell spec §5.6 统一 Hook
*/
export function useCurrentUser(): UseQueryResult<CurrentUser | null> {
const result = useWidgetQuery<CurrentUserQueryData, CurrentUserQueryVars>(
GET_CURRENT_USER_DOC,
{},
);
return {
data: result.data?.me,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}
/**
* 全局搜索。
*
* `keyword` 为空时不发起查询enabled: false避免无效请求。
*
* 关联portal-shell spec §5.6 统一 Hook
*/
export function useGlobalSearch(
keyword: string,
limit: number,
): UseQueryResult<SearchResult[]> {
const result = useWidgetQuery<SearchQueryData, SearchQueryVars>(
SEARCH_DOC,
{ keyword, limit },
{ enabled: keyword.length > 0 },
);
return {
data: result.data?.search,
loading: result.loading,
error: result.error,
refetch: result.refetch,
};
}