Files
kandev/apps/web/hooks/domains/linear/use-linear-issue-watches.ts
T
2026-06-15 18:54:16 +01:00

127 lines
4.7 KiB
TypeScript

"use client";
import { useEffect, useCallback, useRef } from "react";
import {
listLinearIssueWatches,
createLinearIssueWatch,
updateLinearIssueWatch,
deleteLinearIssueWatch,
triggerLinearIssueWatch,
previewResetLinearIssueWatch,
resetLinearIssueWatch,
} from "@/lib/api/domains/linear-api";
import { useAppStore } from "@/components/state-provider";
import type { CreateLinearIssueWatchInput, UpdateLinearIssueWatchInput } from "@/lib/types/linear";
// WORKSPACE_REQUIRED is thrown by per-row mutation callbacks when the
// install-wide listing case forgets to forward the row's workspaceId.
const WORKSPACE_REQUIRED = "workspaceId required";
/**
* useLinearIssueWatches owns the Linear-watcher list:
* - workspaceId: string → fetch and operate on watches in one workspace
* - workspaceId: undefined → fetch every watch across all workspaces; the
* caller supplies workspaceId to update/remove/trigger
* calls per-row (those endpoints still validate it
* against the watch's stored workspace as an IDOR guard)
* - workspaceId: null → don't fetch
*
* Mirrors `useJiraIssueWatches`. Workspace changes reset the cached list so
* the user doesn't see the previous workspace's stale rows during the swap.
*/
export function useLinearIssueWatches(workspaceId?: string | null) {
const items = useAppStore((s) => s.linearIssueWatches.items);
const loaded = useAppStore((s) => s.linearIssueWatches.loaded);
const loading = useAppStore((s) => s.linearIssueWatches.loading);
const setWatches = useAppStore((s) => s.setLinearIssueWatches);
const resetWatches = useAppStore((s) => s.resetLinearIssueWatches);
const setLoading = useAppStore((s) => s.setLinearIssueWatchesLoading);
const addWatch = useAppStore((s) => s.addLinearIssueWatch);
const updateWatch = useAppStore((s) => s.updateLinearIssueWatch);
const removeWatch = useAppStore((s) => s.removeLinearIssueWatch);
const lastScope = useRef<string | null | undefined>(undefined);
const scope: string | null = workspaceId ?? null;
useEffect(() => {
if (workspaceId === null) return;
if (lastScope.current !== undefined && lastScope.current !== scope) {
resetWatches();
}
lastScope.current = scope;
}, [workspaceId, scope, resetWatches]);
useEffect(() => {
if (workspaceId === null || loaded || loading) return;
setLoading(true);
listLinearIssueWatches(workspaceId ?? undefined, { cache: "no-store" })
.then((res) => setWatches(res ?? []))
.catch(() => setWatches([]))
.finally(() => setLoading(false));
}, [workspaceId, loaded, loading, setWatches, setLoading]);
const create = useCallback(
async (req: CreateLinearIssueWatchInput) => {
const watch = await createLinearIssueWatch(req);
addWatch(watch);
return watch;
},
[addWatch],
);
// Per-row mutations require the row's own workspace_id to satisfy the
// backend's IDOR guard. Callers pass it explicitly when the hook itself
// wasn't bound to a single workspace (the install-wide listing case).
const update = useCallback(
async (id: string, req: UpdateLinearIssueWatchInput, rowWorkspaceId?: string) => {
const ws = rowWorkspaceId ?? workspaceId;
if (!ws) throw new Error(WORKSPACE_REQUIRED);
const watch = await updateLinearIssueWatch(ws, id, req);
updateWatch(watch);
return watch;
},
[workspaceId, updateWatch],
);
const remove = useCallback(
async (id: string, rowWorkspaceId?: string) => {
const ws = rowWorkspaceId ?? workspaceId;
if (!ws) throw new Error(WORKSPACE_REQUIRED);
await deleteLinearIssueWatch(ws, id);
removeWatch(id);
},
[workspaceId, removeWatch],
);
const trigger = useCallback(
async (id: string, rowWorkspaceId?: string) => {
const ws = rowWorkspaceId ?? workspaceId;
if (!ws) throw new Error(WORKSPACE_REQUIRED);
return triggerLinearIssueWatch(ws, id);
},
[workspaceId],
);
const previewReset = useCallback(
async (id: string, rowWorkspaceId?: string) => {
const ws = rowWorkspaceId ?? workspaceId;
if (!ws) throw new Error(WORKSPACE_REQUIRED);
return previewResetLinearIssueWatch(ws, id);
},
[workspaceId],
);
const reset = useCallback(
async (id: string, rowWorkspaceId?: string) => {
const ws = rowWorkspaceId ?? workspaceId;
if (!ws) throw new Error(WORKSPACE_REQUIRED);
const res = await resetLinearIssueWatch(ws, id);
resetWatches();
return res;
},
[workspaceId, resetWatches],
);
return { items, loaded, loading, create, update, remove, trigger, previewReset, reset };
}