feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
153
frontend/src/features/layout/useLayouts.ts
Normal file
153
frontend/src/features/layout/useLayouts.ts
Normal file
@ -0,0 +1,153 @@
|
||||
/**
|
||||
* `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 };
|
||||
}
|
||||
Reference in New Issue
Block a user