"use client";
import { IconExternalLink, IconPlus, IconRefresh } from "@tabler/icons-react";
import { Badge } from "@kandev/ui/badge";
import { Button } from "@kandev/ui/button";
import { Dialog, DialogContent, DialogTitle } from "@kandev/ui/dialog";
import type { LinearIssue, LinearWorkflowState } from "@/lib/types/linear";
import {
LinearErrorMessage,
PersonCell,
formatRelative,
priorityClass,
stateBadgeClass,
useIssueState,
type IssueState,
} from "./linear-issue-common";
type LinearIssueDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
workspaceId: string | null | undefined;
identifier: string | null | undefined;
/** Shown while the full issue is loading so the dialog doesn't flash empty. */
initialIssue?: LinearIssue | null;
/** When set, the dialog footer shows a Start task button that calls this. */
onStartTask?: (issue: LinearIssue) => void;
};
function dialogTitle(issue: LinearIssue | null, identifier: string | null | undefined): string {
return issue?.title ?? identifier ?? "Linear issue";
}
function effectiveIdentifier(
issue: LinearIssue | null,
identifier: string | null | undefined,
): string {
return identifier ?? issue?.identifier ?? "";
}
export function LinearIssueDialog({
open,
onOpenChange,
workspaceId,
identifier,
initialIssue,
onStartTask,
}: LinearIssueDialogProps) {
const enabled = open && !!workspaceId && !!identifier;
const state = useIssueState(workspaceId ?? "", identifier ?? "", enabled);
const issue = state.issue ?? initialIssue ?? null;
const errorOnly = state.error !== null && issue === null;
const showFooter = issue !== null && onStartTask !== undefined;
// The "Start task" button hands the dialog's issue back to the caller and
// closes — the caller is responsible for opening the task creation flow.
const handleStart = () => {
if (issue && onStartTask) {
onStartTask(issue);
onOpenChange(false);
}
};
return (
{dialogTitle(issue, identifier)}
{errorOnly ? (
) : (
<>
void state.load()}
/>
{showFooter && }
>
)}
);
}
function IssueFooter({ onStart }: { onStart: () => void }) {
return (
Start task
);
}
function DialogBody({
issue,
state,
identifier,
}: {
issue: LinearIssue | null;
state: IssueState;
identifier: string;
}) {
return (
{state.error && issue && (
)}
{!issue && state.loading && (
Loading issue…
)}
{issue &&
}
);
}
type TopBarProps = {
issue: LinearIssue | null;
loading: boolean;
onRefresh: () => void;
};
function IssueTopBar({ issue, loading, onRefresh }: TopBarProps) {
return (
);
}
function IssueBody({
issue,
state,
identifier,
}: {
issue: LinearIssue;
state: IssueState;
identifier: string;
}) {
return (
void state.handleStateChange(id)}
/>
);
}
function IssueHeading({ issue, identifier }: { issue: LinearIssue; identifier: string }) {
return (
{identifier}
{issue.teamKey && (
<>
·
{issue.teamKey}
>
)}
{issue.title}
);
}
function SectionHeading({ children }: { children: React.ReactNode }) {
return {children}
;
}
function DescriptionSection({ description }: { description: string }) {
return (
Description
{description ? (
{description}
) : (
No description.
)}
);
}
type StateBlockProps = {
issue: LinearIssue;
states: LinearWorkflowState[] | null | undefined;
pending: string | null;
onChange: (stateId: string) => void;
};
// Hide the issue's current state from the available transitions list — moving
// to the same state would be a no-op and clutters the UI.
function visibleStates(issue: LinearIssue, list: LinearWorkflowState[]): LinearWorkflowState[] {
return list.filter((s) => s.id !== issue.stateId);
}
function StateBlock({ issue, states, pending, onChange }: StateBlockProps) {
const list = states ?? [];
const next = visibleStates(issue, list);
return (
{issue.stateName && (
{issue.stateName}
)}
{next.length > 0 && (
{next.map((s) => (
onChange(s.id)}
>
{pending === s.id ? "…" : `→ ${s.name}`}
))}
)}
);
}
function DetailsCard({ issue }: { issue: LinearIssue }) {
return (
Details
{issue.priorityLabel || "—"}
{issue.teamKey}
);
}
function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
);
}
function MetaFooter({ issue }: { issue: LinearIssue }) {
const updated = formatRelative(issue.updated);
if (!updated) return null;
return (
Updated {updated}
);
}