From adba73fcca644a888011dc425748c189fdecab5a Mon Sep 17 00:00:00 2001 From: Matt Ciaccio Date: Wed, 6 May 2026 15:14:50 +0200 Subject: [PATCH] feat(bulk): wire bulk action UI on companies list The /api/v1/companies/bulk endpoint shipped in the previous bulk batch but the UI side was deferred. Mirrors the client-list / yacht-list pattern: Add tag, Remove tag, Archive bulk actions with a single TagPicker dialog for tag operations and a window.confirm for the destructive archive. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/companies/company-list.tsx | 117 +++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/src/components/companies/company-list.tsx b/src/components/companies/company-list.tsx index ee12103..12a1489 100644 --- a/src/components/companies/company-list.tsx +++ b/src/components/companies/company-list.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { useParams } from 'next/navigation'; -import { Plus } from 'lucide-react'; +import { Plus, Archive, Tag as TagIcon, TagsIcon } from 'lucide-react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; @@ -14,6 +14,15 @@ import { EmptyState } from '@/components/shared/empty-state'; import { TableSkeleton } from '@/components/shared/loading-skeleton'; import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog'; import { PermissionGate } from '@/components/shared/permission-gate'; +import { TagPicker } from '@/components/shared/tag-picker'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { CompanyCard } from '@/components/companies/company-card'; import { CompanyForm } from '@/components/companies/company-form'; import { companyFilterDefinitions } from '@/components/companies/company-filters'; @@ -30,6 +39,30 @@ export function CompanyList() { const [createOpen, setCreateOpen] = useState(false); const [editCompany, setEditCompany] = useState(null); const [archiveCompany, setArchiveCompany] = useState(null); + const [tagDialog, setTagDialog] = useState<{ ids: string[]; mode: 'add' | 'remove' } | null>( + null, + ); + const [tagChoice, setTagChoice] = useState([]); + + const bulkMutation = useMutation({ + mutationFn: async ( + payload: + | { action: 'archive'; ids: string[] } + | { action: 'add_tag'; ids: string[]; tagId: string } + | { action: 'remove_tag'; ids: string[]; tagId: string }, + ) => + apiFetch<{ data: { summary: { total: number; succeeded: number; failed: number } } }>( + '/api/v1/companies/bulk', + { method: 'POST', body: payload }, + ), + onSuccess: (res) => { + queryClient.invalidateQueries({ queryKey: ['companies'] }); + const s = res.data.summary; + if (s.failed > 0) { + alert(`${s.succeeded} of ${s.total} succeeded. ${s.failed} failed.`); + } + }, + }); const { data, @@ -124,6 +157,42 @@ export function CompanyList() { onSortChange={setSort} isLoading={isFetching && !isLoading} getRowId={(row) => row.id} + bulkActions={[ + { + label: 'Add tag', + icon: TagIcon, + onClick: (ids) => { + if (ids.length === 0) return; + setTagChoice([]); + setTagDialog({ ids, mode: 'add' }); + }, + }, + { + label: 'Remove tag', + icon: TagsIcon, + onClick: (ids) => { + if (ids.length === 0) return; + setTagChoice([]); + setTagDialog({ ids, mode: 'remove' }); + }, + }, + { + label: 'Archive', + icon: Archive, + variant: 'destructive', + onClick: (ids) => { + if (ids.length === 0) return; + if ( + !window.confirm( + `Archive ${ids.length} compan${ids.length === 1 ? 'y' : 'ies'}? This can be undone from the archived list.`, + ) + ) { + return; + } + bulkMutation.mutate({ action: 'archive', ids }); + }, + }, + ]} cardRender={(row) => ( )} + !o && setTagDialog(null)}> + + + {tagDialog?.mode === 'add' ? 'Add tag' : 'Remove tag'} + + {tagDialog?.mode === 'add' + ? `Add a tag to ${tagDialog?.ids.length ?? 0} selected compan${tagDialog?.ids.length === 1 ? 'y' : 'ies'}.` + : `Remove a tag from ${tagDialog?.ids.length ?? 0} selected compan${tagDialog?.ids.length === 1 ? 'y' : 'ies'}. Companies without the tag are unchanged.`} + + +
+ setTagChoice(ids.slice(-1))} + placeholder="Pick one tag…" + /> +

+ Pick a single tag. To apply multiple tags, run the action once per tag. +

+
+ + + + +
+
+ {editCompany && (