mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-03 02:24:33 +00:00
The /runs/:id/children tab now gets server-side pagination, sortable
columns, column picker, search/time/archived filters, and bulk
archive/unarchive/delete — same affordances as the main runs list view.
Preferences persist to localStorage under a dedicated key so they don't
collide with the /runs page.
Repo and Workflow filter buttons are intentionally omitted (children
typically share these with the parent), but those columns remain visible
for the cases where workflows fan out across repos.
- New childRunsListPreferences in components/runs-list/preferences.ts
- run-children.tsx fetches via useRunsPage({parentId, ...}) with all
list controls wired up
- Empty state retains the existing "Learn about parent links" CTA
- useChildRuns + queryKeys.runs.children removed (replaced by the
generalized useRunsPage)
- useRetryRun broadcasts via mutateRunListCaches now that the dedicated
children cache key is gone
- Revert board-cache children matcher added in the previous commit
(no longer needed)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
234 lines
6.1 KiB
TypeScript
234 lines
6.1 KiB
TypeScript
import useSWRMutation from "swr/mutation";
|
|
import { useSWRConfig, type ScopedMutator } from "swr";
|
|
import type {
|
|
PreviewUrlResponse,
|
|
Run,
|
|
SteerRunRequest,
|
|
SubmitAnswerRequest,
|
|
UpdateRunRequest,
|
|
} from "@qltysh/fabro-api-client";
|
|
|
|
import {
|
|
apiData,
|
|
authApi,
|
|
humanInTheLoopApi,
|
|
runsApi,
|
|
} from "./api-client";
|
|
import { mutateRunListCaches } from "./board-cache";
|
|
import { queryKeys } from "./query-keys";
|
|
import type { LifecycleAction, LifecycleActionError } from "./run-actions";
|
|
import {
|
|
approveRun,
|
|
archiveRun,
|
|
cancelRun,
|
|
denyRun,
|
|
isLifecycleActionError,
|
|
retryRun,
|
|
unarchiveRun,
|
|
} from "./run-actions";
|
|
|
|
export type PreviewRunArg = {
|
|
port: number;
|
|
expires_in_secs: number;
|
|
signed?: boolean;
|
|
};
|
|
|
|
export type PreviewMutationResult = {
|
|
intent: "preview";
|
|
url: string;
|
|
};
|
|
|
|
export type LifecycleMutationResult =
|
|
| {
|
|
intent: LifecycleAction;
|
|
ok: true;
|
|
run: Run;
|
|
}
|
|
| {
|
|
intent: LifecycleAction;
|
|
ok: false;
|
|
error: LifecycleActionError | null;
|
|
};
|
|
|
|
export function usePreviewRun(id: string | undefined) {
|
|
return useSWRMutation(
|
|
id ? queryKeys.runs.preview(id) : null,
|
|
async (_key, { arg }: { arg: PreviewRunArg }): Promise<PreviewMutationResult> => {
|
|
const result = await apiData<PreviewUrlResponse>(() =>
|
|
humanInTheLoopApi.generatePreviewUrl(id!, arg),
|
|
);
|
|
return { intent: "preview", url: result.url };
|
|
},
|
|
);
|
|
}
|
|
|
|
export function useCancelRun(id: string | undefined) {
|
|
return useLifecycleMutation(id, "cancel", cancelRun);
|
|
}
|
|
|
|
export function useApproveRun(id: string | undefined) {
|
|
return useLifecycleMutation(id, "approve", approveRun);
|
|
}
|
|
|
|
export function useDenyRun(id: string | undefined) {
|
|
return useLifecycleMutation(id, "deny", denyRun);
|
|
}
|
|
|
|
export function useArchiveRun(id: string | undefined) {
|
|
return useLifecycleMutation(id, "archive", archiveRun);
|
|
}
|
|
|
|
export function useUnarchiveRun(id: string | undefined) {
|
|
return useLifecycleMutation(id, "unarchive", unarchiveRun);
|
|
}
|
|
|
|
export function useRetryRun(id: string | undefined) {
|
|
return useLifecycleMutation(id, "retry", retryRun, (run, mutate) => {
|
|
void mutate(queryKeys.runs.detail(run.id), run, { revalidate: false });
|
|
if (run.parent_id) {
|
|
mutateRunListCaches(mutate);
|
|
}
|
|
});
|
|
}
|
|
|
|
function useLifecycleMutation(
|
|
id: string | undefined,
|
|
intent: LifecycleAction,
|
|
action: (id: string) => Promise<Run>,
|
|
onSuccessExtra?: (run: Run, mutate: ScopedMutator) => void,
|
|
) {
|
|
const { mutate } = useSWRConfig();
|
|
const key = id ? queryKeys.runs[intent](id) : null;
|
|
return useSWRMutation(
|
|
key,
|
|
async (): Promise<LifecycleMutationResult> => {
|
|
if (!id) {
|
|
return { intent, ok: false, error: null };
|
|
}
|
|
try {
|
|
return { intent, ok: true, run: await action(id) };
|
|
} catch (error) {
|
|
return {
|
|
intent,
|
|
ok: false,
|
|
error: isLifecycleActionError(error) ? error : null,
|
|
};
|
|
}
|
|
},
|
|
{
|
|
onSuccess: (result) => {
|
|
if (!id || !result.ok) return;
|
|
if (intent !== "retry") {
|
|
// Retry doesn't mutate the source run, so skip invalidating its detail/billing keys.
|
|
void mutate(queryKeys.runs.detail(id));
|
|
void mutate(queryKeys.runs.billing(id));
|
|
}
|
|
mutateRunListCaches(mutate);
|
|
onSuccessExtra?.(result.run, mutate);
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
export function useUpdateRunTitle(id: string | undefined) {
|
|
const { mutate } = useSWRConfig();
|
|
return useSWRMutation(
|
|
id ? queryKeys.runs.updateTitle(id) : null,
|
|
async (_key, { arg }: { arg: UpdateRunRequest }): Promise<Run> => {
|
|
if (!id) throw new Error("id is required");
|
|
return apiData(() => runsApi.updateRun(id, arg));
|
|
},
|
|
{
|
|
onSuccess: (run) => {
|
|
if (!id) return;
|
|
void mutate(queryKeys.runs.detail(id), run, { revalidate: false });
|
|
mutateRunListCaches(mutate);
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
export type SubmitInterviewAnswerArg = {
|
|
questionId: string;
|
|
answer: SubmitAnswerRequest;
|
|
};
|
|
|
|
export function useSubmitInterviewAnswer(runId: string | undefined) {
|
|
const { mutate } = useSWRConfig();
|
|
return useSWRMutation(
|
|
runId ? `interview-answer:${runId}` : null,
|
|
async (_key: string, { arg }: { arg: SubmitInterviewAnswerArg }) => {
|
|
if (!runId) throw new Error("runId is required");
|
|
await apiData(() =>
|
|
humanInTheLoopApi.submitRunAnswer(runId, arg.questionId, arg.answer),
|
|
);
|
|
},
|
|
{
|
|
onSuccess: () => {
|
|
if (!runId) return;
|
|
void mutate(queryKeys.runs.questions(runId, 25, 0));
|
|
void mutate(queryKeys.runs.detail(runId));
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
export function useInterruptRun(runId: string | undefined) {
|
|
const { mutate } = useSWRConfig();
|
|
return useSWRMutation(
|
|
runId ? `interrupt-run:${runId}` : null,
|
|
async (_key: string) => {
|
|
if (!runId) throw new Error("runId is required");
|
|
await apiData(() => humanInTheLoopApi.interruptRun(runId));
|
|
},
|
|
{
|
|
onSuccess: () => {
|
|
if (!runId) return;
|
|
void mutate(queryKeys.runs.detail(runId));
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
export function useSteerRun(runId: string | undefined) {
|
|
const { mutate } = useSWRConfig();
|
|
return useSWRMutation(
|
|
runId ? `steer-run:${runId}` : null,
|
|
async (_key: string, { arg }: { arg: SteerRunRequest }) => {
|
|
if (!runId) throw new Error("runId is required");
|
|
await apiData(() => humanInTheLoopApi.steerRun(runId, arg));
|
|
},
|
|
{
|
|
onSuccess: () => {
|
|
if (!runId) return;
|
|
void mutate(queryKeys.runs.detail(runId));
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
export function useToggleDemoMode() {
|
|
const { mutate } = useSWRConfig();
|
|
return useSWRMutation(
|
|
queryKeys.demo.toggle(),
|
|
async (_key, { arg }: { arg: { enabled: boolean } }) => {
|
|
await apiData(() => authApi.toggleDemo(arg));
|
|
},
|
|
{
|
|
onSuccess: () => {
|
|
void mutate(queryKeys.auth.me());
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
export function useLoginDevToken() {
|
|
return useSWRMutation(
|
|
queryKeys.auth.loginDevToken(),
|
|
async (_key, { arg }: { arg: { token: string } }) => {
|
|
return apiData(() => authApi.loginDevToken(arg), {
|
|
redirectOnUnauthorized: false,
|
|
});
|
|
},
|
|
);
|
|
}
|