146 lines
3.9 KiB
TypeScript
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,
|
|
};
|
|
}
|