"use client"; import { IconPlayerPause, IconPlayerPlay, IconEdit, IconRefresh, IconRestore, IconTrash, } from "@tabler/icons-react"; import { Badge } from "@kandev/ui/badge"; import { Button } from "@kandev/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@kandev/ui/table"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import type { IssueWatch, ReviewWatch } from "@/lib/types/gitlab"; import { useTranslation } from "react-i18next"; import type { TFunction } from "i18next"; import { formatDateTime } from "@/lib/i18n/formats"; type Watch = ReviewWatch | IssueWatch; export type WatchTableProps = { watches: TWatch[]; dirtyIds: ReadonlySet; authoritativeEnabledById: ReadonlyMap; onEdit: (watch: TWatch) => void; onDelete: (id: string) => void; onTrigger: (id: string) => void; onReset: (id: string) => void; onToggleEnabled: (watch: TWatch) => void; }; type ActionProps = Omit, "watches"> & { watch: TWatch; mobile?: boolean; }; // `t` is threaded in rather than read from a hook: these are plain functions, and // the guard never inspects a return value, so a literal here would survive lint. function checkUnavailableReason( t: TFunction, dirty: boolean, enabled: boolean, ): string | undefined { if (dirty) return t("gitlab:saveChangesBeforeCheckingNow"); if (!enabled) return t("gitlab:enableThisWatchBeforeCheckingNow"); return undefined; } function CheckNowButton({ size, disabledReason, onClick, }: { size: string; disabledReason?: string; onClick: (event: React.MouseEvent) => void; }) { const { t } = useTranslation(); const button = ( ); if (!disabledReason) return button; return ( {button} {disabledReason} ); } function WatchActions({ watch, dirtyIds, authoritativeEnabledById, onEdit, onDelete, onTrigger, onReset, onToggleEnabled, mobile, }: ActionProps) { const { t } = useTranslation(); const size = mobile ? "h-11 w-11" : "h-8 w-8"; const dirty = dirtyIds.has(watch.id); const authoritativeEnabled = authoritativeEnabledById.get(watch.id) ?? watch.enabled; const checkReason = checkUnavailableReason(t, dirty, authoritativeEnabled); const stop = (action: () => void) => (event: React.MouseEvent) => { event.stopPropagation(); action(); }; return (
onTrigger(watch.id))} />
); } // Project paths are user data (namespace/project as GitLab stores it) — only the // empty-selection fallback is copy. function projectSummary(t: TFunction, watch: Watch): string { return watch.projects.length > 0 ? watch.projects.map((project) => project.path).join(", ") : t("gitlab:allProjects"); } // `formatDateTime` follows the active app locale; the previous bare // `toLocaleString()` followed the browser's, so the timestamp could disagree with // the rest of the page after a language switch. function lastPolled(t: TFunction, value?: string): string { if (!value) return t("gitlab:never"); return formatDateTime(value); } function WatchError({ watch }: { watch: Watch }) { if (!watch.last_error) return null; return (

{watch.last_error}

); } function MobileWatchCard(props: ActionProps) { const { t } = useTranslation(); const { watch, onEdit } = props; return (
); } export function GitLabWatchTable(props: WatchTableProps) { const { t } = useTranslation(); if (props.watches.length === 0) { return (

{t("gitlab:noWatchesConfigured")}

); } return ( <>
{props.watches.map((watch) => ( ))}
{t("gitlab:projects")} {t("gitlab:interval")} {t("gitlab:lastChecked")} {t("common:status")} {t("gitlab:actions")} {props.watches.map((watch) => ( props.onEdit(watch)} >

{projectSummary(t, watch)}

{t("gitlab:intervalMinutes", { count: Math.round(watch.poll_interval_seconds / 60), })} {lastPolled(t, watch.last_polled_at)} {watch.enabled ? t("gitlab:active") : t("gitlab:paused")}
))}
); }