feat(HAI-005): complete Step 2 — add auto-merge toggle UI to In Review column

This commit is contained in:
Dustin Byrne
2026-03-25 20:43:41 -04:00
parent dd3ebe9148
commit 8fcc425433
4 changed files with 85 additions and 3 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useCallback, useEffect } from "react";
import type { TaskDetail, TaskCreateInput, Task } from "@hai/core";
import { fetchConfig } from "./api";
import { fetchConfig, fetchSettings, updateSettings } from "./api";
import { Header } from "./components/Header";
import { Board } from "./components/Board";
import { TaskDetailModal } from "./components/TaskDetailModal";
@@ -14,12 +14,16 @@ function AppInner() {
const [detailTask, setDetailTask] = useState<TaskDetail | null>(null);
const [settingsOpen, setSettingsOpen] = useState(false);
const [maxConcurrent, setMaxConcurrent] = useState(2);
const [autoMerge, setAutoMerge] = useState(false);
const { tasks, createTask, moveTask, deleteTask, mergeTask } = useTasks();
useEffect(() => {
fetchConfig()
.then((cfg) => setMaxConcurrent(cfg.maxConcurrent))
.catch(() => {/* keep default */});
fetchSettings()
.then((s) => setAutoMerge(!!s.autoMerge))
.catch(() => {/* keep default */});
}, []);
const { toasts, addToast, removeToast } = useToast();
@@ -35,6 +39,16 @@ function AppInner() {
[createTask],
);
const handleToggleAutoMerge = useCallback(async () => {
const next = !autoMerge;
setAutoMerge(next);
try {
await updateSettings({ autoMerge: next });
} catch {
setAutoMerge(!next); // revert on failure
}
}, [autoMerge]);
const handleDetailOpen = useCallback((task: TaskDetail) => {
setDetailTask(task);
}, []);
@@ -54,6 +68,8 @@ function AppInner() {
onCancelCreate={handleCancelCreate}
onCreateTask={handleCreateTask}
onNewTask={handleCreateOpen}
autoMerge={autoMerge}
onToggleAutoMerge={handleToggleAutoMerge}
/>
{detailTask && (
<TaskDetailModal

View File

@@ -13,9 +13,11 @@ interface BoardProps {
onCancelCreate: () => void;
onCreateTask: (input: TaskCreateInput) => Promise<Task>;
onNewTask: () => void;
autoMerge: boolean;
onToggleAutoMerge: () => void;
}
export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, isCreating, onCancelCreate, onCreateTask, onNewTask }: BoardProps) {
export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, isCreating, onCancelCreate, onCreateTask, onNewTask, autoMerge, onToggleAutoMerge }: BoardProps) {
return (
<main className="board" id="board">
{COLUMNS.map((col) => (
@@ -29,6 +31,7 @@ export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast
onOpenDetail={onOpenDetail}
addToast={addToast}
{...(col === "triage" ? { isCreating, onCancelCreate, onCreateTask, onNewTask } : {})}
{...(col === "in-review" ? { autoMerge, onToggleAutoMerge } : {})}
/>
))}
</main>

View File

@@ -19,9 +19,11 @@ interface ColumnProps {
onCancelCreate?: () => void;
onCreateTask?: (input: TaskCreateInput) => Promise<Task>;
onNewTask?: () => void;
autoMerge?: boolean;
onToggleAutoMerge?: () => void;
}
export function Column({ column, tasks, allTasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, isCreating, onCancelCreate, onCreateTask, onNewTask }: ColumnProps) {
export function Column({ column, tasks, allTasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, isCreating, onCancelCreate, onCreateTask, onNewTask, autoMerge, onToggleAutoMerge }: ColumnProps) {
const [dragOver, setDragOver] = useState(false);
const handleDragOver = useCallback((e: React.DragEvent) => {
@@ -62,6 +64,17 @@ export function Column({ column, tasks, allTasks, maxConcurrent, onMoveTask, onO
<div className={`column-dot dot-${column}`} />
<h2>{COLUMN_LABELS[column]}</h2>
<span className="column-count">{tasks.length}</span>
{column === "in-review" && onToggleAutoMerge && (
<label className="auto-merge-toggle" title={autoMerge ? "Auto-merge enabled" : "Auto-merge disabled"}>
<input
type="checkbox"
checked={!!autoMerge}
onChange={onToggleAutoMerge}
/>
<span className="toggle-slider" />
<span className="toggle-label">Auto-merge</span>
</label>
)}
{onNewTask && (
<button className="btn btn-primary btn-sm" onClick={onNewTask}>
+ New Task

View File

@@ -719,3 +719,53 @@ html, body {
border-radius: var(--radius);
margin: 4px;
}
/* Auto-merge toggle */
.auto-merge-toggle {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
user-select: none;
margin-left: auto;
}
.auto-merge-toggle input {
display: none;
}
.toggle-slider {
position: relative;
width: 28px;
height: 16px;
background: var(--border);
border-radius: 8px;
transition: background 0.2s;
flex-shrink: 0;
}
.toggle-slider::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 12px;
height: 12px;
background: var(--card);
border-radius: 50%;
transition: transform 0.2s;
}
.auto-merge-toggle input:checked + .toggle-slider {
background: var(--in-review);
}
.auto-merge-toggle input:checked + .toggle-slider::after {
transform: translateX(12px);
}
.toggle-label {
font-size: 11px;
color: var(--text-muted);
white-space: nowrap;
}