"use client"; import { IconRefresh, IconPlayerPlay, IconPlayerPause, IconRestore } from "@tabler/icons-react"; import { Button } from "@kandev/ui/button"; import { Badge } from "@kandev/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@kandev/ui/table"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { useAppStore } from "@/components/state-provider"; import type { LinearIssueWatch, LinearSearchFilter } from "@/lib/types/linear"; import { useTranslation } from "react-i18next"; import type { TFunction } from "i18next"; import { formatRelative } from "@/lib/i18n/formats"; import { WatcherDeleteAction } from "@/components/watches/watcher-delete-action"; import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint"; type LinearIssueWatchTableProps = { watches: LinearIssueWatch[]; dirtyIds: ReadonlySet; // showWorkspace renders a Workspace column when the table aggregates rows // from every workspace (install-wide settings page). showWorkspace?: boolean; onEdit: (watch: LinearIssueWatch) => void; onDelete: (id: string) => void; onTrigger: (id: string) => void; onReset: (id: string) => void; onToggleEnabled: (watch: LinearIssueWatch) => void; }; // `t` is threaded in rather than read from a hook: this is a plain function, and // the guard never inspects a return value, so a literal here would survive lint. // // The bucket copy now comes from `formatRelative`, which owns the same // just-now/m/h/d ladder for every surface and reads the active app locale. Two // consequences, both deliberate and matching the Jira table: the first bucket // reads "just now" rather than "Just now" (it is shared with the rest of the // app), and an unparseable timestamp renders empty instead of "NaNm ago". function formatLastPolled(t: TFunction, dateStr?: string | null): string { if (!dateStr) return t("linear:never"); return formatRelative(dateStr); } // summarizeFilter renders the structured filter as a short tag-style label // the user can scan at a glance. Falls back to "(any)" when somehow empty — // the backend rejects empty filters at create/update time, so this is just // defensive for forward-compat. // // Deliberately NOT translated. This is a filter *expression*, not prose: the // `team:` / `state:` / `assigned:` / `q:` prefixes are Linear filter field // names, the values are user data (a team key, a free-text query), and it is // rendered in a monospace column. It is the direct analogue of the Jira watch // table's column, which shows the raw JQL verbatim for the same reason. function summarizeFilter(filter: LinearSearchFilter | undefined): string { if (!filter) return "(any)"; const parts: string[] = []; if (filter.teamKey) parts.push(`team:${filter.teamKey}`); if (filter.stateIds && filter.stateIds.length > 0) { parts.push(`state:${filter.stateIds.length} selected`); } if (filter.assigned) parts.push(`assigned:${filter.assigned}`); if (filter.query) parts.push(`q:"${filter.query}"`); return parts.length === 0 ? "(any)" : parts.join(" · "); } function WatchActions({ watch, isDirty, onToggleEnabled, onTrigger, onReset, onDelete, }: { watch: LinearIssueWatch; isDirty: boolean; onToggleEnabled: (watch: LinearIssueWatch) => void; onTrigger: (id: string) => void; onReset: (id: string) => void; onDelete: (id: string) => void; }) { const { t } = useTranslation(); const { isFinePointer } = useResponsiveBreakpoint(); const actionSize = isFinePointer ? "h-7 w-7" : "h-11 w-11"; return (
event.stopPropagation()} > {watch.enabled ? t("linear:pause") : t("linear:enable")} {t("linear:checkNow")} {t("common:reset")} onDelete(watch.id)} />
); } function MobileWatchCard({ watch, isDirty, showWorkspace, workspaceName, onEdit, onToggleEnabled, onTrigger, onReset, onDelete, }: { watch: LinearIssueWatch; isDirty: boolean; showWorkspace?: boolean; workspaceName: (id: string) => string; onEdit: (watch: LinearIssueWatch) => void; onToggleEnabled: (watch: LinearIssueWatch) => void; onTrigger: (id: string) => void; onReset: (id: string) => void; onDelete: (id: string) => void; }) { const { t } = useTranslation(); const workspaceLabel = showWorkspace ? workspaceName(watch.workspaceId) : null; return (
); } function DesktopLinearIssueWatchTable({ watches, dirtyIds, showWorkspace, onEdit, onDelete, onTrigger, onReset, onToggleEnabled, workspaceName, }: LinearIssueWatchTableProps & { workspaceName: (id: string) => string }) { const { t } = useTranslation(); return ( {showWorkspace && {t("common:workspace")}} {t("linear:filter")} {t("linear:interval")} {t("linear:lastPolled")} {t("common:status")} {t("linear:actions")} {watches.map((watch) => ( { if ( event.target instanceof Element && event.target.closest("[data-watcher-actions]") ) { return; } onEdit(watch); }} > {showWorkspace && ( {workspaceName(watch.workspaceId)} )} {summarizeFilter(watch.filter)} {t("linear:intervalMinutes", { count: Math.round(watch.pollIntervalSeconds / 60), })} {formatLastPolled(t, watch.lastPolledAt)} {watch.enabled ? t("linear:active") : t("linear:paused")} ))}
); } export function LinearIssueWatchTable({ watches, dirtyIds, showWorkspace, onEdit, onDelete, onTrigger, onReset, onToggleEnabled, }: LinearIssueWatchTableProps) { const { t } = useTranslation(); const workspaces = useAppStore((s) => s.workspaces.items); const { usesDesktopWorkbench = true } = useResponsiveBreakpoint(); const workspaceName = (id: string) => workspaces.find((w) => w.id === id)?.name ?? id; if (watches.length === 0) { return (

{t("linear:noLinearWatchersConfiguredCreateOne")}

); } if (!usesDesktopWorkbench) { return (
{watches.map((watch) => ( ))}
); } return ( ); }