123 lines
4.6 KiB
TypeScript
123 lines
4.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useCallback, useRef, useState } from "react";
|
|
import {
|
|
listSentryIssueWatches,
|
|
createSentryIssueWatch,
|
|
updateSentryIssueWatch,
|
|
deleteSentryIssueWatch,
|
|
triggerSentryIssueWatch,
|
|
previewResetSentryIssueWatch,
|
|
resetSentryIssueWatch,
|
|
} from "@/lib/api/domains/sentry-api";
|
|
import type {
|
|
CreateSentryIssueWatchRequest,
|
|
SentryIssueWatch,
|
|
UpdateSentryIssueWatchRequest,
|
|
} from "@/lib/types/sentry";
|
|
|
|
/**
|
|
* useSentryIssueWatches owns the Sentry-watcher list:
|
|
* - workspaceId: string → fetch and operate on watches in one workspace
|
|
* - workspaceId: undefined → fetch every watch across all workspaces
|
|
* - workspaceId: null → don't fetch
|
|
*
|
|
* Mirrors `useLinearIssueWatches`: update/delete/trigger pass the watch's
|
|
* workspace id as the `workspace_id` query param so the backend can guard
|
|
* cross-workspace mutations.
|
|
*/
|
|
export function useSentryIssueWatches(workspaceId?: string | null) {
|
|
const [items, setItems] = useState<SentryIssueWatch[]>([]);
|
|
const [loaded, setLoaded] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
// Track the raw workspaceId (not `?? null`) so the three scopes stay distinct:
|
|
// undefined = fetch all, null = don't fetch, string = one workspace. Collapsing
|
|
// undefined→null would hide an "all → none" transition and leave stale data.
|
|
const initialized = useRef(false);
|
|
const lastWorkspaceId = useRef<string | null | undefined>(undefined);
|
|
|
|
useEffect(() => {
|
|
if (initialized.current && lastWorkspaceId.current !== workspaceId) {
|
|
// Reset cached list on scope change (incl. → null). Also clear `loading`:
|
|
// a fetch from the old scope can no longer complete into the new one (its
|
|
// .finally is gated by `ignore`), and a → null scope starts no replacement
|
|
// fetch, so without this the hook would stay stuck at loading=true.
|
|
/* eslint-disable react-hooks/set-state-in-effect -- resetting cached state when scope changes */
|
|
setItems([]);
|
|
setLoaded(false);
|
|
setLoading(false);
|
|
/* eslint-enable react-hooks/set-state-in-effect */
|
|
}
|
|
initialized.current = true;
|
|
lastWorkspaceId.current = workspaceId;
|
|
}, [workspaceId]);
|
|
|
|
useEffect(() => {
|
|
if (workspaceId === null || loaded) return;
|
|
// `loading` is intentionally NOT a dependency: setLoading(true) below would
|
|
// otherwise re-run this effect, whose cleanup sets ignore=true on the
|
|
// in-flight request, so its .finally skips setLoading(false) and the hook
|
|
// sticks at loading=true forever. The `loaded` guard already prevents a
|
|
// duplicate fetch, and `ignore` handles a workspaceId change mid-flight.
|
|
let ignore = false;
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- starting external fetch
|
|
setLoading(true);
|
|
listSentryIssueWatches(workspaceId ?? undefined, { cache: "no-store" })
|
|
.then((res) => {
|
|
if (ignore) return;
|
|
setItems(res ?? []);
|
|
setLoaded(true);
|
|
})
|
|
.catch(() => {
|
|
if (ignore) return;
|
|
setItems([]);
|
|
setLoaded(true);
|
|
})
|
|
.finally(() => {
|
|
if (!ignore) setLoading(false);
|
|
});
|
|
return () => {
|
|
ignore = true;
|
|
};
|
|
}, [workspaceId, loaded]);
|
|
|
|
const create = useCallback(async (req: CreateSentryIssueWatchRequest) => {
|
|
const watch = await createSentryIssueWatch(req);
|
|
setItems((prev) => [...prev, watch]);
|
|
return watch;
|
|
}, []);
|
|
|
|
const update = useCallback(
|
|
async (id: string, watchWorkspaceId: string, req: UpdateSentryIssueWatchRequest) => {
|
|
const watch = await updateSentryIssueWatch(id, watchWorkspaceId, req);
|
|
setItems((prev) => prev.map((w) => (w.id === watch.id ? watch : w)));
|
|
return watch;
|
|
},
|
|
[],
|
|
);
|
|
|
|
const remove = useCallback(async (id: string, watchWorkspaceId: string) => {
|
|
await deleteSentryIssueWatch(id, watchWorkspaceId);
|
|
setItems((prev) => prev.filter((w) => w.id !== id));
|
|
}, []);
|
|
|
|
const trigger = useCallback(async (id: string, watchWorkspaceId: string) => {
|
|
return triggerSentryIssueWatch(id, watchWorkspaceId);
|
|
}, []);
|
|
|
|
const previewReset = useCallback(async (id: string, watchWorkspaceId: string) => {
|
|
return previewResetSentryIssueWatch(id, watchWorkspaceId);
|
|
}, []);
|
|
|
|
const reset = useCallback(async (id: string, watchWorkspaceId: string) => {
|
|
const res = await resetSentryIssueWatch(id, watchWorkspaceId);
|
|
// Force the next render to refetch so the now-empty dedup set / cleared
|
|
// last_polled_at land in the UI immediately.
|
|
setLoaded(false);
|
|
return res;
|
|
}, []);
|
|
|
|
return { items, loaded, loading, create, update, remove, trigger, previewReset, reset };
|
|
}
|