Files
multica/packages/views/issues/components/batch-action-toolbar.tsx
Jiayuan Zhang 89fd2ce96e refactor(views): reuse AssigneePicker in CreateIssueModal (#599)
* 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.
2026-04-10 02:18:49 +08:00

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>
</>
);
}