From 03fbead9c8f9f941d6facdeb1f186d6fa92bc02a Mon Sep 17 00:00:00 2001 From: Arnas Donauskas Date: Thu, 16 Apr 2026 22:59:28 +0300 Subject: [PATCH] fix(pipelines): size columns to fill, batch stage saves, inline delete confirm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sizing: - Board columns grow to fill wide viewports (flex-1 basis-[260px]) instead of being capped at 300px; still overflow-scroll on narrow screens. - Analytics grid: 2 cols mobile → 3 cols sm → 6 cols xl (was lg, which cramped labels on 1024-1280px widths). Senior-review pass: - pipeline-settings handleSave: replace N sequential UPDATEs in a for-loop with a single upsert (onConflict: "id"). Previously latency scaled linearly with stage count; now constant. - deal-form delete: swap window.confirm for an inline confirm row that matches the app's pattern (pipeline-settings uses the same inline confirmation for pipeline deletion). - pipeline-board: add KeyboardSensor alongside PointerSensor so deal cards are draggable via keyboard for a11y (Space to pick up, arrows to move). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/pipelines/deal-form.tsx | 51 +++++++++++++------ .../pipelines/pipeline-analytics.tsx | 2 +- src/components/pipelines/pipeline-board.tsx | 6 ++- .../pipelines/pipeline-settings.tsx | 41 ++++++++------- 4 files changed, 66 insertions(+), 34 deletions(-) diff --git a/src/components/pipelines/deal-form.tsx b/src/components/pipelines/deal-form.tsx index 52fa0a5..1027a6f 100644 --- a/src/components/pipelines/deal-form.tsx +++ b/src/components/pipelines/deal-form.tsx @@ -69,10 +69,12 @@ export function DealForm({ const [saving, setSaving] = useState(false); const [statusAction, setStatusAction] = useState(null); const [deleting, setDeleting] = useState(false); + const [confirmDelete, setConfirmDelete] = useState(false); // Reset when opening useEffect(() => { if (!open) return; + setConfirmDelete(false); if (deal) { setTitle(deal.title); setValue(String(deal.value ?? "")); @@ -211,10 +213,6 @@ export function DealForm({ async function handleDelete() { if (!deal) return; - const confirmed = window.confirm( - "Delete this deal? This cannot be undone.", - ); - if (!confirmed) return; setDeleting(true); const { error } = await supabase.from("deals").delete().eq("id", deal.id); setDeleting(false); @@ -223,6 +221,7 @@ export function DealForm({ return; } toast.success("Deal deleted"); + setConfirmDelete(false); onOpenChange(false); onSaved(); } @@ -426,17 +425,39 @@ export function DealForm({ - {deal && ( - - )} + {deal && + (confirmDelete ? ( +
+ Delete this deal? +
+ + +
+
+ ) : ( + + ))} diff --git a/src/components/pipelines/pipeline-analytics.tsx b/src/components/pipelines/pipeline-analytics.tsx index 769ce2f..f13f135 100644 --- a/src/components/pipelines/pipeline-analytics.tsx +++ b/src/components/pipelines/pipeline-analytics.tsx @@ -99,7 +99,7 @@ export function PipelineAnalytics({ stages, deals }: PipelineAnalyticsProps) { return ( -
+
} label="Total Deals" diff --git a/src/components/pipelines/pipeline-board.tsx b/src/components/pipelines/pipeline-board.tsx index dee50ca..ddfa1d9 100644 --- a/src/components/pipelines/pipeline-board.tsx +++ b/src/components/pipelines/pipeline-board.tsx @@ -4,6 +4,7 @@ import { useMemo, useState } from "react"; import { DndContext, DragOverlay, + KeyboardSensor, PointerSensor, useSensor, useSensors, @@ -62,6 +63,9 @@ export function PipelineBoard({ const sensors = useSensors( // 5px activation distance avoids clicks being interpreted as drags. useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), + // Keyboard drag support: focus a card, Space to pick up, arrows to move, + // Space to drop, Escape to cancel. + useSensor(KeyboardSensor), ); const activeDeal = activeDealId @@ -172,7 +176,7 @@ function StageColumn({ const { setNodeRef, isOver } = useDroppable({ id: stage.id }); return ( -
+
{/* 3px colored top border — sits above the column's padding */}
({ + id: s.id, + pipeline_id: s.pipeline_id, + name: s.name, + color: s.color, + position: i, + })); + + const [renameRes, stagesRes] = await Promise.all([ + supabase + .from("pipelines") + .update({ name: name.trim() }) + .eq("id", pipeline.id), + supabase.from("pipeline_stages").upsert(stageRows, { onConflict: "id" }), + ]); + + setSaving(false); + + if (renameRes.error || stagesRes.error) { + toast.error("Failed to save pipeline"); return; } - // Persist each stage's name/color/position - for (let i = 0; i < localStages.length; i++) { - const s = localStages[i]; - await supabase - .from("pipeline_stages") - .update({ name: s.name, color: s.color, position: i }) - .eq("id", s.id); - } - - setSaving(false); onOpenChange(false); onPipelinesChanged(); onStagesChanged();