"use client"; import { useMemo, useRef, useState } from "react"; import { IconAdjustments, IconArrowLeft, IconCheck, IconChevronDown, IconPlus, } from "@tabler/icons-react"; import { Button } from "@kandev/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@kandev/ui/dropdown-menu"; import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "@kandev/ui/drawer"; import { Popover, PopoverContent, PopoverTrigger } from "@kandev/ui/popover"; import { useTranslation } from "react-i18next"; import { useAppStore } from "@/components/state-provider"; import { isActionConfirmationTarget } from "@/components/confirmation/action-confirm-popover"; import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint"; import { DEFAULT_THREAD_VIEW, MAX_THREAD_VIEWS, threadViewName, } from "@/lib/state/slices/ui/thread-view-builtins"; import type { ThreadCandidate, ThreadViewQueryResult } from "@/lib/threads/thread-view-query"; import type { ThreadView, ThreadViewDraft } from "@/lib/state/slices/ui/thread-view-types"; import type { Repository } from "@/lib/types/http"; import { ThreadsViewEditor } from "./threads-view-editor"; type Props = Pick & { candidates: ThreadCandidate[]; repositories?: ReadonlyArray>; admittedCount: number; }; // eslint-disable-next-line max-lines-per-function -- Coordinates the desktop and touch entry points for one saved-view state adapter. export function ThreadsViewControls({ candidates, repositories = [], admittedCount, matchingCount, hiddenCount, }: Props) { const { t } = useTranslation(); const { usesDesktopWorkbench } = useResponsiveBreakpoint(); const views = useAppStore((state) => state.threadViews.views); const activeViewId = useAppStore((state) => state.threadViews.activeViewId); const draft = useAppStore((state) => state.threadViews.draft); const syncError = useAppStore((state) => state.threadViews.syncError); const setActiveView = useAppStore((state) => state.setThreadActiveView); const createView = useAppStore((state) => state.createThreadView); const updateDraft = useAppStore((state) => state.updateThreadViewDraft); const save = useAppStore((state) => state.saveThreadViewDraftOverwrite); const saveAs = useAppStore((state) => state.saveThreadViewDraftAs); const discard = useAppStore((state) => state.discardThreadViewDraft); const deleteView = useAppStore((state) => state.deleteThreadView); const renameView = useAppStore((state) => state.renameThreadView); const duplicateView = useAppStore((state) => state.duplicateThreadView); const reapplySort = useAppStore((state) => state.reapplyThreadViewSort); const retrySync = useAppStore((state) => state.retryThreadViewSync); const clearSyncError = useAppStore((state) => state.clearThreadViewSyncError); const [settingsOpen, setSettingsOpen] = useState(false); const openSettingsAfterPickerClose = useRef(false); const settingsContentRef = useRef(null); const activeView = useMemo( () => views.find((view) => view.id === activeViewId) ?? DEFAULT_THREAD_VIEW, [activeViewId, views], ); const activeViewName = threadViewName(activeView, t); const hasDraft = !!draft && draft.baseViewId === activeView.id; let disabledReason: string | null = null; if (draft) disabledReason = t("threads:saveOrDiscardBeforeNewView"); else if (views.length >= MAX_THREAD_VIEWS) { disabledReason = t("threads:viewLimitReached", { count: MAX_THREAD_VIEWS }); } function startNewView() { if (disabledReason) return; if (!createView()) return; openSettingsAfterPickerClose.current = true; } if (!usesDesktopWorkbench) { return ( 1} onSetActiveView={setActiveView} onCreateView={createView} onUpdate={updateDraft} onSave={save} onSaveAs={saveAs} onDiscard={discard} onRename={(name) => renameView(activeView.id, name)} onDelete={deleteView} onDuplicate={() => duplicateView(activeView.id, "")} onReapplySort={reapplySort} onRetrySync={retrySync} onDismissSyncError={clearSyncError} /> ); } return (
{syncError && ( )} { if (!openSettingsAfterPickerClose.current) return; event.preventDefault(); openSettingsAfterPickerClose.current = false; setSettingsOpen(true); }} > {views.map((view) => ( setActiveView(view.id)} data-testid={`threads-view-option-${view.id}`} className="cursor-pointer gap-2 text-xs" > {threadViewName(view, t)} ))} {t("threads:newView")} {disabledReason && ( {disabledReason} )} { if (isActionConfirmationTarget(event.target)) event.preventDefault(); }} onInteractOutside={(event) => { if (isActionConfirmationTarget(event.target)) event.preventDefault(); }} > 1} onUpdate={updateDraft} onSave={save} onSaveAs={saveAs} onDiscard={discard} onRename={(name) => renameView(activeView.id, name)} deleteFocusBoundaryRef={settingsContentRef} onDelete={(viewId) => { deleteView(viewId); setSettingsOpen(false); }} onDuplicate={() => duplicateView(activeView.id, "")} onReapplySort={reapplySort} /> {t("threads:columnsSummary", { admitted: admittedCount, matching: matchingCount })} {hiddenCount > 0 && ( {t("threads:hiddenCount", { count: hiddenCount })} )}
); } type ThreadViewDraftUpdate = ( patch: Partial>, ) => void; type MobileThreadsViewControlsProps = { activeView: ThreadView; views: ThreadView[]; draft: ThreadViewDraft | null; candidates: ThreadCandidate[]; repositories: ReadonlyArray>; admittedCount: number; matchingCount: number; hiddenCount: number; syncError: string | null; disabledReason: string | null; viewCount: number; canDelete: boolean; onSetActiveView: (id: string) => void; onCreateView: () => string | null; onUpdate: ThreadViewDraftUpdate; onSave: () => void; onSaveAs: (name: string) => void; onDiscard: () => void; onRename: (name: string) => void; onDelete: (viewId: string) => void; onDuplicate: () => void; onReapplySort: () => void; onRetrySync: () => void; onDismissSyncError: () => void; }; // eslint-disable-next-line max-lines-per-function -- Keeps the single mobile drawer lifecycle and its shared editor wiring together. function MobileThreadsViewControls({ activeView, views, draft, candidates, repositories, admittedCount, matchingCount, hiddenCount, syncError, disabledReason, viewCount, canDelete, onSetActiveView, onCreateView, onUpdate, onSave, onSaveAs, onDiscard, onRename, onDelete, onDuplicate, onReapplySort, onRetrySync, onDismissSyncError, }: MobileThreadsViewControlsProps) { const { t } = useTranslation(); const activeViewName = threadViewName(activeView, t); const [open, setOpen] = useState(false); const [page, setPage] = useState<"views" | "editor">("views"); const triggerRef = useRef(null); function openViews() { setPage("views"); setOpen(true); } function startNewView() { if (disabledReason) return; if (onCreateView()) setPage("editor"); } function closeDrawer() { setOpen(false); triggerRef.current?.focus(); } function selectView(id: string) { onSetActiveView(id); closeDrawer(); } return ( <> {syncError && ( )} { setOpen(nextOpen); if (nextOpen) setPage("views"); }} > { event.preventDefault(); triggerRef.current?.focus(); }} onAnimationEnd={(event) => { if (event.currentTarget.dataset.state === "closed") { triggerRef.current?.focus(); } }} >
{page === "editor" && ( )} {page === "editor" ? t("threads:viewSettings") : t("threads:title")}
{page === "views" ? ( setPage("editor")} /> ) : ( { onDelete(viewId); closeDrawer(); }} onDuplicate={onDuplicate} onReapplySort={onReapplySort} /> )}
); } function ThreadViewSyncError({ error, mobile = false, onRetry, onDismiss, }: { error: string; mobile?: boolean; onRetry: () => void; onDismiss: () => void; }) { const { t } = useTranslation(); return (
{t("threads:failedToSyncViews")}: {error}
); } function MobileThreadViewList({ activeView, views, admittedCount, matchingCount, hiddenCount, disabledReason, onSelect, onNewView, onOpenSettings, }: { activeView: ThreadView; views: ThreadView[]; admittedCount: number; matchingCount: number; hiddenCount: number; disabledReason: string | null; onSelect: (id: string) => void; onNewView: () => void; onOpenSettings: () => void; }) { const { t } = useTranslation(); return (
{t("threads:columnsSummary", { admitted: admittedCount, matching: matchingCount })} {hiddenCount > 0 && ( {t("threads:hiddenCount", { count: hiddenCount })} )}
{views.map((view) => ( ))}
); }