Files
kandev/apps/web/components/threads/threads-view-editor.tsx
T

220 lines
6.6 KiB
TypeScript

"use client";
import { useEffect, useMemo, useState, type RefObject } from "react";
import { useTranslation } from "react-i18next";
import { SavedTaskViewDeleteConfirmation } from "@/components/confirmation/saved-task-view-delete-confirmation";
import {
useSavedTaskViewDeleteConfirmation,
type SavedTaskViewDeleteTarget,
} from "@/components/confirmation/use-saved-task-view-delete-confirmation";
import type { ThreadCandidate } 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 { ThreadsViewTaskPicker } from "./threads-view-task-picker";
import { EditorBody, type EditorBodyProps } from "./threads-view-editor-sections";
import { parseThreadMaxColumns } from "./threads-view-editor-utils";
import { threadViewName } from "@/lib/state/slices/ui/thread-view-builtins";
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
type EditorProps = {
activeView: ThreadView;
draft: ThreadViewDraft | null;
candidates: ThreadCandidate[];
repositories: ReadonlyArray<Pick<Repository, "id" | "name">>;
viewCount: number;
canDelete: boolean;
onUpdate: EditorBodyProps["onUpdate"];
onSave: () => void;
onSaveAs: (name: string) => void;
onDiscard: () => void;
onRename: (name: string) => void;
onDelete: (viewId: string) => void;
onDuplicate: () => void;
onReapplySort: () => void;
showHeader?: boolean;
mobile?: boolean;
gridHeightFallback?: boolean;
deleteFocusBoundaryRef?: RefObject<HTMLElement | null>;
};
export function ThreadsViewEditor({
activeView,
draft,
candidates,
repositories,
viewCount,
canDelete,
onUpdate,
onSave,
onSaveAs,
onDiscard,
onRename,
onDelete,
onDuplicate,
onReapplySort,
showHeader = true,
mobile = false,
gridHeightFallback = false,
deleteFocusBoundaryRef,
}: EditorProps) {
const { t } = useTranslation();
const { isMobile } = useResponsiveBreakpoint();
const [pickerOpen, setPickerOpen] = useState(false);
const [nameMode, setNameMode] = useState<"rename" | "saveAs" | null>(null);
const [name, setName] = useState("");
const current = draft && draft.baseViewId === activeView.id ? draft : activeView;
const maxColumns = useMaxColumnsInput(activeView.id, current.maxColumns, onUpdate);
const deletion = useSavedTaskViewDeleteConfirmation<HTMLButtonElement>([activeView]);
const invalidSelectedScope =
current.taskScope.mode === "selected" && current.taskScope.taskIds.length === 0;
const hasDraft = !!draft && draft.baseViewId === activeView.id;
const repositoryNames = useMemo(() => mapRepositoryNames(repositories), [repositories]);
const invalidDraft = invalidSelectedScope || maxColumns.invalid;
const deleteConfirmation = deletion.target ? (
<ThreadViewDeleteConfirmation
deletion={deletion}
mobile={mobile}
focusBoundaryRef={deleteFocusBoundaryRef}
onConfirm={onDelete}
/>
) : null;
if (pickerOpen) {
return (
<TaskPickerEditor
candidates={candidates}
current={current}
onUpdate={onUpdate}
onBack={() => setPickerOpen(false)}
/>
);
}
return (
<>
<EditorBody
activeView={activeView}
current={current}
candidates={candidates}
repositoryNames={repositoryNames}
mobile={mobile}
gridHeightFallback={gridHeightFallback}
showHeader={showHeader}
nameMode={nameMode}
name={name}
maxColumnsInput={maxColumns.input}
maxColumnsInvalid={maxColumns.invalid}
invalidSelectedScope={invalidSelectedScope}
invalidDraft={invalidDraft}
hasDraft={hasDraft}
viewCount={viewCount}
canDelete={canDelete}
onNameChange={setName}
onNameModeChange={setNameMode}
onUpdate={onUpdate}
onSave={onSave}
onSaveAs={onSaveAs}
onRename={onRename}
onDiscard={onDiscard}
onDelete={() =>
deletion.request({ id: activeView.id, label: threadViewName(activeView, t) })
}
deleteAnchorRef={deletion.anchorRef}
deleteConfirmation={mobile && !isMobile ? deleteConfirmation : undefined}
onDuplicate={onDuplicate}
onReapplySort={onReapplySort}
onSetMaxColumns={maxColumns.onChange}
onOpenPicker={() => setPickerOpen(true)}
/>
{!mobile || isMobile ? deleteConfirmation : null}
</>
);
}
function ThreadViewDeleteConfirmation({
deletion,
mobile,
focusBoundaryRef,
onConfirm,
}: {
deletion: {
target: SavedTaskViewDeleteTarget | null;
anchorRef: RefObject<HTMLButtonElement | null>;
close: () => void;
};
mobile: boolean;
focusBoundaryRef?: RefObject<HTMLElement | null>;
onConfirm: (id: string) => void;
}) {
if (!deletion.target) return null;
return (
<SavedTaskViewDeleteConfirmation
target={deletion.target}
presentation={mobile ? "inline" : "popover"}
open
anchorRef={deletion.anchorRef}
focusBoundaryRef={focusBoundaryRef}
onOpenChange={(open) => {
if (!open) deletion.close();
}}
onConfirm={onConfirm}
/>
);
}
function TaskPickerEditor({
candidates,
current,
onUpdate,
onBack,
}: {
candidates: ThreadCandidate[];
current: ThreadView | ThreadViewDraft;
onUpdate: EditorBodyProps["onUpdate"];
onBack: () => void;
}) {
return (
<div className="flex min-h-0 flex-col" data-testid="threads-view-editor">
<ThreadsViewTaskPicker
candidates={candidates}
selectedTaskIds={current.taskScope.mode === "selected" ? current.taskScope.taskIds : []}
onChange={(taskIds) => onUpdate({ taskScope: { mode: "selected", taskIds } })}
onBack={onBack}
/>
</div>
);
}
function mapRepositoryNames(
repositories: ReadonlyArray<Pick<Repository, "id" | "name">>,
): Map<string, string> {
return new Map(repositories.map((repository) => [String(repository.id), repository.name]));
}
function formatMaxColumns(value: number | null): string {
return value === null ? "" : String(value);
}
function useMaxColumnsInput(
viewId: string,
value: number | null,
onUpdate: EditorProps["onUpdate"],
) {
const [input, setInput] = useState(() => formatMaxColumns(value));
const [invalid, setInvalid] = useState(false);
useEffect(() => {
setInput(formatMaxColumns(value));
setInvalid(false);
}, [viewId, value]);
return {
input,
invalid,
onChange: (next: string, badInput = false) => {
setInput(next);
const parsed = parseThreadMaxColumns(next, badInput);
setInvalid(parsed === undefined);
if (parsed !== undefined) onUpdate({ maxColumns: parsed });
},
};
}