fix: fix some displays and features
This commit is contained in:
145
frontend/src/features/skills/useSkills.ts
Normal file
145
frontend/src/features/skills/useSkills.ts
Normal file
@ -0,0 +1,145 @@
|
||||
/**
|
||||
* `useSkills` — view-model hook for the skills feature (L12).
|
||||
*
|
||||
* Owns the skill lists for both scopes (`global` + `project`) of one project and
|
||||
* the CRUD actions. Consumes {@link SkillGateway} exclusively; never touches
|
||||
* `invoke()` or `@tauri-apps/api`, keeping the component layer testable with
|
||||
* mock gateways (ARCHITECTURE §1.3).
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import type { GatewayError, Skill, SkillScope } from "@/domain";
|
||||
import type { CreateSkillInput } from "@/ports";
|
||||
import { useGateways } from "@/app/di";
|
||||
|
||||
/** What the skills UI needs from this hook. */
|
||||
export interface SkillsViewModel {
|
||||
/** Global skills (reusable across projects). */
|
||||
globalSkills: Skill[];
|
||||
/** Project-scoped skills. */
|
||||
projectSkills: Skill[];
|
||||
/** Last error message, or `null`. */
|
||||
error: string | null;
|
||||
/** Whether a request is in flight. */
|
||||
busy: boolean;
|
||||
/** Reloads both scope lists. */
|
||||
refresh: () => Promise<void>;
|
||||
/** Creates a new skill and refreshes its scope's list. */
|
||||
createSkill: (input: Omit<CreateSkillInput, "projectId">) => Promise<void>;
|
||||
/** Updates the content of an existing skill. */
|
||||
updateSkill: (
|
||||
scope: SkillScope,
|
||||
skillId: string,
|
||||
content: string,
|
||||
) => Promise<void>;
|
||||
/** Deletes a skill by id from its scope. */
|
||||
deleteSkill: (scope: SkillScope, skillId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
function describe(e: unknown): string {
|
||||
if (e && typeof e === "object" && "message" in e) {
|
||||
return String((e as GatewayError).message);
|
||||
}
|
||||
return String(e);
|
||||
}
|
||||
|
||||
export function useSkills(projectId: string): SkillsViewModel {
|
||||
const { skill } = useGateways();
|
||||
|
||||
const [globalSkills, setGlobalSkills] = useState<Skill[]>([]);
|
||||
const [projectSkills, setProjectSkills] = useState<Skill[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!projectId) return;
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [globals, projects] = await Promise.all([
|
||||
skill.listSkills(projectId, "global"),
|
||||
skill.listSkills(projectId, "project"),
|
||||
]);
|
||||
setGlobalSkills(globals);
|
||||
setProjectSkills(projects);
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [skill, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const setter = (scope: SkillScope) =>
|
||||
scope === "global" ? setGlobalSkills : setProjectSkills;
|
||||
|
||||
const createSkill = useCallback(
|
||||
async (input: Omit<CreateSkillInput, "projectId">) => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const created = await skill.createSkill({ ...input, projectId });
|
||||
setter(input.scope)((prev) => [...prev, created]);
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
},
|
||||
[skill, projectId],
|
||||
);
|
||||
|
||||
const updateSkill = useCallback(
|
||||
async (scope: SkillScope, skillId: string, content: string) => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const updated = await skill.updateSkill(
|
||||
projectId,
|
||||
scope,
|
||||
skillId,
|
||||
content,
|
||||
);
|
||||
setter(scope)((prev) =>
|
||||
prev.map((s) => (s.id === skillId ? updated : s)),
|
||||
);
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
},
|
||||
[skill, projectId],
|
||||
);
|
||||
|
||||
const deleteSkill = useCallback(
|
||||
async (scope: SkillScope, skillId: string) => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await skill.deleteSkill(projectId, scope, skillId);
|
||||
setter(scope)((prev) => prev.filter((s) => s.id !== skillId));
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
},
|
||||
[skill, projectId],
|
||||
);
|
||||
|
||||
return {
|
||||
globalSkills,
|
||||
projectSkills,
|
||||
error,
|
||||
busy,
|
||||
refresh,
|
||||
createSkill,
|
||||
updateSkill,
|
||||
deleteSkill,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user