Refine issue workflow surfaces and live updates

This commit is contained in:
dotta 2026-04-09 10:26:17 -05:00
parent b4a58ba8a6
commit 03dff1a29a
48 changed files with 2800 additions and 1163 deletions

View file

@ -1,3 +1,5 @@
import type { Issue } from "@paperclipai/shared";
type IssueDetailSource = "issues" | "inbox";
type IssueDetailBreadcrumb = {
@ -5,10 +7,23 @@ type IssueDetailBreadcrumb = {
href: string;
};
export type IssueDetailHeaderSeed = {
id: string;
identifier: string | null;
title: string;
status: Issue["status"];
priority: Issue["priority"];
projectId: string | null;
projectName: string | null;
originKind?: Issue["originKind"];
originId?: string | null;
};
type IssueDetailLocationState = {
issueDetailBreadcrumb?: IssueDetailBreadcrumb;
issueDetailSource?: IssueDetailSource;
issueDetailInboxQuickArchiveArmed?: boolean;
issueDetailHeaderSeed?: IssueDetailHeaderSeed;
};
const ISSUE_DETAIL_SOURCE_QUERY_PARAM = "from";
@ -25,6 +40,58 @@ function isIssueDetailSource(value: unknown): value is IssueDetailSource {
return value === "issues" || value === "inbox";
}
function isIssueDetailHeaderSeed(value: unknown): value is IssueDetailHeaderSeed {
if (typeof value !== "object" || value === null) return false;
const candidate = value as Partial<IssueDetailHeaderSeed>;
const hasOriginKind =
candidate.originKind === undefined || typeof candidate.originKind === "string";
const hasOriginId =
candidate.originId === undefined || candidate.originId === null || typeof candidate.originId === "string";
return (
typeof candidate.id === "string"
&& (candidate.identifier === null || typeof candidate.identifier === "string")
&& typeof candidate.title === "string"
&& typeof candidate.status === "string"
&& typeof candidate.priority === "string"
&& (candidate.projectId === null || typeof candidate.projectId === "string")
&& (candidate.projectName === null || typeof candidate.projectName === "string")
&& hasOriginKind
&& hasOriginId
);
}
function createIssueDetailHeaderSeed(issue: Issue): IssueDetailHeaderSeed {
return {
id: issue.id,
identifier: issue.identifier ?? null,
title: issue.title,
status: issue.status,
priority: issue.priority,
projectId: issue.projectId ?? null,
projectName: issue.project?.name ?? null,
originKind: issue.originKind,
originId: issue.originId ?? null,
};
}
export function withIssueDetailHeaderSeed(state: unknown, issue: Issue): IssueDetailLocationState {
const headerSeed = createIssueDetailHeaderSeed(issue);
if (typeof state !== "object" || state === null) {
return { issueDetailHeaderSeed: headerSeed };
}
return {
...(state as IssueDetailLocationState),
issueDetailHeaderSeed: headerSeed,
};
}
export function readIssueDetailHeaderSeed(state: unknown): IssueDetailHeaderSeed | null {
if (typeof state !== "object" || state === null) return null;
const candidate = (state as IssueDetailLocationState).issueDetailHeaderSeed;
return isIssueDetailHeaderSeed(candidate) ? candidate : null;
}
function readIssueDetailSource(state: unknown): IssueDetailSource | null {
if (typeof state !== "object" || state === null) return null;
const source = (state as IssueDetailLocationState).issueDetailSource;
@ -96,10 +163,14 @@ function readStoredIssueDetailLocationState(issuePathId: string): IssueDetailLoc
: null;
const source = inferIssueDetailSource(parsed, breadcrumb);
if (!breadcrumb || !source) return null;
const headerSeed = isIssueDetailHeaderSeed(parsed.issueDetailHeaderSeed)
? parsed.issueDetailHeaderSeed
: undefined;
return {
issueDetailBreadcrumb: breadcrumb,
issueDetailSource: source,
issueDetailInboxQuickArchiveArmed: parsed.issueDetailInboxQuickArchiveArmed === true,
issueDetailHeaderSeed: headerSeed,
};
} catch {
return null;
@ -115,11 +186,13 @@ function normalizeIssueDetailLocationState(
if (isIssueDetailBreadcrumb(candidate)) {
const source = inferIssueDetailSource(state as Partial<IssueDetailLocationState>, candidate);
if (!source) return null;
const headerSeed = readIssueDetailHeaderSeed(state) ?? undefined;
return {
issueDetailBreadcrumb: candidate,
issueDetailSource: source,
issueDetailInboxQuickArchiveArmed:
(state as IssueDetailLocationState).issueDetailInboxQuickArchiveArmed === true,
issueDetailHeaderSeed: headerSeed,
};
}
}