mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-23 18:17:44 +02:00
* refactor(views): reuse AssigneePicker in CreateIssueModal Replace the hand-rolled inline assignee Popover in CreateIssueModal with the shared AssigneePicker component. This fixes missing features (private agent permission checks, lock icon, disabled state, selection checkmark) and ensures consistent behavior across all assignee dropdowns. * refactor(views): consolidate all picker components across the codebase Enhance shared pickers (StatusPicker, PriorityPicker, DueDatePicker, ProjectPicker) with triggerRender, controlled open/onOpenChange, and align props — matching the AssigneePicker API. Replace inline implementations in: - create-issue.tsx: Status, Priority, DueDate, Project (4 pickers) - issue-detail.tsx sidebar: Status, Priority (2 pickers) - batch-action-toolbar.tsx: Status, Priority (2 pickers) StatusPicker now has its first consumer (was defined but unused). Removes ~200 lines of duplicated picker code.
147 lines
4.7 KiB
TypeScript
147 lines
4.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { X, Trash2 } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { Button } from "@multica/ui/components/ui/button";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@multica/ui/components/ui/alert-dialog";
|
|
import type { UpdateIssueRequest } from "@multica/core/types";
|
|
import { useIssueSelectionStore } from "@multica/core/issues/stores/selection-store";
|
|
import { useBatchUpdateIssues, useBatchDeleteIssues } from "@multica/core/issues/mutations";
|
|
import { StatusPicker, PriorityPicker, AssigneePicker } from "./pickers";
|
|
|
|
export function BatchActionToolbar() {
|
|
const selectedIds = useIssueSelectionStore((s) => s.selectedIds);
|
|
const clear = useIssueSelectionStore((s) => s.clear);
|
|
const count = selectedIds.size;
|
|
|
|
const [statusOpen, setStatusOpen] = useState(false);
|
|
const [priorityOpen, setPriorityOpen] = useState(false);
|
|
const [assigneeOpen, setAssigneeOpen] = useState(false);
|
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
const batchUpdate = useBatchUpdateIssues();
|
|
const batchDelete = useBatchDeleteIssues();
|
|
const loading = batchUpdate.isPending || batchDelete.isPending;
|
|
|
|
if (count === 0) return null;
|
|
|
|
const ids = Array.from(selectedIds);
|
|
|
|
const handleBatchUpdate = async (updates: Partial<UpdateIssueRequest>) => {
|
|
try {
|
|
await batchUpdate.mutateAsync({ ids, updates });
|
|
toast.success(`Updated ${count} issue${count > 1 ? "s" : ""}`);
|
|
} catch {
|
|
toast.error("Failed to update issues");
|
|
}
|
|
};
|
|
|
|
const handleBatchDelete = async () => {
|
|
try {
|
|
await batchDelete.mutateAsync(ids);
|
|
clear();
|
|
toast.success(`Deleted ${count} issue${count > 1 ? "s" : ""}`);
|
|
} catch {
|
|
toast.error("Failed to delete issues");
|
|
} finally {
|
|
setDeleteOpen(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 flex items-center gap-1 rounded-lg border bg-background px-2 py-1.5 shadow-lg">
|
|
<div className="flex items-center gap-1.5 pl-1 pr-2 border-r mr-1">
|
|
<span className="text-sm font-medium">{count} selected</span>
|
|
<button
|
|
type="button"
|
|
onClick={clear}
|
|
className="rounded p-0.5 hover:bg-accent transition-colors"
|
|
>
|
|
<X className="size-3.5 text-muted-foreground" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Status */}
|
|
<StatusPicker
|
|
status="todo"
|
|
onUpdate={handleBatchUpdate}
|
|
open={statusOpen}
|
|
onOpenChange={setStatusOpen}
|
|
triggerRender={<Button variant="ghost" size="sm" disabled={loading} />}
|
|
trigger="Status"
|
|
align="center"
|
|
/>
|
|
|
|
{/* Priority */}
|
|
<PriorityPicker
|
|
priority="none"
|
|
onUpdate={handleBatchUpdate}
|
|
open={priorityOpen}
|
|
onOpenChange={setPriorityOpen}
|
|
triggerRender={<Button variant="ghost" size="sm" disabled={loading} />}
|
|
trigger="Priority"
|
|
align="center"
|
|
/>
|
|
|
|
{/* Assignee */}
|
|
<AssigneePicker
|
|
assigneeType={null}
|
|
assigneeId={null}
|
|
onUpdate={handleBatchUpdate}
|
|
open={assigneeOpen}
|
|
onOpenChange={setAssigneeOpen}
|
|
triggerRender={<Button variant="ghost" size="sm" disabled={loading} />}
|
|
trigger="Assignee"
|
|
align="center"
|
|
/>
|
|
|
|
{/* Delete */}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
disabled={loading}
|
|
onClick={() => setDeleteOpen(true)}
|
|
className="text-destructive hover:text-destructive"
|
|
>
|
|
<Trash2 className="size-3.5 mr-1" />
|
|
Delete
|
|
</Button>
|
|
</div>
|
|
|
|
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>
|
|
Delete {count} issue{count > 1 ? "s" : ""}?
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
This action cannot be undone. This will permanently delete the
|
|
selected issue{count > 1 ? "s" : ""} and all associated data.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={handleBatchDelete}
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
>
|
|
Delete
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</>
|
|
);
|
|
}
|
|
|