feat(scripts): GET /api/v1/scripts/count + DashboardRoute wiring
After #34 the workspace store only holds the root-level scripts plus whatever subfolders the user has expanded. DashboardRoute's "全部脚本"/"工作副本" counts derived from scripts.length therefore underreport the workspace total until the user navigates to /scripts and expands every folder. Fix: separate count endpoint + dedicated store field, mounted independently. Backend — backend/src/backend/scripts.py - New endpoint GET /api/v1/scripts/count. - Route declared BEFORE /api/v1/scripts/{script_id}/... so FastAPI's declaration-order matching does not interpret "count" as a script_id. - Returns { data: { total: number }, meta: {} }; SQL is a single COUNT(*) on scripts filtered by workspace_id + status='active'. Frontend — services/api.ts + context/AuthContext.tsx - countScripts(workspaceId) client; WorkspaceBoundApi gains the field; AuthContext binding forwards workspaceId. Frontend — state/scriptWorkspaceStore.ts - scriptCount: number | null, scriptCountLoading: boolean. - loadScriptCount() action: idempotent (no-op while in-flight), silent on failure (dashboard tolerates a stale count). - Initial state and reset() clear both fields. Frontend — features/platform/DashboardRoute.tsx - Subscribes to scriptCount; calls loadScriptCount() on mount. - Falls back to scripts.length until the count resolves so the dashboard never blanks. Tests — backend/tests/test_count_scripts.py (new) - 3 unit tests: scalar result handling, NULL coercion, route callable. Verified: pytest 55 passed (52 + 3 new); pnpm typecheck clean.
This commit is contained in:
@@ -91,6 +91,11 @@ type State = {
|
||||
// every cache entry that has been loaded in this session.
|
||||
loadedScriptPaths: Set<string>;
|
||||
loadingScriptPaths: Set<string>;
|
||||
// Workspace-wide active-script total — separate from the lazy-loaded
|
||||
// `scripts` array so dashboards don't underreport. `null` until the
|
||||
// first loadScriptCount() resolves; the count endpoint is cheap.
|
||||
scriptCount: number | null;
|
||||
scriptCountLoading: boolean;
|
||||
|
||||
// 只读内容刷新版本号(用于触发已打开标签页的内容刷新)
|
||||
readOnlyRefreshVersion: number;
|
||||
@@ -131,6 +136,9 @@ type State = {
|
||||
// Lazy-load scripts directly under `parentPath`. Idempotent — repeated
|
||||
// calls for an already-loaded path are no-ops; in-flight calls dedupe.
|
||||
loadScripts: (parentPath: string) => Promise<void>;
|
||||
// Fetch the workspace-wide active-script total. Cheap; the dashboard
|
||||
// uses this for its hero count so it doesn't depend on lazy-loaded state.
|
||||
loadScriptCount: () => Promise<void>;
|
||||
toggleScriptLock: (script: ScriptItem) => Promise<void>;
|
||||
openPublishDialog: (script: ScriptItem) => void;
|
||||
submitPublish: (releaseNote: string, visibility: Visibility) => Promise<void>;
|
||||
@@ -215,6 +223,9 @@ export const useScriptWorkspaceStore = create<State>((set, get) => {
|
||||
loadedScriptPaths: new Set<string>(),
|
||||
loadingScriptPaths: new Set<string>(),
|
||||
|
||||
scriptCount: null,
|
||||
scriptCountLoading: false,
|
||||
|
||||
readOnlyRefreshVersion: 0,
|
||||
|
||||
setApiOnline: (online) => set({ apiOnline: online }),
|
||||
@@ -255,6 +266,8 @@ export const useScriptWorkspaceStore = create<State>((set, get) => {
|
||||
loadedChildPaths: new Set<string>(),
|
||||
loadedScriptPaths: new Set<string>(),
|
||||
loadingScriptPaths: new Set<string>(),
|
||||
scriptCount: null,
|
||||
scriptCountLoading: false,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -382,6 +395,22 @@ export const useScriptWorkspaceStore = create<State>((set, get) => {
|
||||
}
|
||||
},
|
||||
|
||||
loadScriptCount: async () => {
|
||||
const api = requireApi();
|
||||
if (get().scriptCountLoading) return;
|
||||
set({ scriptCountLoading: true });
|
||||
try {
|
||||
const total = await api.countScripts();
|
||||
set({ scriptCount: total });
|
||||
} catch {
|
||||
// Leave previous value in place; the dashboard already tolerates
|
||||
// a stale count by rendering `scriptCount ?? 0`. Don't toast —
|
||||
// the dashboard's other metrics are best-effort.
|
||||
} finally {
|
||||
set({ scriptCountLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
loadChildren: async (parentPath) => {
|
||||
const api = requireApi();
|
||||
if (get().loadedChildPaths.has(parentPath)) return;
|
||||
|
||||
Reference in New Issue
Block a user