"use client"; import type { ReactNode, RefObject } from "react"; import { IconPlus } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { Button } from "@kandev/ui/button"; import { Input } from "@kandev/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select"; import type { ThreadCandidate } from "@/lib/threads/thread-view-query"; import type { ThreadFilterClause, ThreadSortSpec, ThreadTaskScope, ThreadView, ThreadViewDraft, } from "@/lib/state/slices/ui/thread-view-types"; import { TypedSortPicker } from "@/components/task/sidebar-filter/sort-picker-primitive"; import { ThreadsViewFilterRow } from "./threads-view-filter-row"; import { EditorActions, SectionLabel } from "./threads-view-editor-actions"; import { MAX_THREAD_VIEW_FILTERS, THREAD_SORT_OPTIONS, createThreadFilterClause, } from "./threads-view-filter-registry"; import { MAX_THREAD_VIEW_COLUMNS } from "./threads-view-editor-utils"; import { threadViewName } from "@/lib/state/slices/ui/thread-view-builtins"; type ThreadViewDraftUpdate = ( patch: Partial>, ) => void; export type EditorBodyProps = { activeView: ThreadView; current: ThreadView | ThreadViewDraft; candidates: ThreadCandidate[]; repositoryNames: ReadonlyMap; mobile: boolean; showHeader: boolean; nameMode: "rename" | "saveAs" | null; name: string; maxColumnsInput: string; maxColumnsInvalid: boolean; invalidSelectedScope: boolean; invalidDraft: boolean; hasDraft: boolean; viewCount: number; canDelete: boolean; onNameChange: (name: string) => void; onNameModeChange: (mode: "rename" | "saveAs" | null) => void; onUpdate: ThreadViewDraftUpdate; onSave: () => void; onSaveAs: (name: string) => void; onRename: (name: string) => void; onDiscard: () => void; onDelete: () => void; deleteAnchorRef?: RefObject; deleteConfirmation?: ReactNode; onDuplicate: () => void; onReapplySort: () => void; onSetMaxColumns: (value: string, badInput?: boolean) => void; onOpenPicker: () => void; }; export function EditorBody({ activeView, current, candidates, repositoryNames, mobile, showHeader, nameMode, name, maxColumnsInput, maxColumnsInvalid, invalidSelectedScope, invalidDraft, hasDraft, viewCount, canDelete, onNameChange, onNameModeChange, onUpdate, onSave, onSaveAs, onRename, onDiscard, onDelete, deleteAnchorRef, deleteConfirmation, onDuplicate, onReapplySort, onSetMaxColumns, onOpenPicker, }: EditorBodyProps) { return (
{showHeader && ( { if (nameMode === "rename") onRename(value); if (nameMode === "saveAs") onSaveAs(value); onNameModeChange(null); onNameChange(""); }} onDuplicate={onDuplicate} /> )}
); } type EditorSectionsProps = Pick< EditorBodyProps, | "current" | "candidates" | "repositoryNames" | "mobile" | "invalidSelectedScope" | "invalidDraft" | "hasDraft" | "viewCount" | "canDelete" | "maxColumnsInput" | "maxColumnsInvalid" | "onUpdate" | "onSave" | "onDiscard" | "onDelete" | "deleteAnchorRef" | "deleteConfirmation" | "onNameChange" | "onNameModeChange" | "onReapplySort" | "onSetMaxColumns" | "onOpenPicker" >; function EditorSections({ current, candidates, repositoryNames, mobile, invalidSelectedScope, invalidDraft, hasDraft, viewCount, canDelete, maxColumnsInput, maxColumnsInvalid, onUpdate, onSave, onDiscard, onDelete, deleteAnchorRef, deleteConfirmation, onNameChange, onNameModeChange, onReapplySort, onSetMaxColumns, onOpenPicker, }: EditorSectionsProps) { return ( <> onUpdate({ taskScope: scope })} onOpenPicker={onOpenPicker} /> { if (current.filters.length < MAX_THREAD_VIEW_FILTERS) { onUpdate({ filters: [...current.filters, createThreadFilterClause()] }); } }} onChange={(next) => onUpdate({ filters: current.filters.map((filter) => (filter.id === next.id ? next : filter)), }) } onRemove={(id) => onUpdate({ filters: current.filters.filter((filter) => filter.id !== id) }) } /> onUpdate({ sort })} onReapplySort={onReapplySort} /> { onNameChange(""); onNameModeChange("saveAs"); }} onDiscard={onDiscard} onDelete={onDelete} deleteAnchorRef={deleteAnchorRef} deleteConfirmation={deleteConfirmation} /> ); } function EditorHeader({ activeView, nameMode, name, onNameChange, onNameModeChange, onSubmit, onDuplicate, }: { activeView: ThreadView; nameMode: "rename" | "saveAs" | null; name: string; onNameChange: (name: string) => void; onNameModeChange: (mode: "rename" | "saveAs" | null) => void; onSubmit: (name: string) => void; onDuplicate: () => void; }) { const { t } = useTranslation(); return (

{t("threads:viewSettings")}

{threadViewName(activeView, t)}

{nameMode ? ( onNameModeChange(null)} onSubmit={onSubmit} /> ) : (
)}
); } function NameEditor({ name, onChange, onCancel, onSubmit, }: { name: string; onChange: (name: string) => void; onCancel: () => void; onSubmit: (name: string) => void; }) { const { t } = useTranslation(); const submit = () => { const trimmed = name.trim(); if (trimmed) onSubmit(trimmed); }; return (
onChange(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") submit(); if (event.key === "Escape") onCancel(); }} placeholder={t("threads:viewName")} aria-label={t("threads:viewName")} data-testid="threads-view-name-input" />
); } function TaskScopeSection({ current, invalid, mobile, onChange, onOpenPicker, }: { current: Pick; invalid: boolean; mobile: boolean; onChange: (scope: ThreadTaskScope) => void; onOpenPicker: () => void; }) { const { t } = useTranslation(); const setMode = (mode: ThreadTaskScope["mode"]) => { onChange( mode === "all" ? { mode: "all", taskIds: [] } : { mode: "selected", taskIds: current.taskScope.mode === "selected" ? [...current.taskScope.taskIds] : [], }, ); }; return (
{t("threads:taskScope")} {current.taskScope.mode === "selected" && ( <> {invalid && (

{t("threads:selectAtLeastOneTask")}

)} )}
); } function ThreadFilterSection({ filters, candidates, repositoryNames, mobile, onAdd, onChange, onRemove, }: { filters: ThreadFilterClause[]; candidates: ThreadCandidate[]; repositoryNames: ReadonlyMap; mobile: boolean; onAdd: () => void; onChange: (next: ThreadFilterClause) => void; onRemove: (id: string) => void; }) { const { t } = useTranslation(); const atLimit = filters.length >= MAX_THREAD_VIEW_FILTERS; return (
{t("threads:filters")}
{atLimit && (

{t("threads:filterLimitReached", { count: MAX_THREAD_VIEW_FILTERS })}

)} {filters.map((filter) => ( onRemove(filter.id)} /> ))}
); } function ThreadSortSection({ sort, mobile, onChange, onReapplySort, }: { sort: ThreadSortSpec; mobile: boolean; onChange: (sort: ThreadSortSpec) => void; onReapplySort: () => void; }) { const { t } = useTranslation(); return (
{t("threads:sort")}
({ key: option.key, label: t(option.labelKey), description: t(option.descriptionKey), }))} onChange={onChange} mobile={mobile} directionLabel={t("threads:toggleSortDirection")} testIds={{ key: "threads-sort-select", direction: "threads-sort-direction" }} />
); } function MaxColumnsSection({ value, invalid, mobile, onChange, }: { value: string; invalid: boolean; mobile: boolean; onChange: (value: string, badInput?: boolean) => void; }) { const { t } = useTranslation(); return (
onChange(event.target.value, event.target.validity.badInput)} className={`${mobile ? "h-11" : "h-9"} text-xs`} placeholder={t("threads:noColumnLimit")} data-testid="threads-max-columns" aria-invalid={invalid} aria-describedby={invalid ? "threads-max-columns-error" : undefined} /> {invalid && ( )}
); }