Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
154 lines
4.5 KiB
TypeScript
154 lines
4.5 KiB
TypeScript
/**
|
|
* `useLayouts` — view-model hook for the named-layout tab bar (L4 / #4).
|
|
*
|
|
* Manages the list of named layouts for a project (list / create / rename /
|
|
* delete / set-active). Speaks exclusively to the {@link LayoutGateway} port.
|
|
*/
|
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
|
|
import type { GatewayError, LayoutInfo, LayoutKind } from "@/domain";
|
|
import { useGateways } from "@/app/di";
|
|
|
|
export interface LayoutsViewModel {
|
|
/** Ordered list of named layouts. */
|
|
layouts: LayoutInfo[];
|
|
/** Currently active layout id. */
|
|
activeId: string | null;
|
|
/** Whether a request is in flight. */
|
|
busy: boolean;
|
|
/** Last error message, or `null`. */
|
|
error: string | null;
|
|
/** Switches the active layout (does NOT force a re-fetch here; the caller uses the returned activeId). */
|
|
setActive: (layoutId: string) => Promise<void>;
|
|
/** Creates a new layout with the given name and kind; resolves with the new layoutId. */
|
|
create: (name: string, kind?: LayoutKind) => Promise<string | null>;
|
|
/** Renames a layout. */
|
|
rename: (layoutId: string, name: string) => Promise<void>;
|
|
/** Deletes a layout; refuses if it is the last one. */
|
|
deleteLayout: (layoutId: 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 useLayouts(projectId: string | null): LayoutsViewModel {
|
|
const { layout: gateway } = useGateways();
|
|
const [layouts, setLayouts] = useState<LayoutInfo[]>([]);
|
|
const [activeId, setActiveId] = useState<string | null>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Load the list on mount and whenever projectId changes.
|
|
useEffect(() => {
|
|
if (!projectId || !gateway) {
|
|
setLayouts([]);
|
|
setActiveId(null);
|
|
return;
|
|
}
|
|
let cancelled = false;
|
|
setBusy(true);
|
|
gateway
|
|
.listLayouts(projectId)
|
|
.then(({ layouts: list, activeId: aid }) => {
|
|
if (!cancelled) {
|
|
setLayouts(list);
|
|
setActiveId(aid);
|
|
}
|
|
})
|
|
.catch((e: unknown) => {
|
|
if (!cancelled) setError(describe(e));
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) setBusy(false);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [gateway, projectId]);
|
|
|
|
const setActive = useCallback(
|
|
async (layoutId: string) => {
|
|
if (!projectId || !gateway) return;
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
await gateway.setActiveLayout(projectId, layoutId);
|
|
setActiveId(layoutId);
|
|
} catch (e) {
|
|
setError(describe(e));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
},
|
|
[gateway, projectId],
|
|
);
|
|
|
|
const create = useCallback(
|
|
async (name: string, kind?: LayoutKind): Promise<string | null> => {
|
|
if (!projectId || !gateway) return null;
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
const { layoutId } = await gateway.createLayout(projectId, name, kind);
|
|
const updated = await gateway.listLayouts(projectId);
|
|
setLayouts(updated.layouts);
|
|
return layoutId;
|
|
} catch (e) {
|
|
setError(describe(e));
|
|
return null;
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
},
|
|
[gateway, projectId],
|
|
);
|
|
|
|
const rename = useCallback(
|
|
async (layoutId: string, name: string) => {
|
|
if (!projectId || !gateway) return;
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
await gateway.renameLayout(projectId, layoutId, name);
|
|
setLayouts((prev) =>
|
|
prev.map((l) => (l.id === layoutId ? { ...l, name } : l)),
|
|
);
|
|
} catch (e) {
|
|
setError(describe(e));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
},
|
|
[gateway, projectId],
|
|
);
|
|
|
|
const deleteLayout = useCallback(
|
|
async (layoutId: string) => {
|
|
if (!projectId || !gateway) return;
|
|
if (layouts.length <= 1) {
|
|
setError("Cannot delete the last layout.");
|
|
return;
|
|
}
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
const { activeId: newActiveId } = await gateway.deleteLayout(projectId, layoutId);
|
|
setLayouts((prev) => prev.filter((l) => l.id !== layoutId));
|
|
setActiveId(newActiveId);
|
|
} catch (e) {
|
|
setError(describe(e));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
},
|
|
[gateway, projectId, layouts.length],
|
|
);
|
|
|
|
return { layouts, activeId, busy, error, setActive, create, rename, deleteLayout };
|
|
}
|