feat(KB-629): complete Step 4 — add bulk edit toolbar with model dropdowns
This commit is contained in:
@@ -2,9 +2,10 @@ import { useState, useCallback, useMemo, Fragment, useEffect, useRef } from "rea
|
||||
import { LayoutGrid, List as ListIcon, ArrowUpDown, ArrowUp, ArrowDown, Search, Link, Columns3, EyeOff, Eye, ChevronRight } from "lucide-react";
|
||||
import type { Task, TaskDetail, Column, TaskStep, TaskCreateInput } from "@fusion/core";
|
||||
import { COLUMN_LABELS, COLUMNS } from "@fusion/core";
|
||||
import { fetchTaskDetail } from "../api";
|
||||
import { fetchTaskDetail, batchUpdateTaskModels } from "../api";
|
||||
import type { ModelInfo } from "../api";
|
||||
import { QuickEntryBox } from "./QuickEntryBox";
|
||||
import { CustomModelDropdown } from "./CustomModelDropdown";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
|
||||
const COLUMN_COLOR_MAP: Record<Column, string> = {
|
||||
@@ -42,6 +43,11 @@ interface ListViewProps {
|
||||
* Called when the user clicks the "Subtask" button in the quick entry box.
|
||||
*/
|
||||
onSubtaskBreakdown?: (description: string) => void;
|
||||
/**
|
||||
* Called when tasks are updated (e.g., after bulk model update).
|
||||
* Allows parent to refresh task list or handle optimistically.
|
||||
*/
|
||||
onTasksUpdated?: (updatedTasks: Task[]) => void;
|
||||
}
|
||||
|
||||
function getStepProgress(steps: TaskStep[]): string {
|
||||
@@ -67,6 +73,7 @@ export function ListView({
|
||||
availableModels,
|
||||
onPlanningMode,
|
||||
onSubtaskBreakdown,
|
||||
onTasksUpdated,
|
||||
}: ListViewProps) {
|
||||
const [sortField, setSortField] = useState<SortField>("id");
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>("desc");
|
||||
@@ -240,6 +247,96 @@ export function ListView({
|
||||
return selectedCount > 0 && selectedCount < visibleTaskIds.length;
|
||||
}, [groupedTasks, selectedTaskIds]);
|
||||
|
||||
// Bulk edit state
|
||||
const [executorModel, setExecutorModel] = useState<string>("__no_change__");
|
||||
const [validatorModel, setValidatorModel] = useState<string>("__no_change__");
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
|
||||
// Handle apply bulk model update
|
||||
const handleApplyBulkUpdate = useCallback(async () => {
|
||||
if (selectedTaskIds.size === 0) return;
|
||||
|
||||
const taskIds = Array.from(selectedTaskIds).filter((id) => {
|
||||
const task = tasks.find((t) => t.id === id);
|
||||
return task && task.column !== "archived";
|
||||
});
|
||||
|
||||
if (taskIds.length === 0) {
|
||||
addToast("No valid tasks to update (archived tasks cannot be modified)", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build payload - only include fields that changed from "__no_change__"
|
||||
const payload: {
|
||||
taskIds: string[];
|
||||
modelProvider?: string | null;
|
||||
modelId?: string | null;
|
||||
validatorModelProvider?: string | null;
|
||||
validatorModelId?: string | null;
|
||||
} = { taskIds };
|
||||
|
||||
if (executorModel !== "__no_change__") {
|
||||
if (executorModel === "") {
|
||||
// "Use default" - clear override
|
||||
payload.modelProvider = null;
|
||||
payload.modelId = null;
|
||||
} else {
|
||||
const slashIdx = executorModel.indexOf("/");
|
||||
if (slashIdx !== -1) {
|
||||
payload.modelProvider = executorModel.slice(0, slashIdx);
|
||||
payload.modelId = executorModel.slice(slashIdx + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validatorModel !== "__no_change__") {
|
||||
if (validatorModel === "") {
|
||||
// "Use default" - clear override
|
||||
payload.validatorModelProvider = null;
|
||||
payload.validatorModelId = null;
|
||||
} else {
|
||||
const slashIdx = validatorModel.indexOf("/");
|
||||
if (slashIdx !== -1) {
|
||||
payload.validatorModelProvider = validatorModel.slice(0, slashIdx);
|
||||
payload.validatorModelId = validatorModel.slice(slashIdx + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any changes were made
|
||||
if (Object.keys(payload).length === 1) {
|
||||
addToast("No changes to apply", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsApplying(true);
|
||||
try {
|
||||
const result = await batchUpdateTaskModels(
|
||||
payload.taskIds,
|
||||
payload.modelProvider,
|
||||
payload.modelId,
|
||||
payload.validatorModelProvider,
|
||||
payload.validatorModelId,
|
||||
);
|
||||
|
||||
// Optimistically update parent with returned tasks
|
||||
if (onTasksUpdated && result.updated.length > 0) {
|
||||
onTasksUpdated(result.updated);
|
||||
}
|
||||
|
||||
addToast(`Updated ${result.count} task${result.count === 1 ? "" : "s"}`, "success");
|
||||
|
||||
// Reset state
|
||||
clearSelection();
|
||||
setExecutorModel("__no_change__");
|
||||
setValidatorModel("__no_change__");
|
||||
} catch (err: any) {
|
||||
addToast(err.message || "Failed to update models", "error");
|
||||
} finally {
|
||||
setIsApplying(false);
|
||||
}
|
||||
}, [selectedTaskIds, tasks, executorModel, validatorModel, addToast, clearSelection, onTasksUpdated]);
|
||||
|
||||
// Toggle a column's visibility
|
||||
const toggleColumn = useCallback((column: ListColumn) => {
|
||||
setVisibleColumns((prev) => {
|
||||
@@ -551,6 +648,37 @@ export function ListView({
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Bulk Edit Toolbar */}
|
||||
{selectedTaskIds.size > 0 && availableModels && availableModels.length > 0 && (
|
||||
<div className="bulk-edit-toolbar">
|
||||
<span className="bulk-edit-label">Bulk Edit Models:</span>
|
||||
<div className="bulk-edit-dropdown">
|
||||
<CustomModelDropdown
|
||||
models={availableModels}
|
||||
value={executorModel === "__no_change__" ? "" : executorModel}
|
||||
onChange={(value) => setExecutorModel(value === "" ? "__no_change__" : value)}
|
||||
label="Executor Model"
|
||||
placeholder="No change"
|
||||
/>
|
||||
</div>
|
||||
<div className="bulk-edit-dropdown">
|
||||
<CustomModelDropdown
|
||||
models={availableModels}
|
||||
value={validatorModel === "__no_change__" ? "" : validatorModel}
|
||||
onChange={(value) => setValidatorModel(value === "" ? "__no_change__" : value)}
|
||||
label="Validator Model"
|
||||
placeholder="No change"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary btn-sm bulk-edit-apply-btn"
|
||||
onClick={handleApplyBulkUpdate}
|
||||
disabled={isApplying || (executorModel === "__no_change__" && validatorModel === "__no_change__")}
|
||||
>
|
||||
{isApplying ? "Applying..." : "Apply"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{onNewTask ? (
|
||||
<button className="btn btn-primary btn-sm" onClick={onNewTask}>
|
||||
+ New Task
|
||||
|
||||
@@ -3738,6 +3738,79 @@ body {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Selection stats */
|
||||
.list-selection-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
background: var(--todo-bg, rgba(59, 130, 246, 0.1));
|
||||
border: 1px solid var(--todo-border, rgba(59, 130, 246, 0.3));
|
||||
border-radius: var(--radius);
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.selection-count {
|
||||
font-weight: 500;
|
||||
color: var(--todo);
|
||||
}
|
||||
|
||||
/* Bulk edit toolbar */
|
||||
.bulk-edit-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
background: var(--surface-hover, rgba(0, 0, 0, 0.03));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.bulk-edit-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bulk-edit-dropdown {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.bulk-edit-dropdown .model-combobox-trigger {
|
||||
font-size: 12px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.bulk-edit-apply-btn {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bulk-edit-apply-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Checkbox column in table */
|
||||
.list-header-checkbox,
|
||||
.list-cell-checkbox {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
.list-header-checkbox input[type="checkbox"],
|
||||
.list-cell-checkbox input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.list-cell-checkbox input[type="checkbox"]:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Column toggle dropdown */
|
||||
.list-column-toggle {
|
||||
position: relative;
|
||||
|
||||
Reference in New Issue
Block a user