Files
IdeA/frontend/src/features/skills/useSkills.ts

146 lines
3.9 KiB
TypeScript

/**
* `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,
};
}